Files
mdpolish/research-wiki/scratch/html-table-real-data-analysis-2026-08-21.md
T
Bepr4 48dd02ce1d 记录 45 份 GovDoc 测试 Markdown 的 HTML 表格只读结构分析
新增 research-wiki/scratch/html-table-real-data-analysis-2026-08-21.md:
按展开 span 后行列一致性给 2474+356+1879 个顶层表格分类(剔除空 tr 后
简单表 53% / 合并合并表 8% / 损坏表 40%),定位标签不闭合、丢失
colspan 标注、无表头三类主因。README 当前阶段补对应条目。
2026-08-22 15:05:35 +08:00

171 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HTML 表格真实数据结构分析:45 份 GovDoc 测试 Markdown
> 状态:只读分析记录,尚未进入任何 design。文中"建议"部分是候选方向,不是批准的方案。
>
> 分析日期:2026-08-21。
>
> 定位:用真实数据回答"我们的 HTML 表格到底长什么样、损坏在哪",为未来表格清洗 design 提供测量依据,
> 并修正 [`html-table-cleaning-ecosystem-research-2026-08-21.md`](html-table-cleaning-ecosystem-research-2026-08-21.md)
> 第 8 节中可以用数据回答的待决问题。
## 1. 结论先行
按"展开 rowspan/colspan 后每行列数是否一致"给全部顶层表格分类,**剔除空 `<tr></tr>` 之后**的分布:
| 分类 | 定义 | 数量 | 占比 |
|---|---|---|---|
| A 简单矩形表 | 无合并单元格、每行列数一致、无嵌套、无游离内容 | 2474 | 53% |
| B 合法合并表 | 有 rowspan/colspan,展开后仍是完整矩形 | 356 | 8% |
| C 损坏表 | 参差网格、标签截断、占位冲突等 | 1879 | 40% |
四个改变预期的发现:
1. **一半的"损坏"是空 `<tr></tr>` 造成的假象**——剔除后 C 类从 54% 降到 40%;
2. **标签不闭合会吞掉半篇文档**——最极端的一个未闭合表格吞了 2.79MB、1294 个表格;
3. **参差网格与合并单元格强相关**——六成以上参差表带 span 属性,指向转换器丢失 colspan 标注;
4. **无表头是常态**(A 类中 96%),**表格内图片为零**,单元格内管道符为零。
## 2. 数据范围与方法
- 输入:`/home/lihaoze/gov_test_data/compare/*/uploads/*.md`45 份,全程只读;
本文只含聚合数字和结构事实,不含任何原文片段;
- 分析脚本在会话级临时目录 `/tmp/table-analysis/analyze.py`,未入库(仓库处于文档治理阶段,无源码目录);
本文第 2.1 节的规则描述是复现依据;
- 解析器:lxml `HTMLParser(recover=True)`。本地未装 html5lib。
### 2.1 测量规则
1. **代码围栏遮蔽**:先标记 ```` ``` ````/`~~~` 围栏内的位置,围栏里的 `<table>` 不计入;
2. **片段切分**:对围栏外的 `<table`/`</table>` 事件按嵌套深度配对出顶层片段;悬空的 `</table>`
(无对应开启)单独计数并忽略;到文件尾仍未闭合的片段标记 `truncated`
3. **结构分析**:每个片段单独喂给 lxml 容错解析;对每个 `<table>` 展开 rowspan/colspan 建二维占位网格,
检测占位冲突,计算每行展开后的列数,收集表头、嵌套、游离文本、块级子标签、空单元格、超长单元格、
退化重复等特征;
4. **分类谓词**`truncated` 为片段级标记,其余为表格级):
```text
A_simple : 非 truncated 且 span_cells=0 且无嵌套 且每行展开列数一致
且无占位冲突 且无游离文本 且块级子标签 ⊆ {br}
B_span_ok: 非 truncated 且无占位冲突 且每行展开列数一致 且无游离文本
C_broken : 其余全部
```
5. **空行修复复核**:把没有任何 `td`/`th` 子元素的 `<tr>` 整行剔除后重新跑同一分类,观察迁移。
## 3. 总量与原始分类
- 45 份文档共 7550 个 `<table` 开标签、7155 个闭标签,**缺口 395 个**
- 深度配对得到 4648 个顶层片段;容错解析后共 **4709 个表格元素**(缺口标签导致部分片段解析出多个表格);
- `parse_fail=0`lxml recover 模式记录 parser 错误的片段仅 30/4648——**recover 解析器的错误日志是弱信号,
不能当损坏检测器用**,损坏要靠网格校验发现。
原始分类(4709 个表格):
| 分类 | 数量 | 占比 |
|---|---|---|
| A_simple | 1921 | 41% |
| B_span_ok | 262 | 5% |
| C_broken | 2526 | 54% |
结构特征(占 4709 的比例):含 span 属性 189140%)、参差网格 250653%)、有 `<th>` 2595.5%)、
单行表 157、截断片段 67、超长单元格(>500 字符)197、巨型片段(>10KB)70、退化重复内容 33、
游离文本 27、占位冲突 14、嵌套表格 5、含块级标签 5、**含图片 0**。
## 4. 发现一:空 `<tr></tr>` 是最大的单一"假损坏"来源
完全没有 `td`/`th` 子元素的空行标签在参差表里出现 3000+ 行次。它们把"列数一致的好表"撑成
"某些行 0 列"的参差表。
剔除空行后重新分类:
| 迁移路径 | 数量 |
|---|---|
| C_broken → A_simple | 553 |
| C_broken → B_span_ok | 94 |
即分类变为 **A 247453%/ B 3568%/ C 187940%**,一条零风险修复救回 13% 的表。
空行不含任何内容,剔除是无损的。
空行出现的位置:表中间 1151 处、表尾 385 处(对修复后仍为 C 的表统计),**没有出现在表头位置**——
符合"转换器输出残留"而非"表头占位"的形态。
## 5. 发现二:标签不闭合会吞掉后续正文
最极端案例 `001-2/uploads/file_0_PDF.md`:全文件 1315 开 / 1124 闭;从第 1315 行开始的一个未闭合
表格把后续 **2,789,092 字符、内部含 1294 个表格**的内容全部吞进一个"顶层片段"。
全库 395 个闭合缺口意味着:**清洗的第一步不是处理表格,而是安全切分片段**。深度配对在标签缺失时
会把正文和后续完整表格归并进一个巨型片段,后续所有基于片段的统计和修改都会失真。
切分策略的可用锚点:相邻顶层表格之间,2943 对隔着真实文本、1661 对只隔空行——块边界
(空行 + 后续正文)在实际数据中是可识别的。
## 6. 发现三:参差网格与 span 强相关,指向"丢 colspan 标注"
2506 个参差表中 **1624 个(65%)带 span 属性**——格子内容在、宽度信息没了,是转换器丢失
colspan 标注的形态,不全是真缺内容。
剔除空行后仍参差的 1851 个表,参差发生位置:
| 位置 | 数量 | 推断成因 |
|---|---|---|
| 中间各行乱 | 780 | 真·结构损坏或逐行丢标注 |
| 表头行比正文长 | 619 | 多级表头被拍平成一行、丢层级 |
| 首行短 | 239 | 表头/首行缺格 |
| 仅尾部截短 | 213 | 跨页截断尾巴 |
四类成因不同,修复策略应当不同,但**都不能靠补空单元格自动修**——那正是
[`../reference/GOVDOC_SAAS_CLEANING_SCOPE.md`](../reference/GOVDOC_SAAS_CLEANING_SCOPE.md) T002
明确反对、且 rumdl MD056 的 auto-fix 方向被本仓库否决的做法。
## 7. 发现四:无表头是常态,转换障碍集中在 `<br>`
对修复后仍是 A 类的 2474 个表,检查转 GFM 管道表格的内容障碍:
| 障碍 | 数量 | 占 A 类 |
|---|---|---|
| 无 `<th>` 表头 | 2393 | 96% |
| 单元格含 `<br>` | 501 | 20% |
| 单行表 | 176 | 7% |
| 超长单元格(>500 字符) | 54 | 2% |
| 单元格文本含 `\|` | 0 | 0% |
| 表格内 `<img>` | 0 | 0% |
转义压力比预期小(管道符、图片都是零),压力集中在**合成表头**和 `<br>` 处理上。
## 8. 建议的处理策略(候选,未批准)
三步走,对齐审计 T001 的分流方向,用真实数据修正边界:
1. **切分**:容错定位 `<table>` 片段;未闭合的在块边界截断并标记 truncated,
绝不让深度配对吞正文;
2. **无损修复**:只做剔除空 `<tr>` 这一级别的零风险修复(实测救回 13% 的表);
3. **分流**
- A 类(53%)→ 转 GFM:合成表头、`<br>` 转空格、管道符转义兜底;
- B 类(8%)→ 保留 HTML,规范化输出;
- C 类(40%)→ 不自动修,保留原样 + 按第 6 节四类成因报告分类原因,交人工确认或回源。
对生态调研第 8 节待决问题的数据回答:
- 问题 4(无表头表格怎么处理):数据表明无表头占绝对多数(96%),"补合成表头转 GFM"
是主流路径;空表头还是首行充当表头仍是 design 待决;
- 问题 1(简单/复杂分界线):分界线除了合并单元格,必须加"展开后网格是否矩形"——
本数据中它是比 span 更强的损坏信号(53% 对 40%);
- 问题 2、3(规范 HTML 程度、JSON schema)本次数据没有新增证据,维持开放。
## 9. 局限
- 全部数字只来自这 45 份文档,不得外推为一般结论(CLAUDE.md 第 5 节约束);
- 53/8/40 依赖"剔除空 `<tr>`"这条规则被采纳;不采纳则为 41/5/54;
- lxml recover 解析可能自行重排损坏片段,个别表格的行列统计是解析结果而非字节事实;
- 巨型片段内部的表格统计(如 001-2/file_0 被吞的 1294 个)已计入总数,但它们在原文中的
真实边界未经人工核对;
- 参差位置的四分类用的是简单规则(与列数众数比较),是启发式归类,不是语义判断。
## 10. 验证状态
- 第 3 至 7 节所有数字:2026-08-21 由只读脚本在真实数据上实际运行得出,脚本未入库;
- 分类谓词与第 2.1 节规则描述和脚本逻辑一致,可据此重建等价测量;
- 未验证:任何修复或转换策略的实际效果——A/B/C 分流、空行剔除、块边界截断都还没有实现,
需要等对应 design 批准后用受控样本测试。