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

案例 #1743:MHTML 图片解析与 Markdown 结构

对齐 写作标准 · 管线标杆 case-1725
PR:#1743 | 8 文件 +1112/-16 | 2026-06-22 合入

结论先行

MHTML 经 Python docreader 转 Markdown 再进 Go docparser 时,有两类独立 bug:(1)Markdown 归一化 删掉空行与行首缩进,破坏段落/列表/代码块/表格边界;(2)图片 resolver 用正则把 ![alt](url "title")整段 target(含 title) 当路径,内嵌图 无法解析

修复:Python 侧保留块结构与 fence 内换行;Go 侧用 状态机 markdown_image_scanner 替代正则,正确拆分 URL 与 title;+600 行级单测与 testdata/mhtml 契约 fixture。

适合谁读:改 Py+Go 跨语言管线、MHTML/多模态 ingest 的贡献者。

与 #1248:间接 — snippet 依赖 chunk 内 完整图片引用;与 case-1725case-1808 合读 = Parse 层 checklist。

Ch13 多模态 · Ch28 docreader · Ch29 docparser


1. 问题现象(What)

1.1 用户可见

![chart](images/example.gif "Q1 revenue")

1.2 修复前 / 修复后

现象 修复前 修复后
段落/列表/引用块 空行、缩进被删 → 结构塌缩 保留有意义块结构;fence 外才归一化多余空行
![...](path "title") lookup path "title" 整串 → 404 scanner 只取 path,title 单独字段
代码块内换行 可能被当普通 MD 处理 fence 内容原样保留
回归 无 titled-image 契约测 testdata/mhtml/titled-image.mhtml + JSON 契约

2. 定位步骤

# Python:MHTML → MD
rg "mhtml|MHTML" docreader/parser/
rg "blank line|indent" docreader/parser/mhtml_parser.py

# Go:图片路径解析
rg "image_resolver|ImageResolver" internal/infrastructure/docparser/
rg "markdown.*image|!\[" internal/infrastructure/docparser/

阅读顺序

  1. docreader/parser/mhtml_parser.py — MD 结构保留
  2. docreader/tests/test_mhtml_parser.py
  3. docparser/markdown_image_scanner.goNEW 状态机
  4. docparser/image_resolver.go — 调用 scanner
  5. testdata/mhtml/titled-image-contract.json — 端到端期望

3. 根因分析(Why)

3.1 类比

MHTML 像 密封箱里的网页 + 相册。Python 工人把网页 复印成 Markdown,却用橡皮擦把 段落空行和缩进 擦掉(章节粘在一起);Go 工人按 标签上的整串字 找相册格(把 "标题" 也当文件名),自然找不到照片。

3.2 跨语言数据流

flowchart TD
    A[.mhtml multipart] --> B[Python mhtml_parser.py]
    B --> C[Markdown 文本]
    C --> D[Go docparser]
    D --> E[markdown_image_scanner]
    E --> F[image_resolver 查内嵌 part]
    F --> G[chunker / embed]
    B -.->|修复前| B1[删空行/缩进 ❌]
    E -.->|修复前| E1[正则整段 target ❌]

3.3 为何正则不够?

Markdown image 语法变体多:() 内可有空格、嵌套引号、<url> 形式。正则 !\[.*\]\(.*\) 无法可靠区分 path 与 "title" — PR 用 state-based scanner(+262 行 + 203 行测)。


4. 解决方案(How)

4.1 Python:保留块结构

4.2 Go:状态机扫描图片

核心思路(概念):

// markdown_image_scanner.go — 扫描 ![alt](destination)
// destination 解析为 URL/path,可选 "title" 与 <angle> URL 形式分离
// image_resolver.go 仅拿 path 查 MHTML multipart

4.3 为何不在 Python 侧删掉 title?

title 是 合法 MD;删掉会丢语义。应在 resolver 解析 destination 时剥离。

4.4 逐文件改动表(8 文件)

文件 ± 说明
docreader/parser/mhtml_parser.py +107/-6 MD 结构
docreader/tests/test_mhtml_parser.py +147 Python 单测
docparser/markdown_image_scanner.go +262 NEW 状态机
docparser/markdown_image_scanner_test.go +203 scanner 测
docparser/image_resolver.go +10/-10 接 scanner
docparser/image_resolver_test.go +324 resolver 测
testdata/mhtml/titled-image.mhtml +48 fixture
testdata/mhtml/titled-image-contract.json +11 契约

读 PR 技巧:+1112 行 ≈80% 是测试;逻辑增量看 parser + scanner + resolver 三处。

4.5 验证命令

cd docreader && python -m pytest tests/test_mhtml_parser.py -v
go test ./internal/infrastructure/docparser/ -run 'MarkdownImage|ImageResolver' -v

5. Review 与合入

PR 四段式

内容
What MHTML 图 unresolved;MD 结构塌缩
Why 过度归一化 + 正则 image target
How 结构保留 + state scanner + fixtures
Test pytest + go test + testdata 契约

维护者 checklist


6. 与 RAG 管线

影响
Parse 图片能否变成可 OCR/可 cite 的资源
Chunk 结构塌缩 → 边界错 → 检索噪声
Generate 多模态问答缺图上下文

与 case-1725:1725 是 HTML 表格 vs chunker;1743 是 MD 结构 + 图片语法 vs resolver — 同属 上游格式满足下游假设


7. 可复用模式


8. 常见误区

错误认知:MHTML 是小众格式,修 Python 就够。
正确:Go image_resolver 才是 lookup 失败点。

错误认知:删掉 image title 最简单。
正确:破坏 MD 合规;应 解析 destination

错误认知:+1112 行太大不能学。
正确:测试占绝大多数 — 学 scanner + fixture 模式

错误认知:与 #1248 无关可跳过。
正确:snippet 展示依赖 完整 chunk 文本


9. 思考点

  1. 只修 Go scanner、Python 仍删空行 — 还会坏什么?
  2. 设计 3 条 ![...](...) 变体单测(角括号 URL、无 title、嵌套括号)。
  3. case-1808 PDF 与 1743 MHTML — 跨语言 checklist 还应含哪条?

章末自检


延伸阅读