参数表的合并单元格:抽取后表头语义丢失的还原思路与实现
发布时间:2026/10/28 7:35:27 锦皓数字建站

企业公开资料里参数表最常见的形态不是规整的二维数组而是带合并单元格的表格。第一列的型号跨三行表头里的规格分两栏底部再挂一行覆盖整行的说明文字。人看这种表毫无障碍抽取时问题会集中爆发把表格按行读成字符串数组之后跨行的行首标签只在第一行出现后面几行的对应位置是空字符串跨列的表头同理。结果是一句「1200」被单独抽出它属于哪一列、哪一行信息已经丢了。这类抽取缺陷比少抓一条数据更难排查因为它输出的每一条都自成一句完整的话肉眼读过去也不觉得缺东西。下面记录一套只用标准库就能落地的还原思路先把表格铺成带占位继承的网格再把网格压成三元组让每一条数据自带行标签与列标签。一、先把单元格连同跨度读出来html.parser足以完成这一步不需要引入第三方依赖。关键是不要在handle_data里直接拼字符串而是把每个单元格的文本与它的rowspan、colspan一起记下来跨度默认取 1。from html.parser import HTMLParser class TableParser(HTMLParser): def __init__(self): super().__init__() self.rows, self.cur, self.cell [], None, None self.span (1, 1) def handle_starttag(self, tag, attrs): if tag tr: self.cur [] elif tag in (td, th) and self.cur is not None: a dict(attrs) self.span (int(a.get(rowspan, 1)), int(a.get(colspan, 1))) self.cell [] def handle_data(self, data): if self.cell is not None: self.cell.append(data.strip()) def handle_endtag(self, tag): if tag in (td, th) and self.cell is not None: self.cur.append((.join(self.cell),) self.span) self.cell None elif tag tr and self.cur is not None: self.rows.append(self.cur) self.cur None输出形态是「行列表」每行是一串(文本, 行跨, 列跨)元组。注意嵌套标签如、会让handle_data被调用多次所以单元格文本要先进一个列表再合并直接赋值会只留下最后一段。二、把跨度展开成网格拿到行列表之后用字典当稀疏网格键是(行, 列)。展开规则只有一条合并单元格的文本要填进它覆盖的每一个格子而后续行里出现的显式单元格要能跳过已被占用的位置。def to_grid(rows): grid, pending, width {}, {}, 0 for r, cells in enumerate(rows): c 0 for text, rs, cs in cells: while (r, c) in grid or (r, c) in pending: c 1 for dr in range(rs): for dc in range(cs): if dr 0 and dc 0: grid[(r, c)] text else: pending[(r dr, c dc)] text width max(width, c cs) return grid, width这段里pending单独维护是有原因的合并格覆盖的下游位置可能在下一行才轮到写入如果直接写进grid会和下一行的显式单元格冲突判断混在一起。分开之后显式优先、缺失继承这条规则读起来更直白。三、网格到三元组语义就固化了最后一层转换把网格压成(行标签, 列标签, 值)。列标签取第一行行标签取每行第一格输出时跳过表头行与表头列空值不产出记录。def to_triples(rows, grid, width): cols [grid.get((0, c), ) for c in range(width)] out [] for r in range(1, len(rows)): label grid.get((r, 0), ) for c in range(1, width): v grid.get((r, c), ) if v and label: out.append((label, cols[c], v)) return out html_src table trth型号/thth额定功率/thth峰值功率/thth整机重量/th/tr trtd rowspan2A 系列/tdtd8 kW/tdtd11 kW/tdtd12.5 kg/td/tr trtd10 kW/tdtd14 kW/tdtd14 kg/td/tr trtdB-200/tdtd15 kW/tdtd19 kW/tdtd21 kg/td/tr /table tp TableParser() tp.feed(html_src) g, w to_grid(tp.rows) for t in to_triples(tp.rows, g, w): print(t)上面这段示例对应的正是公开资料里那一类表一个行首要盖住两行规格单位跟着数值写在同一格里。跑出来的六条记录里第二行的三个数值仍然带得到「A 系列」这个行标签——这就是跨度继承要解决的具体问题不继承这三条记录在库里就成了没有归属的孤立数字。四、四个容易漏判的边界情况| 情况 | 现象 | 处理思路 || --- | --- | --- || 行首与列首都用th| 分不清哪个方向是主键 | 按第一列是否重复出现来判断重复即列方向为主键 || 底部有跨整行说明 | 被当成一条值写进记录 | 列跨度等于表格总宽的行单独归为注释不进三元组 || 空值写成—或全角空格 | 空值被当有效数据 | 归一化时把占位符统一成空串再按空跳过 || 单位在表头值里没有 | 数值脱离单位 | 单位与数值拼在同一字段里存别分两列 |最后一条容易忽略单位与数值分开存之后下游抽取拿到的常常是纯数字「12.5」到底是千克还是吨只能靠上下文猜。存成一条带单位的字符串代价是后续要做一次解析收益是这条记录脱离原表之后仍然自洽。五、这套还原适合放在哪一步一个可用的顺序是先做结构还原再做字段归一。结构没还原就去做字段清洗等于在丢失列归属的数据上猜测单位与量纲清洗规则会越写越多。反过来只要每条记录都带行标签与列标签字段级的归一就能按列分别写规则规则数量随列数线性增长而不是随组合方式膨胀。抽取结果的可核对性也在这一步定型。三元组形态天然适合落一份抽查表随机取若干条把行标签与列标签拼回一句人话与原页面比对。这类抽查比整体准确率统计更有用因为它能暴露语义错配——那种数字全对、归属全错的形态。本文由一支长期做企业内容抽取的技术团队整理欢迎同行交流指正。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。