DeepDoc — 深度文档理解与模板分块
30 秒导读: RAGFlow 的写入线第一段。别的 RAG 拿到 PDF 就
split("\n");RAGFlow 先把它当图片看——OCR 认字、模型分版面、模型认表格结构、还会把拍歪的表格转正——再按「这是论文/合同/简历/手册…」套一套专门的切块策略。产出是一串带版面坐标和图像的干净片段(chunk),交给第 02 章去分词、向量化、写库。
1. 这是什么(零基础也能懂)
一句话定义: DeepDoc 是 RAGFlow 里负责「把非结构化文档变成结构化片段」的模块——输入一个文件(PDF/Word/Excel/图片…),输出一串「文本 + 它在第几页什么位置 + 配图」的片段。
为什么这件事难。 对纯文本文件(.txt、.md)直接按标点切就行。但真实世界的文档大多不是:
- 扫描件 / 图片 PDF:整页就是一张照片,根本没有文本流,只能靠 OCR(光学字符识别)一个字一个字认出来。
- 复杂排版 PDF:双栏论文、页眉页脚水印、跨页表格、竖排表格。就算 PDF 里嵌了文字,按坐标裸读出来也是乱序的(左栏读一行跳到右栏读一行),页眉页脚还会混进正文。
- 字体编码坏掉的 PDF:老的中文标准文档常把汉字映射到私有区码位或 ASCII 码位,
pdfplumber抽出来是一堆(cid:123)或随机符号——看着有文字,其实是乱码。
如果这一步糊弄过去,后面检索再准也没用——垃圾进,垃圾出。DeepDoc 的价值就是把这一步做扎实。
输入 / 输出(本章的边界):
| 内容 | |
|---|---|
| 输入 | 一个文件(bytes 或路径),及其解析器类型 parser_id(naive/paper/laws…) |
| 输出 | 一串 chunk:(文本, 位置标签),表格另存为 HTML,图片另存为 image,版面类型(标题/正文/表/图)标在每个 box 上 |
| 不在本章 | 分词、embedding 向量、写 ES/Infinity 索引——那是 02 入库写入线 |
一句话直觉: 把 DeepDoc 想成一个尽职的实习生——你丢给他一叠扫描的合同,他不会傻乎乎复述,而是先眯眼认字(OCR)、圈出哪块是标题哪块是表格(版面分析)、把拍歪的表格转正、再按「这是合同」的套路分段誊清。誊清稿才是后面能用的东西。
2. 顶层全景(它大概怎么转)
DeepDoc 分两大块,一条流水线串起来。怎么读这张图:从上到下是一次解析的时间顺序,虚线框是可替换/可选的旁路。
一个文件 (PDF / DOCX / XLSX / 图片 …)
│
┌───────────────┴───────────────┐
│ 按扩展名/parser 选一条解析路径 │
└───────────────┬───────────────┘
│
┌────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌────────────────┐ ┌──────────────────┐
│ 视觉 PDF 流水线 │ │ 各格式原生 parser│ │ 外接引擎(可选) │
│ RAGFlowPdfParser│ │ docx/xlsx/ppt/ │ │ MinerU / Docling │
│ ①OCR认字 │ │ md/html/json/ │ │ /PaddleOCR/TCADP │
│ ②版面分析(DLA) │ │ 图片/简历 … │ └────────┬─────────┘
│ ③表格结构(TSR) │ └───────┬────────┘ │
│ ④方向纠正+转正 │ │ │
│ ⑤合并成阅读顺序 │ │ │
└───────┬───────┘ │ │
└───────────────────┼─────────────────────┘
▼
统一结构:boxes[{text, 版面type, 页/坐标, image}]
│
┌──────────────┴───────────────┐
│ 模板分块 rag/app/*.py │
│ FACTORY[parser_id].chunk() │
│ naive/paper/laws/manual/qa … │
└──────────────┬───────────────┘
▼
一串 chunk:(文本, 位置标签@@…##), 表格HTML, 配图
▼
►► 交给第 02 章(分词/向量/写库)
部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
RAGFlowPdfParser | 视觉 PDF 主流水线:OCR→版面→表格→合并 | deepdoc/parser/pdf_parser.py:57 |
OCR | 文本检测 + 识别(ONNX,PP-OCRv4) | deepdoc/vision/ocr.py:542 |
LayoutRecognizer | 版面分析(DLA):认出标题/正文/图/表/页眉… | deepdoc/vision/layout_recognizer.py:33 |
TableStructureRecognizer | 表格结构识别(TSR):认出行/列/表头/合并单元格 | deepdoc/vision/table_structure_recognizer.py:30 |
| 各格式 parser | docx/excel/ppt/md/html/json/图片/简历 归一化 | deepdoc/parser/*.py |
| 外接引擎 | MinerU/Docling/PaddleOCR/腾讯 TCADP 等重型引擎接入 | deepdoc/parser/mineru_parser.py 等 |
| 模板分块 | 按 parser_id 分发到 15 种 chunk() 策略 | rag/app/*.py,分发表 rag/svr/task_executor.py:114 |
| DeepDoc HTTP 服务 | 把 DLA/OCR/TSR 三个 ONNX 模型包成独立 HTTP 服务 | deepdoc/server/deepdoc_server.py |
主线走一遍(高层,不进代码): 以一份扫描的中文论文 PDF 为例——
- 逐页渲染成图片,
pdfplumber试着抽文字;发现是扫描件/乱码就清空,走 OCR。 - OCR 认出每个文本框的字,得到一堆带坐标的 box。
- 版面模型给每个 box 贴标签:这是标题、这是正文、这是页脚(页脚会被丢掉)。
- 表格区域单独抠图,跑 TSR 认行列;若表格是竖排/拍歪的,先转正再重认。
- 把 box 按「双栏阅读顺序」合并、跨行拼接成完整段落,每段带上
@@页-页\tx0\tx1\ttop\tbott##位置标签。 parser_id=paper→ 走rag/app/paper.py的切块策略,切成 chunk。
3. 视觉 PDF 流水线(核心,逐个机制)
这是 DeepDoc 工程含量最高的一支,也是 RAGFlow 的招牌。整条流水线的调度就在一个 __call__ 里,读它就懂了全貌(pdf_parser.py:1673 RAGFlowPdfParser.__call__):
# 真实调度顺序,pdf_parser.py:1690-1698(节选,去掉参数)
self.__images__(fnm, zoomin) # ① 渲染+抽字符+OCR
self._layouts_rec(zoomin) # ② 版面分析 DLA
self._table_transformer_job(zoomin, ...) # ③ 表格结构 TSR + 方向纠正
self._text_merge() # ④ 同行合并
self._concat_downward() # ⑤ 跨行/跨页向下拼接
self._filter_forpages() # ⑥ 去目录页等
tbls = self._extract_table_figure(...) # ⑦ 抠出表格/图片
下面挑 4 个不显然的机制细讲。
3.1 抽字符 + 乱码检测 + OCR 兜底
要解决的小问题: PDF 里到底有没有能信的文字?有就直接用(快、准),没有或是乱码就得上 OCR(慢、可能错,但至少能认)。判断「能不能信」是关键。
思路。 先用 pdfplumber 逐页抽字符(__images__,pdf_parser.py:1527):
# pdf_parser.py:1539-1544(节选)
with pdfplumber.open(...) as pdf:
self.page_images = [p.to_image(resolution=72*zoomin, ...).annotated for p in pages]
self.page_chars = [[c for c in page.dedupe_chars().chars if self._has_color(c)] for page in pages]
抽完立刻做乱码检测,坏了就把这页的 page_chars 清空,强制走 OCR。检测分三招,层层递进:
| 招 | 判什么 | 怎么判 | 符号 |
|---|---|---|---|
| 单字符级 | 这个字符是不是坏码 | 落在 Unicode 私有区(0xE000–0xF8FF)、替换符 0xFFFD、Cn/Cs 类 → 坏 | _is_garbled_char (:200) |
| 文本级 | 整段坏码占比 | 坏字符 ≥ 阈值(默认 0.5),或命中 (cid:数字) 正则 → 坏 | _is_garbled_text (:228) |
| 字体编码级 | 汉字被映射成 ASCII | 子集字体(XXXXXX+ 前缀)占比高、却几乎无 CJK、满屏 ASCII 标点 → 坏 | _is_garbled_by_font_encoding (:263) |
第三招最巧妙,专治老中文标准文档:正常抽出来「本页无 CJK、全是 !@#$ 这类 ASCII 标点、且这些字来自嵌入子集字体」——这组合几乎不可能是真内容,判定为字体编码坏掉(pdf_parser.py:313 的 cjk_ratio < 0.05 and punct_ratio > 0.4)。
OCR 兜底。 页面级清空后,__ocr(pdf_parser.py:703)对该页跑检测+识别:
# pdf_parser.py:705 / :786(节选)
bxs = self.ocr.detect(np.array(img), device_id) # 文本框检测
...
texts = self.ocr.recognize_batch([b["box_image"] for b in boxes_to_reg], device_id) # 批量识别
关键细节 / 坑:
- 双保险。 就算整页没判成乱码,
__ocr里还会对每个文本框再算一次坏码率,某个框坏码 ≥ 0.5 就清掉它的文字、只对这个框重 OCR(pdf_parser.py:758)。粗粒度页面级 + 细粒度框级,两道闸。 - 能用 pdf 自带字符就不 OCR。 OCR 慢且可能错。对每个 OCR 检测框,会把
pdfplumber的原生字符「塞回」框里当文字,只有塞不进(乱码/无字符)的框才真去跑识别模型(pdf_parser.py:735-789)。 - 放大重试。 如果一整轮下来一个 box 都没有,会把渲染倍率
zoomin*3再来一遍,直到zoomin>=9(pdf_parser.py:1670)——扫描件太糊时放大能救回来。 - 并行。 多卡时用
asyncio.Semaphore按设备分页并行 OCR(pdf_parser.py:74、:1622)。
3.2 版面分析(DLA):给每个 box 贴身份
要解决的小问题: OCR 只认字,不知道「这块是标题还是页脚」。检索时页眉页脚水印是噪声,必须能认出来丢掉。
思路。 _layouts_rec(pdf_parser.py:796)把整页图 + OCR 的 box 喂给版面模型 LayoutRecognizer,模型给每个区域打一个类别标签:
# layout_recognizer.py:34(11 类标签)
labels = ["_background_", "Text", "Title", "Figure", "Figure caption",
"Table", "Table caption", "Header", "Footer", "Reference", "Equation"]
其中 footer/header/reference 被列为垃圾版面(layout_recognizer.py:49 garbage_layouts),drop=True 时直接扔掉,扔掉的内容还会按出现频次统计存进 garbages(用来事后判断哪些是反复出现的页眉页脚水印)。
关键细节:
- 模型是 YOLOv10(
LayoutRecognizer4YOLOv10,layout_recognizer.py:168),底层 ONNX Runtime。 - 可切换后端:设了
DEEPDOC_URL/TENSORRT_DLA_SVR环境变量就走远程 DLA 服务,否则本地加载rag/res/deepdoc/layout.onnx(layout_recognizer.py:52);华为昇腾走AscendLayoutRecognizer(:245)。 - 打完标签后,box 的
top/bottom会加上累计页高,变成全文档连续坐标(pdf_parser.py:801),方便后面跨页拼接。
3.3 表格结构识别(TSR)+ 方向纠正
要解决的小问题: 表格是二维的,OCR 出来的是一堆散乱文本框。要还原成「第几行第几列、哪些是合并单元格」,才能转成 HTML 表给大模型看。更麻烦的是——表格可能是竖排或拍歪 90°的。
思路(_table_transformer_job,pdf_parser.py:409): 对每个被版面模型标为 table 的区域抠图,跑 TSR 模型认结构。TSR 输出 6 类:
# table_structure_recognizer.py:31
labels = ["table", "table column", "table row",
"table column header", "table projected row header", "table spanning cell"]
方向纠正是精华。 抠出表格图后,_evaluate_table_orientation(pdf_parser.py:318)会把它转 0°/90°/180°/270° 各跑一遍 OCR,用识别置信度选最正的方向:
# pdf_parser.py:372(打分:置信度 × 区域数量的小加权)
combined_score = avg_score * (1 + 0.1 * min(total_regions, 50) / 50)
怎么读这张判定图(防止过度纠偏):
四个角度都 OCR → 各得一个 combined_score
│
▼
最高分是 0° ? ──是──► 用 0°(不转)
│否
▼
(最高分 − 0°分 > 0.2) 且 (0°分 < 0.8) ? ──否──► 退回 0°(证据不足,不敢转)
│是
▼
采用该角度,转正后重跑 TSR + 重 OCR
这条「绝对阈值规则」(pdf_parser.py:397)很关键:只有当别的方向明显更好、且 0° 本身确实不行时才转,否则宁可不动——避免把本来正的表格误转坏。转正的表要重新 OCR(_ocr_rotated_tables,pdf_parser.py:556),因为坐标系变了。
认完结构后,把行(R)/表头(H)/列(C)/合并跨格(SP)标签打回每个 box(pdf_parser.py:525-554),construct_table 再据此拼成 HTML <table>(table_structure_recognizer.py:156、__html_table :356)。