犀牛鸟 2026 研究笔记 estelledc.github.io

案例 #1725:HTML 表格归一化与分块保护

结论先行

PaddleOCR-VL 将文档中的表格渲染为带内联样式的 HTML,而 WeKnora 的 chunker 只对 Markdown 管道表做”不可切割”保护。结果是:HTML 表格一旦超过 ChunkSize 就被暴力切断,产出大量残缺 chunk,同时 text-align 等纯视觉属性还浪费了宝贵的 token 预算。

PR #1755 的修复策略是在 docparser 层插入一个 HTML 表格归一化器:能转 Markdown 的转,不能转的(rowspan/colspan)剥离样式属性后保留 HTML。改动仅 149 行新增代码 + 66 行测试,零破坏性,当天合入。

核心启发:管线中间层的输出格式必须满足下游的保护条件——这一原则同样适用于 #1248 的 feedback 数据流。


1. 问题现象(What)

用户上传含表格的文档,经 PaddleOCR-VL 解析后,在知识库搜索时发现:

Issue #1725 的一句话描述:PaddleOCR-VL 解析结果表格 HTML 影响分块效果(中途切断、浪费 Token)。

修复前 / 修复后

指标 修复前 修复后
HTML 大表超 ChunkSize 暴力截断 <td> 简单表→MD 管道表;复杂表去样式
chunker protectedPatterns 只认 \| 管道表 MD 表受保护
Token 占用 大量 style= 剥离或转 MD

定位步骤

rg "HTML|table|Normalize|protectedPatterns" internal/infrastructure/docparser/
rg "PaddleOCR|paddleocr" internal/infrastructure/docparser/
go test ./internal/infrastructure/docparser/ -run HTML -v
  1. paddleocr_vl_converter.go — 谁调用 normalizer
  2. html_table_normalizer.go — 转换逻辑
  3. html_table_normalizer_test.go — 变体覆盖

2. 根因分析(Why)

类比

想象一条流水线:第一个工人(docreader/OCR)把纸质表格抄成”带花体字的手写版”(HTML + 样式),第二个工人(chunker)负责把长文档裁成固定长度的卡片。裁卡片的工人知道”如果看到 Markdown 表格的竖线 |,就整张表不裁”——但他不认识花体字(HTML <table>),所以直接从中间一刀切下去。

管线数据流

flowchart LR
    A[用户上传文档] --> B[docreader<br/>Python / PaddleOCR-VL]
    B -->|HTML table with styles| C[docparser<br/>Go / 格式规范化]
    C -->|Markdown or clean HTML| D[chunker<br/>Go / 分块]
    D --> E[向量存储 & 检索]

    style B fill:#f9d,stroke:#333
    style C fill:#bfb,stroke:#333
    style D fill:#bbf,stroke:#333

关键路径:问题出在 B→C→D 这段。docreader 输出了 HTML 表格(这是 PaddleOCR-VL 的默认行为,无法轻易修改),而 docparser 原本没有对 HTML 表格做任何处理就直接传给 chunker。chunker 的 protectedPatterns 正则只匹配 Markdown 管道表语法(|...|...|),对 <table>...</table> 完全无视。

两个问题的交织

问题 原因 影响
Token 浪费 每个 <td> 携带 style="text-align:center" 等纯视觉属性 同样内容的表格,HTML 格式 token 数是 Markdown 的 2-3 倍
分块切断 chunker 只保护 Markdown 管道表,不保护 HTML <table> 表格超过 ChunkSize 时被粗暴截断,产生残缺 chunk

关键文件

文件 角色 改动类型
internal/infrastructure/docparser/html_table_normalizer.go 新增归一化器 NEW (+77)
internal/infrastructure/docparser/html_table_normalizer_test.go 单元测试 NEW (+66)
internal/infrastructure/docparser/paddleocr_vl_converter.go 接入归一化器 MODIFIED (+6)

3. 解决方案(How)

设计思路

