跳到主要内容

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
各格式 parserdocx/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 为例——

  1. 逐页渲染成图片,pdfplumber 试着抽文字;发现是扫描件/乱码就清空,走 OCR。
  2. OCR 认出每个文本框的字,得到一堆带坐标的 box。
  3. 版面模型给每个 box 贴标签:这是标题、这是正文、这是页脚(页脚会被丢掉)。
  4. 表格区域单独抠图,跑 TSR 认行列;若表格是竖排/拍歪的,先转正再重认。
  5. 把 box 按「双栏阅读顺序」合并、跨行拼接成完整段落,每段带上 @@页-页\tx0\tx1\ttop\tbott## 位置标签。
  6. 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:313cjk_ratio < 0.05 and punct_ratio > 0.4)。

OCR 兜底。 页面级清空后,__ocrpdf_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>=9pdf_parser.py:1670)——扫描件太糊时放大能救回来。
  • 并行。 多卡时用 asyncio.Semaphore 按设备分页并行 OCR(pdf_parser.py:74:1622)。

3.2 版面分析(DLA):给每个 box 贴身份

要解决的小问题: OCR 只认字,不知道「这块是标题还是页脚」。检索时页眉页脚水印是噪声,必须能认出来丢掉。

思路。 _layouts_recpdf_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(用来事后判断哪些是反复出现的页眉页脚水印)。

关键细节:

  • 模型是 YOLOv10LayoutRecognizer4YOLOv10layout_recognizer.py:168),底层 ONNX Runtime。
  • 可切换后端:设了 DEEPDOC_URL/TENSORRT_DLA_SVR 环境变量就走远程 DLA 服务,否则本地加载 rag/res/deepdoc/layout.onnxlayout_recognizer.py:52);华为昇腾走 AscendLayoutRecognizer:245)。
  • 打完标签后,box 的 top/bottom 会加上累计页高,变成全文档连续坐标pdf_parser.py:801),方便后面跨页拼接。

3.3 表格结构识别(TSR)+ 方向纠正

要解决的小问题: 表格是二维的,OCR 出来的是一堆散乱文本框。要还原成「第几行第几列、哪些是合并单元格」,才能转成 HTML 表给大模型看。更麻烦的是——表格可能是竖排或拍歪 90°的

思路(_table_transformer_jobpdf_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_orientationpdf_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_tablespdf_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)。

3.4 合并成阅读顺序 + 产出位置标签

要解决的小问题: 到这一步还是一堆散 box。要拼成人能读的段落,还要记住每段「在原文档哪一页哪个位置」,方便检索时溯源、前端高亮。

思路(多步合并):

步骤干什么符号
_text_merge同一行内水平相邻的 box 合并pdf_parser.py:888
_naive_vertical_merge竖直方向朴素合并:926
_concat_downward用 XGBoost 模型判断「上下两块该不该拼成一段」:1030
_extract_table_figure把表格/图片从正文流里抠出来单独处理:1206

_concat_downward 是个亮点:它不是靠规则,而是训了个 XGBoost 小模型(updown_concat_xgb.modelpdf_parser.py:93),拿两块之间的 30 多个特征(行距/字高差/是否以句号结尾/是否项目符号开头…,见 _updown_concat_features :132)来预测该不该拼接。中文没有明显的段落空行,纯规则很难判,用模型学更稳。

位置标签。 每个最终 box 通过 _line_tagpdf_parser.py:1441)生成一个尾巴:

# pdf_parser.py:1454,形如 @@页码-页码\tx0\tx1\ttop\tbott##
return "@@{}\t{:.1f}\t{:.1f}\t{:.1f}\t{:.1f}##".format(...)

这个 @@…## 会跟着 chunk 文本一路带到索引里,检索命中后前端能凭它在原 PDF 上画框高亮。这就是「chunk 文本 + 版面元数据」里的版面元数据

两个轻量替身。 不想跑全套视觉流水线时,PlainParserpdf_parser.py:2005)直接用 pypdf 抽纯文本按 outline 切;VisionParser:2026)把整页图片直接丢给多模态大模型(VLM)描述——naive.py:307/318 会按 parser_configlayout_recognize 配置在三者间选。


4. deepdoc/vision 的 ONNX 模型与独立服务

4.1 模型都是 ONNX,本地跑

deepdoc/vision/ 下三个识别器都继承 Recognizerrecognizer.py:31),构造时用 onnxruntime 加载 .onnx 权重:

# recognizer.py:48
self.ort_sess, self.run_options = load_model(model_dir, task_name)

模型清单(来自 HuggingFace InfiniFlow/deepdoc,Apache 2.0;本地缺文件会自动 snapshot_download):

文件大小用途底层
layout.onnx75.7 MB版面分析 DLAYOLOv10
det.onnx4.7 MBOCR 文本检测PP-OCRv4
rec.onnx10.8 MBOCR 文本识别PP-OCRv4
tsr.onnx12.2 MB表格结构识别PaddleDetection
ocr.res26 KBOCR 字典

