记录 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 当前阶段补对应条目。
This commit is contained in:
2026-08-22 15:05:35 +08:00
parent 8c23ac5521
commit 48dd02ce1d
2 changed files with 173 additions and 0 deletions
+3
View File
@@ -37,6 +37,9 @@ profile 表达论文、政务文档、RAG、文档对比等不同需求。
`research-wiki/scratch/html-table-cleaning-ecosystem-research-2026-08-21.md`):核实 Pandoc 表格
方言能力边界、Turndown 不处理合并单元格、Docling Markdown 导出重复合并单元格内容、MinerU 全
HTML 输出,印证审计 T001 的分流方向。
- 2026-08-21 完成 45 份测试 Markdown 中 HTML 表格的只读结构分析
`research-wiki/scratch/html-table-real-data-analysis-2026-08-21.md`):剔除空 `<tr>`
简单表 53% / 合法合并表 8% / 损坏表 40%,并定位标签不闭合、丢失 colspan 标注、无表头三类主因。
- 2026-08-21 仓库由 `govdoc-md-cleaner` 更名为 `mdpolish`,GitHub 远程仓库与本地目录同步改名;
冻结的 design 记录和带日期的 scratch 笔记保留当时的旧名。
@@ -0,0 +1,170 @@
# 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 批准后用受控样本测试。