☰
HTML转JSON实战:用html-to-json解析网页表格
2026/10/17 1:45:04 网站建设 项目流程

简介:html-to-json是一款将HTML文档(含表格)转换为JSON的Python工具,面向网页数据清洗、信息结构化及爬虫开发场景。它能够解析HTML标签层级,捕获元素属性与文本值,并特别针对表格做了智能处理——优先使用表头作为结果JSON键,免去手工指定键名的繁琐。压缩包共32个文件,以Python源码(7个py)为核心,附带6个HTML测试样例、2个md说明文档、测试用例、YAML工作流配置、Dockerfile和覆盖率配置等,整包仅512KB,结构紧凑。项目包含convert_html.py和convert_html_tables.py两个关键模块,tests目录下既有单元测试也有表格变体样例图,便于理解边界情况;配合README与CHANGELOG,可快速上手安装集成或二次开发。目前已有716人学习下载,适合希望高效批量提取网页结构化数据的开发者研究参考。

1. HTML 转 JSON 的日常困境:为什么我最后选了 html-to-json

如果你处理过从网页采集数据、把旧系统页面里的表格搬到新前端、或者把邮件模板里的结构化内容抽出来,你一定经历过这种尴尬:页面里明明是一个规整的 HTML 表格,复制出来却只剩一堆标签,手动清洗字段能清一下午。我有一段时间每周都要处理十几个类似页面,试过正则硬抠、试过浏览器插件导出,最后稳定用下来的方案是一个很小众但足够老实的库——html-to-json。它干两件事:把任意 HTML 结构整体映射成 JSON 树,以及把 HTML 表格智能转换成以表头为键的 JSON 数组。后者是我用它最频繁的场景。适合谁用?前端、爬虫脚本爱好者、需要做数据迁移的工程师,以及任何不想再写丑陋正则去拆<td>的人。

2. 环境准备与基础转换:从 URL、文件到字符串的三条入口

2.1 安装与项目初始化

先说安装。这个库是标准的 npm 包,Node.js 环境下直接装就行。我一般在独立目录里初始化项目,避免把全局环境搞脏。

mkdir html-to-json-demo cd html-to-json-demo npm init -y npm install html-to-json --save

--save会把依赖写进package.json,这点对后面复现很重要——你装完最好立刻确认node_modules/html-to-json/package.json里的main字段指向哪个入口文件。不同版本的主入口写法不一样,有的直接暴露一个模块对象,有的拆成lib/目录。我习惯在装完后先打开node_modules/html-to-json/package.json看一眼,确认导出名称,免得一上来就 ReferenceError。

安装完成后建议先用 Node 交互模式验证加载是否正常:

node -e "const h = require('html-to-json'); console.log(Object.keys(h));"

这一步能让你看到这个库实际导出了哪些方法。我见过有人在网上复制了老版本 API,结果新版本里方法名已经改了,第一步验证就能避免这种翻车。

2.2 基础模式:把 HTML 结构完整映射成 JSON 树

html-to-json 的基础模式不是针对表格的,而是把整个文档解析成嵌套的 JSON 结构。它的内部工作方式并不神秘:先用一个 HTML 解析器把标签串成 DOM 树,再递归遍历每个节点,把标签名、属性、子节点和文本内容各归各的位置。