在 docparser 的 PaddleOCR-VL converter 中,对所有 <table> 块执行归一化:

  1. 简单表格(无 rowspan/colspan):转为 GFM Markdown 管道表 → chunker 的 protectedPatterns 天然覆盖
  2. 复杂表格(含 rowspan/colspan):Markdown 语法无法表达合并单元格,保留 HTML 结构但剥离所有 presentational attributes(stylealignwidth 等)→ 减少 token 浪费

关键代码片段

// html_table_normalizer.go — 核心逻辑(简化)
func NormalizeHTMLTables(content string) string {
    // 用正则提取所有 <table>...</table> 块
    return tablePattern.ReplaceAllStringFunc(content, func(table string) string {
        if hasComplexStructure(table) {
            // 复杂表格:剥离样式但保留结构
            return stripPresentationalAttrs(table)
        }
        // 简单表格:转为 GFM Markdown
        md, err := convertToMarkdown(table)
        if err != nil {
            return stripPresentationalAttrs(table) // fallback
        }
        return md
    })
}

两种策略对比

表格类型 判断条件 处理方式 输出格式 chunker 保护
简单表格 无 rowspan/colspan html-to-markdown/v2 table plugin 转换 \| col1 \| col2 \| protectedPatterns 覆盖
复杂表格 含 rowspan/colspan 剥离 style/align/width 等属性 干净的 <table> HTML 依赖 ChunkSize 兜底(已减少 token 浪费)

接入点

paddleocr_vl_converter.go 中,解析完成后调用归一化:

// paddleocr_vl_converter.go — 接入(+6 行)
func (c *PaddleOCRVLConverter) Convert(raw string) string {
    result := c.parseRawOutput(raw)
    // 新增:HTML 表格归一化
    result = NormalizeHTMLTables(result)
    return result
}

4. Review 与合入(Maintainer 视角)

PR #1755 由 lyingbug(WeKnora 主维护者)自己提交并合入,无外部 review 评论。从 PR 结构可以推断合入信心来源:

这是一个典型的”信心足够则快速合入”场景——改动小、测试全、有 fallback、作者即维护者。


5. 可复用模式(Pattern)

模式一:管线中间层格式归一化

场景:上游输出格式 A,下游只对格式 B 有特殊保护/优化。

手法:在中间层插入 normalizer,将 A 转为 B;无法完美转换时做 graceful degradation(降级处理但不恶化现状)。

适用条件

模式二:双策略 + Fallback

场景:输入有两种子类型,需要不同处理路径。

手法

  1. 判断子类型(简单 vs 复杂)
  2. 优选路径处理(转 Markdown)
  3. 降级路径兜底(剥离属性)
  4. 转换出错时自动 fallback 到降级路径

模式三:只加不改

PR 净增 149 行,删除 0 行。新文件封装所有逻辑,老文件只加 6 行调用。这种”加法式修复”的好处:


6. 常见误区

错误认知:在 chunker 层用正则删 HTML 标签就能解决表格截断。
正确理解:chunker 只保护 Markdown 管道表;应在 docparser 把 HTML 转为 MD 或瘦身。

错误认知:rowspan/colspan 表转成 Markdown 一定更好。
正确理解:复杂表 保留 HTML 结构、剥离样式 比错误 MD 更安全。

错误认知:PaddleOCR 输出改上游比中间层归一化更干净。
正确理解:第三方 OCR 升级周期不可控;中间层归一化 掌控力强、回滚易

错误认知:style 属性不影响语义检索,可以忽略。
正确理解:style 浪费 token、稀释表格数据向量,应剥离。


7. 思考点

Q1:请画出一份完整的数据流图,标注 #1725 修复前后,一个含 3 行表格的文档经过管线各阶段时的格式变化(包括 token 数估算的变化方向)。

Q2:如果 #1248 的 feedback 数据流是 用户点踩 → message 记录 → chunk 定位 → snippet 归因 → 知识库修正,其中哪一步最可能出现类似 #1725 的”格式假设不匹配”问题?你会如何设计防御?

Q3:假设未来 PaddleOCR-VL 升级后直接输出 Markdown 表格,html_table_normalizer.go 会怎样?这对你设计”只加不改”风格的中间层有什么启示?


8. 延伸阅读