HTML转JSON实战指南:从表格提取到声明式映射的完整方案
2026/9/7 10:33:34 网站建设 项目流程

简介:这是一款开源的Python工具,用于将HTML文档及其中的表格智能转换为JSON格式,特别适合需要从网页中提取结构化数据的开发者与数据分析人员。资源包含完整的项目源码、单元测试、示例页面以及Docker部署配置,共32个文件,涵盖Python脚本、HTML样例、YAML配置、Markdown文档与许可证等,压缩包仅512KB,轻量易用。调用html_to_json.convert即可完成转换,并支持通过参数控制是否捕获文本值或标签属性,灵活适配不同解析需求。目前已有714人学习下载,适合前端爬虫、数据清洗等场景参考。包内附有测试数据与说明文档,可帮助快速掌握用法并集成到自己的项目中。 先讲个我一直以来的观点:只要跟网页数据打过交道,就绕不开“HTML转JSON”这件事。爬虫要结构化字段,测试要断言页面内容,数据分析要从页面表格里抽数据,前端做mock也要拿现成页面当数据源。而html-to-json这个工具,干的就是这件事:把HTML变成JSON,还能把HTML表格里的表头自动当成结果对象的键。这篇是我实际用下来整理的笔记,会讲清楚它适合谁、核心用法、表格转换的原理,以及我在项目里踩过的一些坑。

1. 从“抠数据”到“映射数据”:为什么要用html-to-json

1.1 正则抠数据的痛点

早些年我做页面数据提取,最原始的办法就是一个正则接着一个正则地抠。比如从一个商品列表里拿标题、价格、链接,先匹配外层容器,再在容器里继续匹配。

这种写法在页面结构稳定的时候确实能跑,但问题也很明显。只要前端同事改个class名、调一下标签嵌套层级,正则是真的会“静默失效”——不是报错,而是匹配不到内容,直接返回空数组。等发现问题时,数据已经缺了好几个小时。而且正则表达式本身可读性极差,一条复杂的提取规则过两周自己都看不懂,更别提交接给其他人维护。

html-to-json这类工具解决的就是这个维护性和可读性的问题。它不依赖你写一堆用来匹配的pattern,而是基于DOM结构本身去做提取。你要表达的不是“在哪个字符串后面找哪一段”,而是“取哪个节点下的哪一类元素”,语义清楚得多。

1.2 HTML和JSON本质上都是树

想要理解html-to-json为什么可行,得先想明白一件事:HTML是一棵树,JSON也是一棵树。HTML的DOM结构里,节点之间有父子关系、兄弟关系;JSON对象里,键值对也可以嵌套成同样的层级。既然两者的数据模型本质上都是树,那转换就不是“从字符串里猜结构”,而是“把一种树结构映射成另一种树结构”。

这个思路在实现层面对开发者非常友好。你不需要去写一个完整的HTML解析器,也不用自己维护栈来匹配标签。解析过程分两步:先把HTML字符串解析成DOM树,再用类似jQuery的选择器语法去定位节点、读取文本和属性,最后按你声明的键结构组装成JSON对象。

整个过程的难点不在“解析”,而在“怎么把你的目标结构描述清楚”。html-to-json好用的地方也恰恰在这里——它用一套声明式的配置来代替手写遍历逻辑,让“怎么提取”变得一目了然。

1.3 它到底解决了什么问题

总结下来,我认为html-to-json主要解决四类场景:

第一,接口不稳定或者没有接口的数据采集。页面结构比接口参数稳定得多,从页面提取比模拟请求省事。第二,把第三方平台的报表页面转成结构化数据做二次分析。第三,自动化测试里的页面数据断言,把页面内容直接转成JSON,再跟接口返回做对比。第四,批量处理本地HTML文件,比如把导出的网页书签、聊天记录、报表文件统一转成JSON入库。

适合使用的人群也很明确:写爬虫的工程师、做数据处理的分析师、前端做自动化测试的同学。如果你只是想偶尔转一个页面,那在线转换工具就够了;但如果你要批量处理、要集成进自己的代码流程,那就值得认真看一下html-to-json的用法。