const HTMLtoJSON = require('html-to-json'); const htmlString = ` <div class="card"> <h2>商品标题</h2> <p>const HTMLtoJSON = require('html-to-json'); HTMLtoJSON.get('https://example.com/page.html', function (err, $) { if (err) { console.error('抓取失败:', err.message); return; } // 这里拿到的是经过解析的 JSON 树,$ 是简化后的对象 console.log(JSON.stringify($, null, 2)); });

注意get的回调参数和parse不同,第二个参数直接是解析结果,不再需要再调一次parse。这个方法本质上是「请求页面 → 拿 HTML → 解析成 JSON」的合体,少写一步代码。但代价是超时控制比较简略,如果目标站点响应慢,回调会迟迟不来。我一般会在外层套一个setTimeout做兜底,或者先单独抓取 HTML 字符串,确认内容正常后再丢给parse。

从本地文件读取就更直白——没有请求环节,文件系统读取是同步的,适合脚本内联使用:

const fs = require('fs'); const HTMLtoJSON = require('html-to-json'); const htmlContent = fs.readFileSync('./page.html', 'utf-8'); HTMLtoJSON.parse(htmlContent, function (err, result) { if (err) { console.error('解析失败:', err.message); return; } console.log(`解析完成,根节点类型:${result.name || result.type}`); });

readFileSync第二参数指定utf-8编码是必须的,否则拿到的是 Buffer,解析器会直接报错或得到一堆乱码。字符串入口最好用,测试时直接复制一段 HTML 丢进去跑,不需要起服务也不需要建文件。

三种入口的适用边界我给个参考:get适合线上页面一次性抓取,但对超时、重定向、登录态都没什么处理能力;文件入口适合离线批处理,比如把一批.html文件挨个转 JSON;字符串入口适合写单元测试和快速验证。实际项目里我最常用的是「先get后parse」的两段式,因为get出的结果有时已经被预处理过,你看不到原始 HTML,遇到诡异页面很难定位到底哪一步出了错。

3. 表格智能转换:表头如何一步步变成 JSON 的键

3.1 智能表格识别的核心规则

这部分是这个库最值得称道的地方。普通解析器只会把<table>当成一个普通节点,子节点还是<tr>、<td>那一套嵌套结构,你拿到后还得自己遍历才能变成对象数组。html-to-json 的表格模式则直接做到位:识别出<table>结构后,自动把表头那一行(或几行)的文本提取出来作为键,把每个<tr>的数据行变成一个个对象,最终输出一个纯数组。

它的判定逻辑大致是这样:先找到<table>标签,然后检查表结构里是否存在<thead>。如果有<thead>,就取<th>的文本内容作为键;如果没有<thead>,则尝试把第一行<tr>里的单元格当作表头。这就是"智能"两个字的来源——键不是写死的,是从页面里读出来的。识别完成后,后续每一行的<td>会按顺序和表头对齐。单元格少于表头数时缺的键自动跳过,多于表头数时多余的单元格会被忽略。

const HTMLtoJSON = require('html-to-json'); const tableHtml = ` <table> <thead> <tr> <th>姓名</th> <th>部门</th> <th>工号</th> </tr> </thead> <tbody> <tr> <td>张三</td> <td>研发部</td> <td>1001</td> </tr> <tr> <td>李四</td> <td>产品部</td> <td>1002</td> </tr> </tbody> </table> `; HTMLtoJSON.parse(tableHtml, { table: true }, function (err, result) { if (err) throw err; console.log(JSON.stringify(result, null, 2)); });

注意这里的关键是第二个参数{ table: true }。不传这个参数时它走基础模式,传了之后才会启动表格识别逻辑。输出会是一个数组,长度等于<tbody>里的行数,每个元素是一个对象,key是表头文本,value是对应单元格文本。

{ table: true }这个参数在不同版本里写法可能略有变化,有的版本接受{ table: 'auto' },有的直接{ tables: true }。我的经验是安装后先在小样本文档上跑一遍,确认参数名。这个细节我在后面避坑章节会细说。

3.2 典型表格转换的可复现示例

上面代码输出结果的结构大致是:

[ { "姓名": "张三", "部门": "研发部", "工号": "1001" }, { "姓名": "李四", "部门": "产品部", "工号": "1002" } ]

这个结构对绝大多数下游任务来说已经非常理想了——你可以直接把它喂给前端表格组件,也可以JSON.stringify后存进文件,或者逐条 INSERT 进数据库。我实际项目里最常用的是第三种,把表格数据导入到业务系统里做数据迁移。

下面是文件入口的完整可复现脚本,把本地一个 HTML 里的表格变成 JSON 文件:

const fs = require('fs'); const HTMLtoJSON = require('html-to-json'); // 读取本地 HTML 文件 const rawHtml = fs.readFileSync('./products.html', 'utf-8'); // 开启表格智能转换 HTMLtoJSON.parse(rawHtml, { table: true }, function (err, orders) { if (err) { console.error('解析异常:', err.message); process.exit(1); } // 表格模式返回的是数组,如果页面里没有 table 则可能是空数组 console.log(`解析出 ${orders.length} 行数据`); // 写出 JSON 文件 fs.writeFileSync('./products.json', JSON.stringify(orders, null, 2), 'utf-8'); });

这段脚本有几个细节值得展开。第二参数{ table: true }是开启表格模式的关键,漏掉它你会得到一整棵 DOM JSON 树而不是干净的数组,这是最常见的使用错误。回调里orders的类型取决于解析结果——如果页面里恰好有多个<table>,某些版本会返回一个数组的数组(外层每个元素对应一个表格),但这个行为并不稳定,我见过有版本只取第一张表。为了稳妥,我一般先在控制台打一行console.log(orders.length)确认结构,再往下写业务逻辑。

JSON.stringify的第三参数2是缩进空格数,调试阶段建议保留,正式跑批时可以改成0或去掉,文件体积能小不少。写文件时同样必须指定utf-8,不然中文存出来会变成\uXXXX转义序列,虽然合法但人类没法读。

3.3 合并单元格、嵌套表等特殊结构的处理结果

现实世界里的 HTML 表格远远没有示例这么规矩。你一定会遇到rowspan、colspan、表头里混着图片/链接、甚至表格嵌在表格里的情况。html-to-json 对这些的处理策略我认为属于「老实但有限」——它不会像人眼那样智能推断合并逻辑,而是给出一套保守的映射结果。

colspan的情况比较常见。比如表头有一行是「姓名」和「联系方式」,联系方式这个<th>设置了colspan="2",底下两个子列是「手机号」和「邮箱」。这个库的处理方式是:把表头行里的所有文本按顺序压平成一个键数组,colspan并不会让键产生重复,但也不会自动生成层级对象。你得到的是类似{ "姓名": "...", "联系方式手机号": "...", "联系方式邮箱": "..." }的扁平结构,具体拼接规则要看版本实现。

rowspan更麻烦。比如第一列写「部门」竖着合并两行,标准 HTML 里第二行的第一列是没有<td>的。解析器会把缺失的那格当作空值跳过,结果就是第二行对象里缺了「部门」这个键。这不算解析器的错,它是诚实地反映了原表格结构,但下游你要么接受空值,要么自己在后处理阶段做向上填充。

嵌套表格我建议直接避开——如果<td>里又裹了一个<table>,解析器通常会把这个嵌套表当成单元格里的多个文本节点,你得到的是一段拼接在一起的字符串,里面的结构信息全丢了。遇到这种页面,我的做法是先用基础模式把嵌套区域单独拆出来解析,再合并结果。虽然麻烦,但比对抗解析器的内部逻辑要省时间。

这意味着什么?如果你处理的表格是系统导出、模板生成之类比较规整的来源,这个库能给你省下 90% 的体力活。如果表格是手工排版、大量合并单元格的那种,别指望一跑就出完美 JSON,把它当成「先自动转一遍再用脚本修」的半成品,心态会踏实很多。

4. 避坑与常见问题:五个我踩过的坑和对应解法

4.1 坑一:表头有重复文本时键会冲突

现象:表格里两列都叫「操作」,转换后的 JSON 对象里两个键同名,后面的值把前面的覆盖了,数据直接少一列。如果两列都恰好有值,最终只保留最后一个。

原因:这个库把表头文本直接当键名,没有做去重或加下标处理。这符合"智能"的保守边界——它不会猜你到底想区分哪一列。

解决:我在转换后加一层重命名映射。先解析出原始 JSON 数组,然后根据表头顺序做一次显式改名。常见做法是在调用parse之前,先用基础模式单独解析thead拿到表头数组,检查是否有重复项,有就在文本后面拼列号,再进入表格模式。

// 伪代码:先解析表头 HTMLtoJSON.parse(html, function (err, theadResult) { const headers = extractHeaders(theadResult); // 递归找到 th 文本 const seen = {}; const cleanHeaders = headers.map((h, i) => { if (seen[h]) { seen[h] += 1; return `${h}_${seen[h]}`; } seen[h] = 1; return h; }); // 把 cleanHeaders 保存下来,转换后用这个映射去重命名 key });

4.2 坑二:表头写在<tbody>第一行而不是<thead>,键全乱

现象:解析出的数组里对象没有「姓名」「部门」这些键,取而代之的是"0"、"1"、"2"这样的数字键,或者th1、th2之类的占位名。

原因:这个库的表头识别逻辑优先找<thead>,找不到时虽然会尝试把第一个<tr>当表头,但很多页面是直接把表头写在<tbody>里,且用的是<td>而不是<th>标签,这可能导致识别失败或走了另一条分支。

解决:两条路。一是改 HTML——如果页面是你自己的,直接输出规范结构;二是预处理——在调用parse前,把第一行的<td>文本手动提出来,强制设置为表头数组。我不建议用正则去改 HTML 结构,很容易误伤,尤其是表格里嵌套了其他标签时。

4.3 坑三:URL 抓取请求超时,脚本直接挂起

现象:调用get方法访问一个没有响应的域名,回调一直不触发,整个脚本挂在进程里,不报错也不退出。

原因:底层 HTTP 请求没有设置超时选项,或者默认超时时间很长。页面如果遇到重定向循环、DNS 解析慢、或者服务器静默丢弃连接,这个坑就会踩到。

解决:我会在外面包一层超时控制,或者干脆不用get,改成手动请求拿到 HTML 字符串后再进parse。前者写法是给请求对象挂setTimeout定时器,后者思路更干净——独立抓取环节可以做的事情很多,比如设置timeout、自定义User-Agent、处理 gzip 压缩,这些都是get方法不太方便的。从那以后我基本都是「抓取」和「解析」两步分开走,get只用在确定目标站点稳定的脚本里。

4.4 坑四:动态渲染页面,抓回来的是空壳 HTML

现象:用库抓取某个单页应用页面,表格位置在 JSON 里只有标题没有数据行,或者干脆找不到<table>标签。

原因:(亻这里请你补全一句话)

原因:页面内容是通过 JavaScript 在浏览器里渲染出来的,初始 HTML 里只有<div id="app"></div>,表格根本没在服务端生成。爬虫拿到的是空壳结构,自然解析不出数据。

解决:这个库本身不含浏览器内核,解决方向是换一个能执行 JS 的方案。我常用的做法是把页面交给无头浏览器脚本先渲染一遍,拿到最终 HTML 再落盘,然后用 html-to-json 解析本地文件。两者配合的流程是:无头浏览器负责「还原页面」,html-to-json 负责「提取数据」,职责清晰。

4.5 坑五:中文乱码和编码识别错误

现象:解析结果里中文全是系统这类乱码,或者 JSON 对象里出现的文本和页面上肉眼可见的完全对不上。

原因:抓取阶段没有正确解码。网页可能是 GBK/GB2312 编码,但请求库里硬按 UTF-8 解了字符串;或者响应头和<meta>声明的编码不一致,请求库优先信了响应头,结果解错。

解决:在抓取步骤显式处理编码。常见做法是抓原始 Buffer,然后根据响应头里的charset或页面<meta>字段决定用哪种编码解码。html-to-json 只接收字符串,所以编码问题应当在字符串阶段解决,别拖到解析环节。本地文件同理,读取时强制指定正确编码,别依赖猜。

5. 进阶:转换后数据清洗的三个习惯与验证清单

表格模式转换出的 JSON 数组只能算半成品,离"能直接入库"还差几步清洗。我总结三个每次都做的动作,对应三类高频问题。

第一个动作是统一空值。表格里的空单元格会被解析成空字符串"",但你可能希望它变成null,方便数据库存储。写一个循环把""替换掉就行。第二个动作是类型推断。原页面里「价格:199元」这种字段多半是字符串,但你的业务需要数字;199元里有单位、有空格,正则抽数字是这步的常见实现。第三个动作是去表头杂质——有些表头文字里带换行、注释符号或多余空格,可以直接对 key 做一次trim()和去重。

这三个动作我建议封装成一个独立函数,不要混进解析脚本里。这样测试、复用、排查都容易控制。

验证步骤我习惯这么做:转换完后打印三行日志——原始 HTML 里的表格行数、JSON 数组长度、以及抽样第一条和最后一条数据。行数对不上就说明有合并单元格或解析丢行;首尾数据能对上说明顺序没乱。这套验证逻辑我几乎每次都用,不管页面多简单都会过一次,因为这个库处理复杂表格时的内部行为到现在仍有一部分像黑匣子,靠实际数据验证比看文档可信。

从那以后我每次把一批 HTML 转 JSON,都强制走一遍「抓取 → 解析 → 清洗 → 验证」四步流程,哪怕只是两个表格的小任务。这个习惯帮我挡住了好几次因为页面结构微调导致的无声数据错乱。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询