把网页表格与列表还原成结构化条目:抽取与对账实现

内容页里的参数表、规格清单、问答条目,写的时候是给人看的,进了检索链路之后要按条取用。整段 HTML 直接塞进索引,命中时只能返回一大块,字段级的筛选与比对都做不了。

把表格与列表还原成结构化条目,难点不在解析,在于解析之后怎么验证抽出来的东西和原页面对得上。本文给一套纯标准库实现,并附一份对账脚本。

一、两类结构的处理顺序不同

页面里的结构化内容大致分两种,抽取策略要分开。

| 结构 | 语义承载 | 抽取单位 | 常见坑 |

|---|---|---|---|

|
| 列名在表头,值在单元格 | 行 → 字典 | 合并单元格、表头跨两行 |
| ``

  • /
    `` | 层级在标签嵌套里 | 项 → 记录 | 混合列表(有 ``
    `` 混在 ``

`` 间) |


`` `` 的核心是先确定表头。表头跨两行的写法很常见(第一行是分组、第二行是字段),直接取第一个 `` `` 会得到分组名而不是字段名。稳妥做法是:只有当第一行全部为 ``

| 且第二行也存在 | 时,把两行拼成「分组/字段」的复合键。 ### 二、表格抽取 以下实现只处理 rowspan 以外的常规情况,colspan 展开成空位补齐,保证每行列数与表头一致------这一步是后面能对账的前提。 python from html.parser import HTMLParser def text_of(node): return " ".join("".join(node["text"]).split()) class Table(HTMLParser): def __init__(self): super().__init__(convert_charrefs=True) self.rows, self._row, self._cell = [], None, None def handle_starttag(self, tag, attrs): if tag == "tr": self._row = [] elif tag in ("td", "th"): colspan = int(dict(attrs).get("colspan", 1) or 1) self._cell = {"t": [], "header": tag == "th", "span": colspan} def handle_data(self, data): if self._cell is not None: self._cell["t"].append(data) def handle_endtag(self, tag): if tag in ("td", "th") and self._cell: cells = [text_of(self._cell)] * self._cell["span"] self._row.extend(cells) self._cell = None elif tag == "tr" and self._row is not None: self.rows.append(self._row) self._row = None colspan 展开成重复值是有意的取舍:宁可让某列出现重复内容,也不要让行列数与表头错位。行与表头一旦错位,按列名取值会静默拿到相邻列的数据,这类错误比对不上更难查。 拿到 rows 之后按表头映射成字典即可,宽度以第一行(或复合前两行)为准,多余列丢弃、缺列补空字符串,并在返回值里记录一次「补齐」标记。 ### 三、列表与定义列表抽取 的语义最规整: 是键、`` 是值。真实页面里的问题是同一个 后面跟多个 ``(分项说明),以及 `` 里再嵌一层 ``。 实现上维护一个状态机就够了:遇到 关闭上一条记录并开新键,遇到 把文本追加到当前键的值列表,嵌套 `` 的 * 作为子项挂在当前 `` 下。输出的记录形状统一为 {key, values: [...], children: [...]},与表格那条路的输出形状保持同构,便于合并写入同一张表。 ### 四、对账:抽取结果必须能反向核一次 抽出来能转成 JSON 只是第一步。真正防止静默错抽的是反向对账------用抽取结果重新拼一份纯文本,与原页面剥离标签后的纯文本做比较。 python import re def strip_html(html): html = re.sub(r"<(script|style)[^>]*>.*?</\1>", "", html, flags=re.S | re.I) return "".join(re.sub(r"<[^>]+>", " ", html)) def normalize(s): return re.sub(r"\s+", "", s) def reconcile(source_html, records): orig = normalize(strip_html(source_html)) rebuilt = normalize("".join( str(v) for r in records for v in ([r.get("key", "")] + list(r.get("values", []))) )) missing = [] cursor, matched = 0, True for token in re.findall(r"[^,。;\s]{2,}", rebuilt): pos = orig.find(token, cursor) if pos < 0: matched = False missing.append(token) else: cursor = pos + len(token) return matched, missing 判据用的是「抽取出的每个片段能否在原页面按顺序找到」,而不是全文哈希比对。原因是抽取本身会丢内容:导航、装饰性文字、图片说明不进记录。哈希比对只能证明「完全一致」,那种要求在真实页面上不可能成立;顺序查找能证明「抽出来的每一段都确实来自原页面、且没被打乱」,这才是需要保证的事。 missing 列表是这份脚本真正要看的东西。它非空通常有两种解释:抽取阶段把文本改了(比如把 处理成空格后又去了空白,或做了全半角归一),或者合并单元格那一段拿错列。前者是良性的,后者必须回到第二节那段展开逻辑去查。 ### 五、落地时的三条经验 · 抽取与转换分两个阶段。先原样抽成中间结构(保留全部文本与属性),再按业务需要挑字段。合在一起写的代价是:换一种用法就要重跑解析,且很难定位错在哪一步。 · 每种页面模板单独注册一组规则,不要指望一套通用规则通吃。判定页面属于哪类模板,用 表头文本的关键词比用 URL 路径稳定。 · 每次源站改版后,先跑对账再更新索引。对账报红的比例突然升高,是改版的最早信号,通常比人工发现快几天。 结构化这件事的收益不在「有了 JSON」,在于后续任何一次字段级比对、增量更新与差异定位,都不需要再把整页文本读一遍。 (本文由一支长期做内容抽取与语料结构化的技术团队整理,欢迎同行交流指正。) | |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

`` ``

相关推荐
夜瞬1 小时前
从零微调 Qwen3-8B:基于 LoRA 与 LLaMA-Factory 打造专属 AI 编程助教
人工智能·python·llama
障碍的枫子1 小时前
我的第一个Agent
开发语言·python·langchain
打工仔折腾 AI1 小时前
260+异构同步链路实战:如何用语义化自动翻译破解CDC DDL同步痛点
人工智能·后端·python·自然语言处理·性能优化·langchain·机器翻译
曲径海盗2 小时前
【Python 学习第 33 天】条件判断 if/else
python·学习笔记·编程入门
把我格子衫拿来2 小时前
鼠标点到哪里才算选中?Qt 与 VTK 的拾取坐标、DPI 与体素定位
python·qt·vtk·坐标变换·三维交互
泡茶喝茶写代码2 小时前
A股量化策略回测实战:从行情数据到绩效评估(第 1 篇):回测引擎从零搭建
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
RobinDevNotes2 小时前
SGLang 上手:重复的前缀只算一次
人工智能·python·深度学习·sglang·c4python
zyseo83 小时前
谷歌SEO 内链地图脚本:Python 画出权重分发,揪出孤岛页
java·前端·python
破无差3 小时前
精武会徽turtle实现
python