2. 上手实操:三种典型用法与核心参数

2.1 安装与基本调用

以Node.js环境为例,安装很简单:

npm install html-to-json

引入方式:

const htmlToJson = require('html-to-json');

核心API就是parse(html, config)html可以是一段HTML字符串,也可以是你用fs.readFileSync读进来的文件内容。config是一个对象,声明“结果JSON里每个键对应页面上哪个位置的内容”。

这里有一个经验:网上很多旧教程会直接传一个URL进去,让它内部去请求页面。但在我实际使用中,更推荐自己先把HTML拿到手再用它解析。这样你可以先确认页面编码、先做一次清洗,遇到动态渲染的页面也能先做预处理,整个流程更可控。把获取和解析这两件事分开,是这类工具用得顺手的第一个关键点。

2.2 整个页面转JSON:声明式键映射

先看一个最基础的例子。假设有这样一个HTML片段:

<div class="user"> <h1>张三</h1> <span>const html = ` <div class="user"> <h1>张三</h1> <span>{ "name": "张三", "job": "软件开发", "age": "28" }

这里有个值得注意的点:namejob直接传选择器字符串时,取的是匹配元素的文本内容并自动去掉首尾空白。如果你需要属性值或者要对文本做二次处理,就要像age那样,把值定义成一个对象,键是选择器,值是一个函数。函数接收的参数是匹配到的元素,你可以调用.attr().text().find()等等。

这种“键是输出字段名,值是提取规则”的配置方式,本质上是在声明一份JSON的Schema。以后页面结构变了,你只需要改对应的选择器,不用翻整段逻辑代码。

2.3 表格转JSON:让表头变成键

HTML转JSON最烦人的一种情况就是表格。没有智能转换之前,你得先拿到所有行、所有单元格,然后手动把第一行当key、后面每一行当value。现在html-to-json可以直接把表头提出来当键。

市面上常见实现的核心逻辑一般是这样的:读取表格所有行,取第一行的单元格文本作为字段名,然后从第二行开始逐行组装对象。我用一个完整示例来说明:

<table id="score"> <thead> <tr><th>姓名</th><th>语文</th><th>数学</th></tr> </thead> <tbody> <tr><td>张三</td><td>88</td><td>95</td></tr> <tr><td>李四</td><td>76</td><td>89</td></tr> </tbody> </table>

用html-to-json提取:

htmlToJson.parse(html, { 'score_rows': ['#score tr', function ($tr) { return $tr.find('th, td').map(function () { return $(this).text().trim(); }).get(); }] }).then(result => { const rows = result.score_rows; const headers = rows[0]; const data = rows.slice(1).map(row => { const obj = {}; headers.forEach((key, index) => { obj[key] = row[index] || ''; }); return obj; }); console.log(data); });

输出的JSON:

[ { "姓名": "张三", "语文": "88", "数学": "95" }, { "姓名": "李四", "语文": "76", "数学": "89" } ]

这样出来的数组,后面不管你是做报表、写测试断言,还是灌进数据库,都很顺手。而且因为键名直接来自表头,代码里写item.数学比写item[3]可读性高太多了。

2.4 为什么声明式映射比手写循环好用

可能有人觉得,不就一个map加一个reduce吗,自己写也行。确实,一张表格怎么都能写。但遇到真实页面时,手写循环的复杂度是快速膨胀的。

比如表头单元格里带着图标、带着超链接,你直接text()拿出来可能带了一堆杂七杂八的东西;比如某个页面设计成多级表头,需要拼接父级表头和子级表头;再比如一个页面有七八张表,每张表结构还不一样。这些情况如果全用命令式的for循环一个一个处理,代码写出来怕是有上百行,而且每个页面的逻辑都不同,不能复用。

用声明式配置,差异被收敛成“每个键怎么提取”的局部问题。一个配置对象走天下,页面A和页面B之间的切换成本大幅降低。这也是我在项目中坚持用这类工具的核心原因:它为“提取规则”本身提供了结构化的管理方式。

3. 核心机制拆解:表格“智能”转换的原理与边界

3.1 二维表格模型:第一行做键,后续行做值

所谓“智能地”将表格转成JSON,最基础的思路是把一个table当成一个二维数组来处理。第一行是表头,后面的每一行是数据。表头单元格的文本就是结果对象的键名,数据行单元格的文本按位置跟表头对应。

这个思路之所以有效,是因为大多数业务表格都符合这种二维的行列结构。但是“智能”二字也意味着,工具在实现时会主动处理一些常见干扰:表头如果用的是<th>标签、数据用<td>标签,提取时两类标签都会抓;单元格里的空白符会做trim;重复的表头键名会做去重或加后缀。

一个我实测中很有用的细节是:如果表格没有表头行,传统做法会退化成“第一行也是数据”,这时键名可能自动变成column_0column_1这种占位名。这跟标题里说的一致——“使用表头(如果有)作为结果JSON中的键”。建议拿到数据后第一件事检查有没有出现column_0,如果有,说明源页面的表头行没被正确识别,需要回到选择器层面调试。

3.2 复杂表头:colspan、rowspan、嵌套表怎么处理

真实页面里最坑的就是合并单元格。表头里一个大类跨了两列,下面两个子类,这种多级表头在Excel里随处可见,在网页报表里同样常见。

处理多级表头时,简单的取第一行做键会出问题。因为第一行的单元格数量可能少于数据行的单元格数量,直接按位置对齐会导致键和数据错位。我的做法是先把多行表头“拍平”成一行:遍历表头上所有<th>单元格,遇到colspan=2就把当前表头文本重复两次;遇到rowspan大于1的,先记录这个文本,等解析到下面一行时再继续填到对应位置。

这个逻辑在代码里大概长这样:

function flattenHeader($table) { const headerRows = $table.find('tr').map(function () { return $(this).find('th').map(function () { return $(this).text().trim(); }).get(); }).get(); const maxCols = Math.max(...headerRows.map(row => row.length)); const flat = []; for (let i = 0; i < maxCols; i++) { flat[i] = ''; } headerRows.forEach((row, rIndex) => { row.forEach((cellText, cIndex) => { if (cellText) { flat[cIndex] = flat[cIndex] ? flat[cIndex] + '_' + cellText : cellText; } }); }); return flat; }

这样拍平之后,“2024年”和“一季度”会拼成“2024年_一季度”,作为最终的键名。虽然有点长,但至少不会丢失层级关系。需要注意,这个函数默认表头都在<th>里。如果页面用<td>当表头,你需要多做一层判断。

3.3 边界条件:无表头、空单元格、属性缺失

说几个我真实遇到过的边界情况,你们可以提前规避。

第一个是表头里有重复文本。Excel里合并过的表头,后面几列单元格内容是空的,会导致生成的键名重复。这种键名在JSON里后面一个会覆盖前面一个,数据直接少掉一列。解决办法很简单:键名重复时,给后面的加_2_3后缀。

第二个是数据单元格是空的。对空单元格,直接取text()回来是空字符串。如果你不在意,它没问题;但如果你后面要做数值计算,空字符串可能让你的SQL或者统计脚本行为异常。我建议在封装转换函数时,把空字符串统一替换成null,或者保持字段缺失只保留键名。这个看业务需要,但不要留着空字符串不处理。

第三个是标签里夹HTML内容。单元格里如果有<a><span><img>,直接text()可能把链接文字和图片的alt一起带出来。你要想清楚保留纯文本还是保留内部HTML。保留纯文本用.text(),保留完整内容用.html(),两者差别很大,用错会让后续清洗变得很痛苦。

第四个是编码问题。网页声明是GBK但HTTP头没写对,HTML解析出来中文直接乱码。这种问题工具层很难帮你解决,必须在解析之前用iconv-lite之类做转码。我的习惯是拿到HTML先检查前几百字节里的charset声明,再决定要不要转码,而不是等到输出一堆乱码再返工。

4. 踩坑实录与实战避坑指南

4.1 高频问题速查表

我把实操中遇到的问题整理成一张速查表,方便你排查。

现象原因解决办法
输出JSON里少了一个键选择器匹配不到节点,或节点文本为空先在浏览器里确认选择器是否正确,是否绑定在正确的父节点下
表格键名是column_0第一行没识别为表头检查表头是否用了<th>,或加入自定义表头配置
中文全部乱码页面编码与解析编码不一致解析前用iconv-lite转码到UTF-8
键值对顺序不对表格里有合并单元格按colspan/rowspan拍平表头,再对齐数据行
大量耗时的卡顿HTML体量过大,或选择器太宽泛先按容器缩小范围,再拆分多次解析

4.2 选择器陷阱:看似能匹配,结果却不是想要的

用选择器提取数据,最大的坑是“选择器写太宽”。比如页面里除了目标表格,还有一个隐藏的统计表格,你如果直接用table tr去匹配,两个表格的数据会混在一起,键和数据全乱套。

我的习惯是先给目标容器加一个唯一的定位点。最靠谱的是配合父级容器的idclass,比如#score-table tr#summary-table tr,这样哪怕两个表格结构一模一样,提取结果也是互不干扰的。

另一个问题是动态加载内容。有些页面表格数据是AJAX请求回来再渲染的,直接拿初始HTML解析只能看到空表。这种情况用纯粹的html-to-json是搞不定的,需要先用无头浏览器把异步内容等出来,再拿渲染后的HTML转JSON。我的做法是分两步:无头浏览器负责渲染和等待,html-to-json负责结构转换,各干各的,不要混在一起。

4.3 性能优化:别把整个页面丢进去

有一次我批量处理一个数据报表页面,一个页面差不多1MB的HTML,里面还有大段内联CSS和base64图片。直接对整个页面做转换,单页耗时接近三秒,批量跑下来完全不可接受。

后来我学乖了,在转换前先做一次裁剪。核心操作就是用深度选择器把目标区域先抠出来,然后再把这段子HTML交给html-to-json去处理。例如:

const $ = cheerio.load(html); const targetHtml = $('#report-content').html();

这样处理之后,解析的数据量缩小到原来的十分之一,单页耗时降到几百毫秒以内。

另外,如果同一个HTML你需要提取多组数据,尽量在一次parse()调用里完成所有键的映射,而不是反复多次解析同一个HTML字符串。一次解析、多键提取,内存占用和耗时都比多次解析要低很多。

4.4 工具选型:四种方案怎么选

我根据自己的实践,把HTML转JSON常见的几种方案做了个对比:

方案优点缺点适用场景
正则提取零依赖、上手最快难维护、易失效、处理嵌套困难一次性小任务、结构非常固定的单条数据
cheerio/jQuery风格手写遍历灵活、可控性强代码量大、每个页面都要重写逻辑页面结构差异极大、需要深度定制
html-to-json声明式、可读性好、表格转换方便选择器写法有学习成本批量页面提取、报表转结构化数据、自动化测试
无头浏览器渲染后提取支持动态页面资源占用大、速度慢必须有JS渲染才能看到数据的场景

我的建议是:静态页面优先用html-to-json,因为它把“解析”和“映射”这两层处理得刚刚好,省代码又容易维护。一旦遇到需要等待请求完成的页面,再考虑无头浏览器方案。当前端页面结构改得频繁时,把映射规则独立成配置文件,会让你的维护成本低很多。

最后再分享一点个人经验:做这类转换任务,最容易翻车的其实是编码,十个报错里有八个不是选择器写错,而是中文乱码。所以拿到陌生页面,第一件事先确认charset,再决定要不要转码。另外,凡是给业务方用的转换任务,我都会在结果里加一个_meta字段,记下源URL和抓取时间,后面排查问题会省很多事。还有一个小技巧:调试映射规则时,别一遍遍重新请求目标网站,把HTML先存成本地文件,脚本里直接读取本地文件来测,速度快,还不会给目标服务器带去不必要的压力。

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

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

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

立即咨询