ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

docling实战:用视觉模型攻克PDF表格与复杂排版解析

docling实战:用视觉模型攻克PDF表格与复杂排版解析 做文档解析做了快十年见过太多项目在“把 PDF 转成 Markdown”这一步夭折。普通文本抽取还好说一到表格、多栏、页眉页脚那些开源库就集体失灵。直到我前段时间深入用了 IBM 开源出来的 docling才感觉这条路终于走对了。这篇文章不吹不黑把我从安装到跑通、再到接入 RAG 流水线的完整过程包括踩过的坑一次性说清楚。如果你被 PDF 表格重排、扫描件 OCR、多栏论文解析这些问题折磨过或者正在搭知识库底座、做 RAG 数据预处理那这篇内容应该能帮你省掉不少试错时间。1. 被 PDF 表格逼疯的日常为什么常规库搞不定复杂排版先说个反直觉的结论PDF 里的表格在计算机看来根本不存在。PDF 文件最底层存的是文本块、线条、矩形这些图形对象的坐标至于“哪几行组成表头”“这个单元格跨了几行几列”“这块文字是正文还是页脚”文件本身完全不告诉你。传统解析库做的事情本质上是在拿坐标做启发式拼图。比如 pdfplumber它的原理是先通过pdfminer.six把每个字符、每条线的坐标提取出来再用find_tables()方法把横线和竖线的交点当成表格网格线最后把文本按坐标落在格子里。流程图大概是这样的逻辑PDF 内嵌文本 线条坐标 - 检测横线竖线 - 构建网格 - 把文本块填充进网格这个思路对付规规矩矩的简单表格没问题一旦遇到下面这几种情况就开始翻车合并单元格跨行跨列的单元格在坐标网格里对应的是坐标重叠的多个矩形区域传统逻辑很难判断哪个文本该归属哪个合并区域。无框线表格很多现代排版为了好看表格只有较少的竖线甚至完全没有竖线只有间距。网格检测直接失效文本被完全打散。跨页表格页脚、页眉、续表标题混在一起纯坐标逻辑无法重建“这个表在下一页仍在继续”的语义。双栏排版论文里常见的双栏布局。文本坐标上左右两栏混在同一个 Y 区间抽取时经常出现左右两栏文字交错拼接。我早期做过一个财报信息抽取项目客户要求从年报 PDF 里完整抽取“合并资产负债表”。那个表本身没有外框线只有少数横线分割大项前 20 行是流动资产明细后面几十行是合并抵消项。用 pdfplumber 抽出来的结果某一个单元格里的数字直接串到了三行之外最后全靠人工对照复核才没酿成大错。这些痛点本质上指向一个判断只靠坐标做规则匹配永远无法理解文档语义。文档中哪块是标题、哪块是表格、哪个单元格和哪个表头对应这些问题在规则引擎看来是无法清晰表达的。它需要模型从大量带标注的版面数据里学出“长什么样的文本块集合可以被称为表格”“单元格之间的逻辑关系是什么”。图里的内容docling 解决的就是这个层面。2. docling 的解析链路版面分析、TableFormer 与 OCR 的分工逻辑docling 之所以和上面那些传统库不是一回事是因为它把解析工作拆成了一条视觉模型 结构化输出的管线。整个流程不是简单的坐标堆叠而是逐层解构文档语义从视觉层面识别版面再从识别出的区域里抽取精细化结构。2.1 输入的格式归一化docling 的设计思路值得单独拿出来说它把所有输入格式都先归一成统一的中间格式。无论你给的是 PDF、DOCX、PPTX、XLSX 还是图片它先提取内容到统一的DoclingDocument数据结构后续所有处理都只认这种数据。PDF / DOCX / PPTX / XLSX / PNG / JPG | v 输入解析器(按格式分别处理) | v 统一的 DoclingDocument | v 版面分析 - 表格识别 - OCR - 结构化输出这个设计和“先全部转成文本再处理”的思路有本质区别。它保证了后续每个环节拿到的都是完整的布局信息坐标、尺寸、层级关系不会在第一步就丢失版式语义。2.2 版面分析模型先分区块再读内容版面分析Layout Analysis是 docling 最重要的一层。它用基于深度学习的视觉模型识别页面上的区域类型标题、正文、表格、图片、页眉、页脚、页码、侧边栏等。这个模型的训练数据来自DocLayNet数据集里面包括了金融报告、学术论文、技术文档等多种类型的标注页面。每个页面都标出了每个区域的边界框和类型标签。模型学到的不是“横线 竖线 表格”这种表面规则而是“这个视觉区域内部元素排列紧凑、存在网格对齐关系、通常有边框或底色”这种更接近人类直觉的判断方式。这意味着即使表格没有框线只要视觉上有行列对齐关系模型也能把它识别为一个表格区域。这一步直接解决了我前面说的无框线表格这一老大难。2.3 TableFormer合并单元格和复杂表头也能识别识别出“这是表格”之后下一步是识别单元格结构。docling 的表格结构识别模块叫TableFormer这个模型做两件事识别表格的行、列、单元格边界。推断单元格的逻辑角色表头、数据以及合并关系。TableFormer 在训练时学习了大量工业文档表格的标注对列合并、行合并这些复杂结构有专门的建模。我从财报里抽过那种“项目 / 期末余额 / 上年年末余额”的多层表头它把“期末余额”下的“合并 / 母公司”两个子列正确归到了同一父列下输出结果直接就是语义完整的 Markdown 表格。2.4 OCR 兜底扫描件不再是断路PDF 分两种文字型 PDF内嵌文本可直接提取和扫描型 PDF本质是图片。docling 对扫描件会走 OCR 兜底路径。OCR 环节支持 EasyOCR、Tesseract 等后端可配置切换。OCR 识别的结果会带坐标信息重新送入版面分析流程。因此扫描件可以走完整链路OCR 提取文字和坐标 → 版面分析理解区域 → 表格识别重建表格结构。这一整套是从“图片”直接到“结构化文档”而不是像传统方案那样 OCR 完就变成纯文本流、表格结构全部丢失。2.5 统一中间格式与多种输出所有解析结果先进入DoclingDocument然后可以导出成Markdown保留表格、标题层级、代码块适合直接给 LLM 或向量化。JSON带完整的坐标信息、类型信息、结构关系适合程序进一步处理。HTML适合前端预览或 Web 展示。纯文本适合简单全文检索场景。这个分层设计最大的好处是下游需求怎么变上游不用重新解析。你只需要换一种emit方式。3. 从安装到跑通一份真实文档CLI 与 Python API 的实操记录前面原理聊了不少这部分直接上实操。我用的是目前的主分支代码环境是 Ubuntu 22.04 Python 3.10显卡有一张 RTX 3080CPU 也能跑就是慢后面单独说。3.1 环境安装官方推荐通过 pip 安装pip install docling也可以从源码安装以便改底层逻辑git clone https://github.com/docling-project/docling cd docling pip install -e .依赖里会有 torch、transformers、torchvision 这些大块头装的时候建议用虚拟环境隔离。我用过 Python 3.9~3.12 各版本3.10 和 3.11 最稳。注意首次运行需要从 Hugging Face 下载模型权重。如果你所在环境的网络下载不稳定提前把模型下载好放到本地缓存目录否则每次跑都要等很折磨人。模型可通过huggingface-cli download预取或直接手动从 HF 仓库下载放到~/.cache/huggingface对应路径下。3.2 CLI 一句话转 Markdowndocling 提供了命令行工具最简单的用法docling data/example.pdf --to md --output output_dir跑完会在输出目录里生成同名.md文件。不是“能用”那种级别是真的能直接用的 Markdown 表格| 项目 | 期末余额 | 上年年末余额 | | :--- | ---: | ---: | | 流动资产 | 1,234,567,890.00 | 1,100,000,000.00 | | 非流动资产 | 890,123,456.00 | 950,000,000.00 |我是从一个带有合并表头的资产负债表页里直接拿到的输出。传统方案抽这种跨行跨列表格基本都会错位docling 直接给了正确结构。如果你的输入是扫描件 PDFCLI 会自动走 OCR 流程只是速度会明显下降。也可以显式指定某些选项跳过 OCRdocling data/scan.pdf --to md --ocr False还有几个实用选项值得记一下--from指定输入格式比如--from docx在某些格式检测异常时很好用。--page直接指定页码范围比如只要前 10 页避免解析整本厚文件。--json同时输出 JSON方便看结构化数据里到底保留了哪些信息。3.3 Python API把解析嵌进你自己的流程CLI 适合一次性转换真正做工具链集成还是得用 Python API。核心就是一个DocumentConverterfrom docling.document_converter import DocumentConverter source data/example.pdf converter DocumentConverter() result converter.convert(source) doc result.document markdown_output doc.export_to_markdown() print(markdown_output)如果想要 JSON把导出函数换成doc.export_to_dict()即可。我实际用的时候还会额外保留一些元信息比如来源文件路径、页码、区域坐标。docling 的 JSON 输出里会保存每个元素的 bounding box 和页码这在后面做 RAG 引用溯源时非常有用。# 详细一点的转换配置 from docling.datamodel.base_models import InputFormat from docling.document_converter import DocumentConverter, PdfFormatOption from docling.datamodel.pipeline_options import PdfPipelineOptions pipeline_options PdfPipelineOptions() pipeline_options.do_ocr True pipeline_options.ocr_options.lang [en, zh] # 按需开启多语言 OCR converter DocumentConverter( format_options{ InputFormat.PDF: PdfFormatOption(pipeline_optionspipeline_options) } )3.4 输出 JSON 的层级结构自检我强烈建议第一次跑通后先打开 JSON 输出看一眼它的层级设计不要直接拿 Markdown 往下游丢。DoclingDocument 的 JSON 结构大致是这样组织的doc ├── text │ └── 各种文本元素带坐标和标签 ├── tables │ └── 表格元素带单元格结构、行列索引、坐标 ├── pictures │ └── 图片元素带坐标和尺寸 └── page └── 页码、页面尺寸、区域坐标映射理解了这套层级后续你想“只取每页的表格内容”或者“跳过图片分析”这种定制需求就非常容易实现。4. 三类典型文档实测财报、学术论文、扫描件的解析效果对比只说原理不给数据相当于纸上谈兵。我挑了三类代表性文档做了完整实测4.1 财报 PDF合并单元格和无框线表格测试对象是一份上市公司年报里的“合并资产负债表”无外框线、有大量合并单元格、跨页。结果表格区域识别准确识别全部表格区域没有把表格切碎。单元格归并跨行跨列的归并逻辑符合预期表头层级完整。Markdown 输出表格符合 GitHub 风格规范可直接粘贴到任意 Markdown 渲染器。和 pdfplumber 的对比结果如下对比项pdfplumberdocling无框线表格识别失败成功合并单元格还原严重错位正确跨页表格语义断裂保持多层表头无法还原正确表格内外文本混淆常见未出现实测结论很直接对于财务、审计这类重度表格场景docling 的效果是传统工具的降维打击。4.2 学术论文双栏、公式、复杂排版测试对象是一篇双栏英文论文 PDF包含公式、图片、参考文献。结果双栏读取正确左右栏文本顺序正确没有交错重现。标题层级标题识别为对应层级的标题元素而不是正文文本。公式行内公式和独立公式不会破坏段落结构但公式本身如果要求严格语义化还有一定局限。图片被识别到pictures区域并保留坐标信息。这里唯一需要注意的地方在于公式如果在极端排版条件下可能需要额外做 LaTeX 转换后处理。docling 本身定位不是公式识别器它对公式处理的方式是把公式作为文本元素保留其上下文位置。如果你的主要诉求是公式识别需要串接其他工具。4.3 扫描件OCR 链路的稳定性测试对象是一份中文表格扫描件 PDF纯图像无文本层。结果OCR 识别中文效果默认后端对中文支持尚可但不是百分之百准确和商用 OCR 还有差距。表格重建OCR 识别出的坐标被成功用于表格结构重建输出仍然保留表格结构。速度明显慢于文字型 PDF如果是上百页的扫描件建议直接上 GPU。4.4 速度与资源占用对比我做了个简单基准处理一个 20 页文字型 PDF。配置耗时显存/内存占用RTX 3080约 25 秒显存约 4GBCPU8核约 4 分钟内存 8GB 左右CPU 可用但每页耗时达到 10 秒以上批量处理场景确实有点折磨人。如果你是生产环境使用强烈建议准备一块 GPU。哪怕是老的 2080Ti 都能有质的飞跃。5. 把 docling 接进 RAG 流水线向量化之前的文档清洗思路其实我一开始用 docling就是想解决 RAG 链路里召回质量差的问题。之前很多朋友做 RAG 的效果不理想很大一部分原因不在于向量模型选得不好而在于喂给向量模型的内容本身就是一团糟的——表格结构被拆散、标题层级丢失、双栏文本交错。召回效果再好也召不回丢失的语义结构。docling 做的事其实就是 RAG 链路前关键的“文档清洗”。把文档转成结构语义完整的 Markdown 后再切块做向量化召回质量会有明显提升。5.1 LlamaIndex 里的现成接入方案LlamaIndex 官方已经有 docling 的接入方案DoclingReader可以直接把 docling 变成数据加载器from llama_index.core.readers import DoclingReader reader DoclingReader(document_pathdata/annual_report.pdf) docs reader.load_data()每个文档块会带上页码和坐标元信息实现引用溯源。实测效果是财务报告的表格召回命中率比纯文本切块方式高了很多——因为现在每个分块拿到的是结构完整的表格而不是被拆成一行一行的碎片。5.2 自定义切块策略按 docling 的结构元素来切LlamaIndex 的现成接入很方便但我自己用的其实是 docling 的 JSON 输出做的自定义切块逻辑。核心思路是先把整份文档解析成 DoclingDocument。遍历 document 里的元素区分出标题、段落、表格、列表。以标题为边界切块表格永远作为一个完整块不跨表格拆分。给每个块附加页码和坐标信息。from docling.document_converter import DocumentConverter converter DocumentConverter() result converter.convert(data/annual_report.pdf) doc result.document chunks [] current_section None for item in doc.texts doc.tables: label item.label.value if hasattr(item.label, value) else str(item.label) if label in (title, section-header): current_section item.text if item.text else section elif item is not None: # 表格单独作为一个块 chunk_text item.export_to_markdown() if table in label else str(item) chunks.append({section: current_section, text: chunk_text})切出来的块再去做 embedding效果远远好于固定 500 字符硬切。为什么因为固定长度切分最容易把表格从中间切断一行行孤立的数据块完全失去了上下文语义。5.3 一个容易被忽略的价值引用溯源RAG 场景里用户经常问“这个数字从哪来的”如果你只是把文档切成文本块丢给向量库溯源时只能定位到“文档某一页”没法精确到“这个数字出自哪个表格的哪一行”。docling 的 JSON 输出里保留了每个元素在原始 PDF 页面的坐标和页码你可以把这些信息写入向量库的 metadata。用户问“去年营收多少”时系统不仅给出数字还能定位到年报第 X 页的具体表格位置。这个体验级别完全不一样。5.4 让转换结果先进入你的数据管道如果你有更复杂的数据管道比如需要把解析结果存储到数仓、做增量更新docling 可以做到批处理。官方提供.convert_batch()支持一次转换多份文档。我自己做的工具链里就是把解析后的 DoclingDocument 先序列化为 JSON存入对象存储后续无论是生成 Markdown 还是进入向量库都是从 JSON 重新渲染。这样做的好处是如果下游需求变了不需要重新解析 PDF只需重新渲染已有 JSON。6. 实际使用中的注意事项与踩坑清单这部分全是实测中遇到过的真实问题官方文档不一定写这么细。6.1 模型权重下载问题docling 第一次运行要下载多个模型权重包括版面分析模型和表格结构模型。网络不稳定时极易中断。建议先用下面的方式把模型预下载好huggingface-cli download ds4sd/docling-models --local-dir ~/.cache/docling-models如果服务器没有外网访问权限就需要在两台机器之间离线迁移模型文件夹。我第一次在纯内网环境部署时就踩了这个坑整个下载过程一直失败后来靠手动导入模型才跑通。6.2 torch 相关依赖冲突docling 依赖 torch 和 transformers如果你环境中已经装了 FastAI、timm 这类深度学习库版本很容易冲突。建议在虚拟环境里安装 docling不要直接往全局环境里塞。实测下来 3.10 最新稳定版 torch 的组合最稳追新追旧都可能遇到兼容问题。6.3 加密 PDF 和损坏 PDF 的处理docling目前不支持加密 PDF。如果文件打开需要密码你需要先用 PyPDF2 等库解掉密码再交给 doclingfrom pypdf import PdfReader, PdfWriter reader PdfReader(encrypted.pdf) reader.decrypt(password) writer PdfWriter() for page in reader.pages: writer.add_page(page) with open(decrypted.pdf, wb) as f: writer.write(f)损坏的 PDF 偶尔会让模型推理报错或内存暴增建议批量处理前先做文件完整性检查比如用pdfinfo验证文件可读性。6.4 处理超大扫描件的姿势处理几百页扫描件时如果直接把整个 PDF 喂给 converter 转换不仅速度慢还容易中途崩掉。我当时是把 PDF 按页拆成小文件每 10~20 页为一个处理单元批量推进每批次完成后及时释放内存。# 每批页码范围 for start in range(1, total_pages 1, batch_size): end min(start batch_size - 1, total_pages) extract_pages(input_pdf, start, end, fpart_{start}_{end}.pdf) result converter.convert(fpart_{start}_{end}.pdf) # 处理 result这个方法对内存管理非常有效也让失败重试的成本降到最低。6.5 OCR 语言设置docling 的 OCR 默认语言设置不一定包含中文。需要中英文混排的文档时显式指定 OCR 语言pipeline_options.ocr_options.lang [en, zh]不设的话中文文档 OCR 的效果会惨不忍睹。如果 OCR 后发现大量乱码先检查是不是这个问题。6.6 GPU 显存不足的处理小显存显卡跑长文档时偶尔会爆显存。可以把模型精度切换到半精度来降低显存占用pipeline_options.accelerator_options.device cuda pipeline_options.accelerator_options.dtype float16实测 16G 显存跑常见文档都没问题8G 显存通过半精度也能跑只是批量处理时要控制页面数。6.7 明确边界什么场景不要硬上 docling用了一段时间之后我建议对以下场景保持清醒纯文本抽取场景如果你只需要从 PDF 里抽几段文字不需要结构docling 是杀鸡用牛刀慢了也重了。要求公式 LaTeX 语义化的场景docling 不是为复杂公式设计的它的强项是表格和版面结构。超大文件的严格实时场景单页毫秒级响应对 docling 不现实模型推理成本摆在那里。极低资源环境没有 GPU 且对耗时敏感的环境docling 可能不适合。这些边界清楚了你才好在项目里正确评估是否值得引入它。个人经验来说docling 最值得投入的场景是多种复杂排版的文档需要统一解析成结构化数据。它把传统方案里百分之七八十正确率的表格识别提升到了可以直接投产的水平。如果让我重新搭知识库底座docling 大概率会是我固定管线的第一环——毕竟后面的 RAG 也好、数仓也好都是建立在第一步解析质量之上。最后再分享一个小细节生产环境跑批量之前先拿三五份同类文档做一次结构自检重点看表格边界和标题层级是否符合预期确认后再铺量处理能省下大量的返工时间。
返回列表