依据:deepdoc/server/README.md 模型表 + layout_recognizer.py:63table_structure_recognizer.py:42ocr.py:72snapshot_download/load_model

OCR 内部再拆成 TextDetectorocr.py:420,出文本框四边形)和 TextRecognizerocr.py:139,框内认字),get_rotate_crop_imageocr.py:591)负责把倾斜框摆正再识别。

4.2 独立 HTTP 服务(LitServe)

同一批 ONNX 模型还能脱离主进程、包成一个独立 HTTP 服务,供别的语言(如 RAGFlow 的 Go parser)调用。入口 deepdoc/server/deepdoc_server.py:65,用 LitServe 起服务:

# deepdoc_server.py:73-93(节选)
apis = [DLAEndpoint(...), OCREndpoint(...), TSREndpoint(...)]
server = ls.LitServer(lit_api=apis, accelerator="cpu", ...)
server.run(port=args.port) # 默认端口 9390

端口订正: 此 commit 下默认端口是 9390deepdoc_server.py:36 --port default=9390,README deepdoc/server/README.md:20 也写 9390)。源码(*.py)里没有 8124——8124 只出现在仓库自带的 CLAUDE.md:40/:116(那份面向 agent 的文件把端口误载成 8124)。这里以源码为准:真正生效的是 9390。

端点(deepdoc/server/README.md):

方法路径作用
GET/health存活探针
GET/model返回 {"model":"oss","version":"1.0"}
POST/predict/dla版面分析
POST/predict/tsr表格结构识别
POST/predict/ocrOCR,用表单字段 operator=det(检测)或 operator=rec(识别)

链路:endpoints/(HTTP 层)→ adapters/(模型包装)→ deepdoc/vision/(复用同一批模型类)→ ONNX Runtime。


5. 多格式 parser 如何归一化

不是所有文件都要走视觉流水线。Office/网页/结构化格式各有原生 parser,最后都汇成同一种结构(PDF 是 box 列表;其余多为 {text, doc_type_kwd} 或 sections 三元组),后续切块逻辑就能统一处理。

格式parser归一化做法
Worddocx_parser.py逐段落取文本 + 表格 + 内嵌图,出 (text, image, tables)
Excelexcel_parser.py:RAGFlowExcelParser每行拼成一句 列名:值,超大表分块
PPTppt_parser.py逐页取形状文本
Markdownmarkdown_parser.py切分标题层级、表格转 HTML、可抓远程图
HTMLhtml_parser.py抽正文、去脚本样式
JSONjson_parser.py按结构递归展开
图片picture / figure_parser.py直接 OCR 或交 VLM 描述
简历resume/step_one.pystep_two.py抽字段 → 结构化实体(学历/工作/技能)

外接重型引擎(可选旁路)。 想要更强的解析质量时,可接第三方引擎,它们各自把结果再归一化回 RAGFlow 结构:

  • mineru_parser.py — 接 MinerU。
  • docling_parser.py — 接 IBM Docling。
  • paddleocr_parser.py — 接 PaddleOCR 全家桶。
  • tcadp_parser.py — 接腾讯云文档解析。
  • opendataloader_parser.py — 接 OpenDataLoader。

图片描述(VLM)。 抠出来的图/表若配了多模态模型,figure_parser.py:VisionFigureParser 会调 VLM 生成一段文字描述(prompt 见 rag/prompts/generator.py:vision_llm_figure_describe_prompt),让「图里的信息」也能被检索到——这是把图变成可检索文本的关键一步。


