简介这份资源是一个基于 Python 的 HTML 转 JSON 工具包面向需要从网页或 HTML 文档中提取结构化数据的开发者尤其适合处理 HTML 表格并希望以表头作为 JSON 键的场景。包内共 32 个文件以 7 个 py 源码文件为核心辅以 6 个 html 测试页面、4 个 yml 配置、若干 txt、md 说明文档及 Dockerfile、setup.py 等工程化配置压缩包约 512KB体量轻便、结构完整。已有 716 人学习下载说明其在数据抽取类小工具中具备一定实用参考价值。读者可获得 convert 与 convert_html_tables 等核心转换逻辑的实现代码了解如何通过 capture_element_values、capture_element_attributes 等参数控制文本与属性捕获并借助测试用例与表格样例图快速验证效果适合作为爬虫数据清洗、报表解析或自动化测试中的轻量工具脚本直接复用。1. 从一张报表页说起html-to-json 到底解决什么问题上周有个做数据采集的朋友丢给我一段 HTML说“表头在 thead 里数据在 tbody 里合并单元格一堆我正则写到怀疑人生”。这不是个例。凡是做过网页数据提取的人几乎都经历过同一个阶段先上正则正则崩了换 BeautifulSoupBeautifulSoup 拿到一堆标签又得自己拼字典拼完发现表头对不上列、colspan 一合并全乱套。html-to-json 这个资源瞄准的正是这段最脏最累的活——把一段 HTML 直接转成结构化 JSON尤其是把 HTML 表格智能地转成以表头为键的 JSON 对象数组。它适合三类人做爬虫后处理的数据工程师、需要把后台导出的 HTML 报表落库的开发者、以及手上有一批静态页面想快速结构化又不想写解析器的人。核心价值不在“能转”而在“表头当键”这个细节——它决定了你拿到的是一坨嵌套节点还是一份能直接喂给下游的干净数据。2. 拆开看HTML 转 JSON 的两条路径与表头映射逻辑2.1 通用节点转换DOM 树到 JSON 的映射规则任何 HTML 转 JSON 工具底层都绕不开一个问题一棵 DOM 树怎么变成 JSON。常见做法是递归遍历每个元素节点产出一个对象对象里带 tag、attributes、children 三个字段文本节点单独处理。这样转出来的结构保真度高但层级深、冗余多直接拿去用往往还要再写一层扁平化。html-to-json 的通用转换走的就是这条路它保证标签、属性、嵌套关系不丢适合“我要完整保留页面结构”的场景比如做页面快照比对、做 DOM 差异分析。理解这条路径的关键是搞清楚它输出的形状。一个div classaphi/p/div转出来大致是带 tag 名、属性字典和子节点数组的嵌套对象文本落在专门的字段里。你拿到之后如果要取p的文本得顺着 children 往下找而不是直接.text。这不是缺陷是设计取舍——通用转换不预设你的用途所以不做语义裁剪。我一般会在这条路径上做一层薄封装把常用的“按标签名取所有节点”“按 class 过滤”写成小函数避免每次都在嵌套结构里手挖。下面这段是典型的递归遍历骨架理解它比背 API 有用def walk(node, depth0): # node 是解析后的元素节点含 tag / attrs / children result {tag: node.tag, attrs: dict(node.attrs)} texts [c for c in node.children if is_text(c)] if texts: result[text] .join(t.strip() for t in texts) kids [walk(c, depth 1) for c in node.children if is_element(c)] if kids: result[children] kids return result这段代码的逻辑说明先固定当前节点的 tag 和属性再把直接文本子节点合并成一个 text 字段去空白最后递归处理元素子节点。参数上depth只是调试用实际转换不需要限制深度但如果你面对的是超深嵌套页面加一个最大深度阈值能防止递归爆栈。注意is_text和is_element要区分文本节点和元素节点很多新手在这里把注释节点也当元素处理结果 JSON 里混进一堆无意义条目。2.2 表格专用转换用表头当键的智能映射表格转换是这个资源真正的卖点。普通节点转换把table当普通元素处理转出来是 tr/td 的嵌套数组你还得自己找表头、对齐列。表格专用模式多做了一步先定位 thead没有 thead 就取第一行 tr 当表头提取每个 th 的文本作为键名再遍历 tbody 的每一行把每个 td 的文本按列序映射到对应键上产出一个对象数组。这就是“表头当键”的含义——你拿到的不是[[姓名,年龄],[张三,28]]而是[{姓名:张三,年龄:28}]。这个映射听起来简单实际有三个决策点。第一表头缺失怎么办常见做法是回退到第一行数据行当表头或者生成col_0、col_1这样的占位键。第二表头重复怎么办两列都叫“金额”时后一个会覆盖前一个稳妥做法是自动加后缀。第三空单元格怎么表示是空字符串、null 还是直接省略键不同实现不一样下游消费时要先确认。下面是一个表格映射的核心逻辑把这三个决策点都显式写出来def table_to_json(rows, headersNone): # rows: 二维文本数组headers: 可选外部传入的表头 if headers is None: headers rows[0] # 回退首行当表头 rows rows[1:] # 表头去重重复的加序号后缀 seen, clean {}, [] for h in headers: h h.strip() or col if h in seen: seen[h] 1 clean.append(f{h}_{seen[h]}) else: seen[h] 0 clean.append(h) out [] for row in rows: obj {} for i, key in enumerate(clean): obj[key] row[i].strip() if i len(row) else None out.append(obj) return out逻辑说明headers is None时走首行回退这是没有 thead 的页面最常见的处理方式去重循环用seen字典记录每个表头出现次数第二次出现就拼_1后缀避免键覆盖行映射时用i len(row)判断越界短行补 None 而不是抛异常因为真实页面里 td 数量不齐是常态。参数上如果你明确知道表头在第几行直接传headers比让它猜更稳。这套逻辑不依赖具体库换成任何解析器都能套。2.3 选型理由为什么不用正则或通用解析器硬扛有人会问BeautifulSoup 也能干这事为什么要专门用这个资源。区别在“表头当键”这层语义。BeautifulSoup 给你的是标签树表格语义要你自己重建找 thead、处理 colspan、对齐列、去重表头这一套写下来少说百来行而且每个页面结构一变就得改。html-to-json 把这段通用逻辑封装好了你只需要关心“表头在哪、要不要去重、空值怎么填”这几个参数。正则就更不用说了HTML 不是正则语言嵌套表格、属性里带尖括号、注释干扰随便一个就能让正则翻车血泪经验是凡是结构化的 HTML别用正则硬刚。选型时还要看输出契约。如果你下游是数据库需要的是扁平对象数组表格模式直接可用如果你下游是前端渲染需要保留层级通用模式更合适。这个资源两种模式都提供切换成本低这是它比单一用途脚本强的地方。至于性能纯解析转换在万行级表格上通常是秒级以内真正的瓶颈在 HTML 获取和清洗不在转换本身所以不必过早优化。3. 动手复现从一段 HTML 到可用 JSON 的完整流程3.1 环境准备与最小可运行示例先把最小闭环跑通再谈复杂场景。假设你拿到一段本地 HTML 字符串目标是转成 JSON 并落盘。常见做法是装好解析依赖后写一个入口脚本读文件、调转换、写结果。下面这段是可直接抄的骨架# 建目录、装依赖解析器按你选的库来这里以通用 HTML 解析为例 mkdir -p html2json-demo cd html2json-demo python -m venv venv source venv/bin/activate pip install beautifulsoup4 lxmlfrom bs4 import BeautifulSoup import json def html_to_json(html, modegeneric): soup BeautifulSoup(html, lxml) if mode table: return [table_to_json(extract_rows(t)) for t in soup.find_all(table)] return walk(soup.body or soup) # 通用模式walk 见 2.1 if __name__ __main__: with open(sample.html, encodingutf-8) as f: html f.read() data html_to_json(html, modetable) with open(out.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)逻辑说明mode参数决定走通用还是表格路径表格模式对页面里每个 table 单独转换并汇总成列表ensure_asciiFalse保证中文表头不被转义成\uXXXX这是中文场景必设项忘了它你的 JSON 里全是转义码下游读起来痛苦。extract_rows负责把 table 拆成二维文本数组内部要处理 thead/tbody 分离和 colspan 展开这部分是下一节的重点。参数上indent2只为可读性生产环境可以去掉省体积。3.2 表格转换的参数调优表头行、空值与合并单元格真实表格的坑集中在三处表头不在第一行、空单元格语义不明、合并单元格导致列错位。先说表头行。有些报表页面前面有标题行、说明行真正的表头在第三行甚至更靠后。稳妥做法是显式指定表头行索引而不是让它盲猜首行。下面这段演示如何按索引取表头并处理 colspandef extract_rows(table, header_row0): trs table.find_all(tr) headers, data None, [] for idx, tr in enumerate(trs): cells [] for cell in tr.find_all([th, td]): span int(cell.get(colspan, 1)) # 合并列展开 cells.extend([cell.get_text(stripTrue)] * span) if idx header_row: headers cells else: data.append(cells) return table_to_json(data, headers)逻辑说明header_row参数让你指定第几行是表头默认 0colspan展开是把一个跨列单元格复制成多份保证后续按列索引对齐这是合并单元格不翻车的关键——不展开的话后面所有列都会左移。注意rowspan这里没处理跨行合并更麻烦常见做法是先按行展开成网格再回填如果你的页面 rowspan 多建议单独写一个网格化预处理。参数上get_text(stripTrue)会丢掉单元格内的换行和多余空格如果单元格里有br需要保留分隔改成separator\n。空值的处理要跟下游约定好。我一般默认空单元格给空字符串因为数据库里空串和 null 语义不同空串表示“有这一列但没填”null 表示“这一列不存在”。如果你的下游是 JSON Schema 校验null 更合适如果是直接拼 SQL空串更省事。这个选择没有对错但要在转换入口统一别一半空串一半 null后面排查能把你逼疯。3.3 批量处理与输出校验单文件跑通后下一步是批量。批量场景的难点不在转换在“坏数据不中断整批”。常见做法是每个文件独立 try失败记日志继续跑最后汇总成功失败清单。下面是一个批量入口import glob, os, traceback def batch_convert(pattern, out_dir): os.makedirs(out_dir, exist_okTrue) ok, fail [], [] for path in glob.glob(pattern): try: with open(path, encodingutf-8) as f: data html_to_json(f.read(), modetable) name os.path.splitext(os.path.basename(path))[0] with open(os.path.join(out_dir, name .json), w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) ok.append(path) except Exception: fail.append((path, traceback.format_exc())) return ok, fail逻辑说明glob按模式匹配输入文件输出目录自动创建每个文件独立捕获异常traceback.format_exc()保留完整堆栈方便定位返回成功和失败两个列表失败项带堆栈便于事后逐个修。参数上如果你的 HTML 编码不统一encodingutf-8会炸稳妥做法是先探测编码或用errorsreplace兜底但兜底会掩盖乱码问题建议先小批量试跑确认编码。输出校验这一步很多人省掉结果 JSON 看着能解析字段却对不上。我一般会加一个轻量校验检查每个输出对象的键集合是否一致、行数是否与源表 tr 数吻合、有没有全空行混进来。这三条能拦住大部分映射错位问题。校验不通过的文件单独拎出来人工看比全量重跑高效得多。4. 避坑与排查五条真实翻车记录4.1 表头被当成数据行结果多出一条脏记录现象转换结果第一条记录是{姓名:姓名,年龄:年龄}表头混进了数据。原因页面没有 thead表头在第一行 tr 里但代码没做首行回退或者header_row设成了 -1 导致没匹配上。解决显式判断 thead 是否存在不存在就把header_row设为 0 并在取完表头后跳过该行如果页面有说明行先按文本特征比如全 th、或含“序号/名称”关键词定位表头行再传索引。4.2 合并单元格导致列整体左移现象某几行的“金额”跑到了“备注”列后面全错位。原因源表有 colspan代码按 td 个数取列没展开合并列导致该行少了几列后续按索引映射时整体前移。解决在提取单元格时读colspan并复制文本展开保证每行单元格数与表头列数一致rowspan 多的表先做网格化预处理把跨行单元格填到对应行的对应列再走映射。4.3 中文表头变成 \uXXXX下游读不了现象JSON 文件打开全是转义码字段名对不上。原因json.dump默认ensure_asciiTrue所有非 ASCII 字符被转义。解决显式传ensure_asciiFalse同时确认写文件时encodingutf-8。这两个要一起设只设一个仍可能出问题。另外如果下游是某些老系统可能反而要求 ASCII那就保持默认并在读取端解码别两头都改。4.4 空单元格被省略键集合不一致现象有的记录有“备注”键有的没有下游按固定 schema 解析时报缺字段。原因转换时对空单元格直接跳过没写键或者用了if value:过滤。解决映射时无条件写键空值给空串或 null保证每条记录键集合一致如果确实想省体积至少在输出层做一次键补全别让不一致流到下游。4.5 大表格转换内存飙升甚至卡死现象几万行的表格一转内存直接拉满。原因一次性把整个 DOM 和中间二维数组都留在内存里加上递归遍历深嵌套对象数量爆炸。解决改流式处理按 table 分块、按行迭代转完一块写一块通用模式如果不需要完整树改成事件驱动解析边解析边产出别先建全树再遍历。参数上给递归加深度上限超深嵌套的页面直接降级为扁平提取。5. 进阶技巧把转换结果接进校验与回填闭环转换本身只是中间步骤真正决定这份 JSON 好不好用的是它能不能顺利进下游。我一般会在转换后加一道“回填校验”拿转换出的 JSON 反查源表随机抽几行把 JSON 里的值按表头键还原成一行文本跟源表对应 tr 的文本比对不一致就标记出来。这一步能抓出映射错位、空值误判、编码问题比肉眼扫 JSON 靠谱得多。下面是一个最小回填校验import random def verify(sample_rows, json_rows, headers, n5): # sample_rows: 源表二维文本json_rows: 转换结果headers: 表头 idxs random.sample(range(len(json_rows)), min(n, len(json_rows))) bad [] for i in idxs: rebuilt [str(json_rows[i].get(h, )) for h in headers] if rebuilt ! sample_rows[i]: bad.append((i, rebuilt, sample_rows[i])) return bad逻辑说明随机抽 n 行按表头顺序从 JSON 里取值拼回一行跟源表对应行逐列比对不一致的记录连同两边内容一起返回方便定位是哪个键映射错了。参数上n不用大5 到 10 行足够暴露系统性问题如果表头有去重后缀比对时要用清洗后的表头别用原始表头。这个校验跑一次几毫秒但能省掉后面几小时的排查。再往上一层是“转换即入库”的闭环转换输出直接对接批量插入插入前用数据库约束非空、唯一、类型再兜一道把脏数据挡在库外。这里有个习惯我保持了很久——每次改转换参数表头行、空值策略、colspan 处理都强制重跑一遍回填校验和一小批入库确认键集合和行数没漂移再放量。翻车最狠的一次就是改了个表头回退逻辑没重验结果整批数据列错位事后靠备份才捞回来。从那以后我每次动映射规则都先跑校验再批量这个习惯比任何工具都值钱。希望这套流程能帮到你少走我踩过的弯路。本文还有配套的精品资源点击获取