6. 模板分块 rag/app/*.py

要解决的小问题: 同样一堆干净片段,论文该按「摘要/章节」切,合同该按「第X条」切,简历该按「字段」切,问答对该成对切。一刀切的分块会毁掉文档固有结构。

思路:一种文档类型一套 chunk() 后台 task executor 用一张工厂表按 parser_id 分发:

# rag/svr/task_executor.py:114 FACTORY(节选)
FACTORY = {
"general": naive, ParserType.NAIVE.value: naive,
ParserType.PAPER.value: paper, ParserType.BOOK.value: book,
ParserType.LAWS.value: laws, ParserType.MANUAL.value: manual,
ParserType.QA.value: qa, ParserType.TABLE.value: table,
ParserType.RESUME.value: resume, ParserType.PRESENTATION.value: presentation,
...
}
# :280 chunker = FACTORY[task["parser_id"].lower()]

每个模块暴露同一个 chunk(filename, binary, ...) 入口。分发对照:

parser_id模块适用 & 切块策略
naive / generalrag/app/naive.py:839通用:按分隔符切碎,再按 token 数合并成块
paperrag/app/paper.py论文:识别摘要/章节标题分层
bookrag/app/book.py书籍:按章节
lawsrag/app/laws.py法律:按「第X条/章/节」
manualrag/app/manual.py手册:按小节
qarag/app/qa.py问答对:Q/A 成对成块
tablerag/app/table.py表格文件:按行/表
resumerag/app/resume.py:2469简历:结构化字段
presentationrag/app/presentation.py幻灯片:一页一块
picture / one / audio / email / tag各同名模块图片/整文件不切/音频转写/邮件/打标

以 naive 为例走一遍。 chunk()naive.py:839)先按扩展名选原生 parser 拿 sections,PDF 走 Pdf(PdfParser)naive.py:607,内部就是 §3 那套 RAGFlowPdfParser),再交 naive_merge 系列按 chunk_token_num生效默认 128task_executor.py:315naive.py 各真实分支、naive_mergerag/nlp/__init__.py:1070)都取 128;naive.py:851 那个 512 只是parser_config 时的兜底字典,几乎不命中,别当成实际默认)和分隔符 \n!?。;!? 合并成块:

# naive.py:611(PDF 分支:视觉流水线 → (文本, 位置标签) 列表)
return [(b["text"], self._line_tag(b, zoomin)) for b in self.boxes], tbls

与第 02 章对齐: 02 §3.2 讲写入线时也把 chunk_token_num 生效默认写作 128——两章口径一致,别被 512 误导。

本章边界到此为止。 chunk() 结尾会调 tokenize_chunks / naive_merge 产出片段——再往后的分词、embedding、写索引属于 02 入库写入线,本章不展开。


7. 边界与局限(诚实)

  • 端口以源码为准,不以文档口径为准。 独立服务默认 9390,非仓库 CLAUDE.md 误载的 8124(见 §4.2 订正)。
  • 重度依赖模型质量。 OCR/DLA/TSR 都是固定 ONNX 权重,遇到训练分布外的版式(手写、极端花哨排版、罕见语种)会退化,且无在线纠错。
  • 方向纠正只处理 0/90/180/270°的整旋。 轻微倾斜(skew,几度的歪)不在 _evaluate_table_orientation 的四选一里,靠 OCR 框自身鲁棒性兜。
  • 慢。 每页要渲染成图 + 多次 ONNX 推理,扫描件还可能放大重试(zoomin*3)。视觉流水线远比纯文本 split 重——所以才有 PlainParser/外接引擎作为取舍。
  • 乱码检测是启发式,有阈值。 坏码占比、子集字体占比、CJK/标点比都是硬编码阈值(如 0.5/0.3/0.05/0.4),边缘文档可能误判——真乱码没清(漏),或正常小样本被误清转 OCR(枉)。
  • 表格转 HTML 会丢失部分视觉信息(颜色、精细边框),复杂嵌套表的还原不保证 100% 准确。

8. 代码地图(导航索引)

主题文件符号
视觉 PDF 主流水线 / 调度deepdoc/parser/pdf_parser.pyRAGFlowPdfParser__call__parse_into_bboxes
渲染+抽字符+OCR 编排deepdoc/parser/pdf_parser.py__images____ocr
乱码检测三招deepdoc/parser/pdf_parser.py_is_garbled_char_is_garbled_text_is_garbled_by_font_encoding_has_subset_font_prefix
表格方向纠正deepdoc/parser/pdf_parser.py_evaluate_table_orientation_table_transformer_job_ocr_rotated_tables
阅读顺序合并 / 位置标签deepdoc/parser/pdf_parser.py_text_merge_concat_downward_updown_concat_features_extract_table_figure_line_tag
轻量替身 parserdeepdoc/parser/pdf_parser.pyPlainParserVisionParser
版面分析 DLAdeepdoc/vision/layout_recognizer.pyLayoutRecognizerLayoutRecognizer4YOLOv10AscendLayoutRecognizergarbage_layouts
表格结构 TSRdeepdoc/vision/table_structure_recognizer.pyTableStructureRecognizerconstruct_table__html_table
OCR 检测/识别deepdoc/vision/ocr.pyOCRTextDetectorTextRecognizerget_rotate_crop_imagerecognize_batch
ONNX 加载基类deepdoc/vision/recognizer.pyRecognizerload_model
独立 HTTP 服务 (9390)deepdoc/server/deepdoc_server.pymainDLAEndpoint/OCREndpoint/TSREndpoint
各格式 parserdeepdoc/parser/*.pydocx_parserexcel_parsermarkdown_parserfigure_parserresume/step_one/step_twomineru_parserdocling_parser
模板分块分发rag/svr/task_executor.pyFACTORYchunker = FACTORY[parser_id]
naive 切块入口rag/app/naive.pychunkPdfDocxMarkdown
其余模板rag/app/*.pypaperbooklawsmanualqatableresumepresentationchunk

相邻章节: 全景与阅读地图见 index.md;本章产出的 chunk 如何分词/向量化/写库见 02-ingestion-write-path.md;检索与重排见 03-hybrid-retrieval-and-rerank.md