去年年底我接到一个活儿:把一个客户积压了好几年的行业研报PDF全部转成结构化数据,大概两千多份,里面全是扫描页、复杂表格、多级标题,还有些图片里带数据。我一开始用的是老路子,PyPDF2抽文本、pdfplumber抓表格、Tesseract硬怼扫描页,结果挫败感拉满——抽出来的文字顺序是乱的,表格结构识别得七零八落,折腾了一周才处理了不到一百份,质量还不行。
后来在GitHub热门仓库里翻到IBM开源的docling,抱着试试看的心态跑通了一次,整个思路就变了。它有别于我习惯的那种“逐页抽文本”的方案,而是先做版面分析,再做表格结构识别,再按文档层级重组,最终直接输出结构完整的Markdown和JSON。那批研报我用它重跑了一遍,处理效率和下游使用体验都提了一个档次。这篇文章就把我这一个多月实际跑docling的完整经验写出来,包括安装、核心原理、参数调优、踩坑记录,以及它在我这里最适合干的活儿。
1. 文档处理的老问题:抽出文本容易,还原结构难
在展开docling的具体用法之前,我得先把这类工具解决的痛点说透。很多人以为“PDF转Markdown”就是把文字提出来而已,真上手干过才知道,90%的活儿难在怎么把版面结构还原出来,而不是识不识别得了字。
1.1 传统文本抽取方案的几个死穴
我以前常用的方案无非是PyPDF2、pdfplumber、pdfminer.six这几个库。它们擅长的事情是“把页面上的字符流按坐标提取出来”,注意是字符流,不是文档结构。这意味着:
- 多栏排版的研报,左边栏和右边栏的文字会被混在一起,读取顺序完全错乱;
- 标题和正文的层级关系没有任何标记,只能靠字体大小去猜;
- 表格识别基本靠“线段交叉”去猜单元格,遇到无框线表格、跨页表格、合并单元格就直接崩;
- 扫描件和图片型PDF完全没有文本层,传统抽文本方案直接报废,只能上OCR;
- 页眉页脚、页码、脚注会混进正文,定向下游RAG检索时全是噪音。
我之前用pdfplumber处理一份带三栏排版的行业分析报告,提取出来的文本从头到尾读不通,句子在半截断掉,数据表格里的数字跑到了段落文本中间。这种输出别说喂给大模型做问答,自己看都费劲。
1.2 docling的解法:先分块,再识别,最后重组
docling的核心思路跟传统方案不一样。它不直接去“读字符”,而是先把整个页面切成视觉区块,识别哪些区域是标题、段落、表格、图片、公式、页眉页脚,然后分别处理再按阅读顺序重组。
这个过程类似人眼读文档的方式:先看整个版面的结构,再看每个区域里的内容。好处非常明显:
- 处理多栏排版时,文本块按版面顺序重组,不再串行错乱;
- 表格单独走一路表格结构识别模型,输出的Markdown表格基本可以直接用;
- 扫描件可以挂OCR引擎补充文本层,不要求PDF本身带文本;
- 页眉页脚、页码有专门的区域类型标记,可以在输出时过滤掉;
- 最终同时生成Markdown和JSON,JSON里保留了区块坐标、层级关系、类型标签,方便下游做细粒度处理。
我实际用下来,它在版面还原上的表现,比“抽字符+猜结构”的传统路子强太多。尤其是那种图文混排、表格密集的行业报告,输出质量是质的差别。
2. 环境准备与最小跑通:从安装到第一次看到输出
这块我踩过不少坑,先把最简路径说清楚。docling的安装本身不复杂,但有几个细节不注意会卡住很久。
2.1 安装环节的几个实际注意点
docling是基于Python的,官方推荐Python 3.10以上版本。我用的是3.11,跑起来没什么问题。安装命令:
pip install docling这里有个容易疏忽的点:docling会拉取torch、torchvision这类重依赖,不加任何处理的话pip会默认装CPU版还是GPU版取决于你的环境。如果机器上有NVIDIA显卡且装了CUDA,建议先装好对应版本的torch再装docling,否则会自动装成CPU版,跑起来慢得让人怀疑人生。
我自己的工作站配置是16核CPU加一张RTX 3060 12GB显卡,显存不算大但跑docling完全够用。内存方面建议至少16GB,因为处理大文件时版面分析模型和OCR模型会同时吃内存。
安装完成以后,先跑一下版本确认:
docling --version首次运行docling会下载模型文件到本机缓存目录(一般是~/.cache/docling),包括版面分析模型、表格结构识别模型、还有OCR相关的组件。如果你在的网络环境下访问模型下载源比较慢,这一步可能会卡很久,解决办法我后面单独说。
2.2 命令行一键转换体验
docling提供了非常简洁的命令行接口。拿一份PDF直接跑:
docling /path/to/your/document.pdf默认情况下,它会在同目录下生成三个文件:document.md、document.json、document.html。我第一次跑通的时候真有点意外,一份二十多页的双栏排版PDF,转出来的Markdown格式准确,标题层级清楚,表格是标准的Markdown表格,代码块、引用块也都保留着。
如果想只输出一种格式,用--output-format参数控制:
docling /path/to/your/document.pdf --output-format md支持的格式包括md、json、html、text。text格式就是纯抽文本,适合只关心文字的场合。
2.3 Python API的极简调用
CLI适合快速体验,真正做批处理肯定要走Python API。最简调用:
from docling.document_converter import DocumentConverter source = "/path/to/your/document.pdf" converter = DocumentConverter() result = converter.convert(source) # 输出Markdown print(result.document.export_to_markdown()) # 输出JSON结构 print(result.document.export_to_dict())这段代码量不大,但已经把docling最核心的用法覆盖了:DocumentConverter负责把输入文档转成内部统一的文档对象,然后这个文档对象可以导出成任意格式。
我批量处理那两千多份研报的核心循环简化后长这样:
from pathlib import Path from docling.document_converter import DocumentConverter converter = DocumentConverter() input_dir = Path("./reports") output_dir = Path("./output_md") output_dir.mkdir(exist_ok=True) for pdf_file in input_dir.glob("*.pdf"): result = converter.convert(str(pdf_file)) md_content = result.document.export_to_markdown() (output_dir / f"{pdf_file.stem}.md").write_text(md_content, encoding="utf-8") # 简单打点看进度 print(f"processed: {pdf_file.name}")这个脚本看着简单,但直接跑有个隐患——每处理完一份文档,模型都常驻内存,遇到特别大的文件内存会持续上涨,批量跑多了会卡死。我的解决办法是每处理一定数量就重启进程,或者按文件大小分批跑。这个坑后面踩坑章节细说。
3. 核心能力拆解:docling在哪些环节上真正下了功夫
docling的能力不是靠一个模型打天下,而是一套组合流程。深入理解每个环节的作用,才知道它适合处理什么文档,不适合处理什么文档。
3.1 版面分析:把“视觉区域”变成“逻辑区块”
版面分析是docling整个流程的地基。它会用视觉模型把每一页检测成若干个区域,每个区域带一个类型标签,常见的类型包括:
- 标题(Title)和章节标题(Section Header)
- 正文段落(Text)
- 表格(Table)
- 图片(Picture)
- 公式(Formula)
- 页眉页脚(Header/Footer)
- 页码(Page Number)
- 文本框(Text Box)
- 侧边栏和注记
这个区域检测做完以后,docling会再依据区块之间的位置关系做阅读顺序排序。这一点很关键,因为检测到“这是标题”容易,难的是把“第2页的标题A”和“第3页的段落B”按照正确的先后顺序串起来。docling处理这个问题的策略是结合页内坐标和跨页内容特征重组,实测下来对常见的双栏、三栏排版效果都不错。
3.2 表格结构识别:TableFormer与复杂表头
表格是文档解析里公认最难啃的部分。docling在表格上用了专门的TableFormer模型做结构识别,它不只是识别单元格的边界,还会去识别单元格之间的行跨列、列跨行、合并单元格这些复杂的结构关系。
我拿一份带三线表、多级表头和合并单元格的金融数据报表测试过,docling输出的Markdown表格虽然不能做到100%还原,但整体可用度很高。复杂的合并单元格会被拆分成多个单元格并做内容去重,表头层级通过Markdown加粗的方式体现,人工整理成本比传统方案低很多。
但这块别神化它。我实测遇到无边框表格、斜线表头、完全是截图的表格,docling也有识别错位的情况。无边框表格主要靠内容分布推断单元格边界,一旦内容排版密集,比如一行里有五六个短数字列,单元格边界就经常猜偏。斜线表头目前基本无解,识别出来就是一团乱麻。
3.3 OCR兜底:扫描件和图片型PDF的救星
很多老PDF根本没有文本层,本质上是图片。这类文档如果直接用docling不做任何配置,提取出来的内容区基本是空的。docling的解法是集成OCR引擎。
docling支持的OCR引擎不是自己从头训练,而是对接EasyOCR、Tesseract、RapidOCR这些现成的方案,用户按需选用。其中RapidOCR是一个基于PaddleOCR模型的轻量方案,对中文的支持很不错,识别速度也快,我拿它处理一堆中文扫描研报,准确率完全可以接受。
配置OCR的Python写法:
from docling.document_converter import DocumentConverter from docling.datamodel.pipeline_options import PdfPipelineOptions, RapidOcrOptions pipeline_options = PdfPipelineOptions() pipeline_options.do_ocr = True pipeline_options.ocr_options = RapidOcrOptions() converter = DocumentConverter(pipeline_options=pipeline_options) result = converter.convert("./scanned_report.pdf")跑OCR以后,处理时间会大幅上升。同样一份30页的扫描PDF,不开OCR可能几秒钟就出结果了,开了OCR以后直接翻到几分钟。如果你的文档本身有文本层,没必要开OCR,反而拖慢速度还可能引入识别噪音。
3.4 多格式输入:Word、PPT、HTML也不在话下
docling出来以后,我的第一直觉是它是个PDF专用工具,实际测了才发现它支持的不只是PDF,还包括DOCX、PPTX、XLSX、HTML和图片。底层是用一套统一的文档模型去承接不同格式的解析结果,这样下游消费端就不用关心输入格式了。
我试过把一批PPT转成Markdown,版式里的文本框、SmartArt图形、图片会被识别成不同的区块,文字内容基本能保留,但复杂的SmartArt层级结构会丢一部分逻辑关系。这个能力用来做“格式统一”非常合适,比如公司知识库里有PDF、有Word、还有存量网页导出的HTML,用docling统一洗一遍变成干净的Markdown,再全部灌进向量库,省去了为每种格式各写一套解析逻辑的麻烦。
4. 参数调优与批处理:从能跑到好用
CLI跑通只是第一步,真正用到生产级批处理的时候,需要对docling的管道参数做针对性调整。这个章节记录我认为最有价值的几个配置项和批处理策略。
4.1 PDF Pipeline核心参数详解
docling对PDF的处理逻辑封装在PDF Pipeline里,通过PdfPipelineOptions来控制。我最常用到的参数如下:
| 参数 | 作用 | 我的建议值 |
|---|---|---|
do_ocr | 是否启用OCR | 扫描件开,文本型PDF关 |
ocr_options | 指定OCR引擎及语言 | 中文选RapidOCROptions,语言设ch, en |
table_structure_model | 表格结构识别模型 | 默认值即可,V1保精度 |
do_table_structure | 是否做表格结构识别 | 普通文档可关,表格多必开 |
include_page_breaks | 输出Markdown时插入分页标记 | 按需开启 |
num_pages | 只处理指定页数 | 调试时用,省时间 |
一个实际配置示例:
from docling.document_converter import DocumentConverter from docling.datamodel.pipeline_options import PdfPipelineOptions pipeline_options = PdfPipelineOptions() pipeline_options.do_ocr = True pipeline_options.do_table_structure = True from docling.datamodel.pipeline_options import RapidOcrOptions pipeline_options.ocr_options = RapidOcrOptions(lang=["ch", "en"]) converter = DocumentConverter(pipeline_options=pipeline_options)我的经验是,如果一份PDF既有文本层又含扫描页,最好先跑一遍不开OCR的,看看到底哪些页面内容缺失,再针对性开OCR重跑缺失的页面,这样能省不少时间。
4.2 性能调优:大文件、超长文档、内存管理
docling在性能和资源占用上有个比较明显的特点:刚启动时模型加载会把内存拉高,处理大文件时内存继续涨。我拿一份200多页的PDF实测,转换过程峰值内存到了6GB左右,加上OCR更是内存翻倍。
针对大文件的处理,我总结了一套组合拳:
- 按页切片处理。docling的
num_pages参数可以限定处理页数,把大文件按20-30页一批切分,处理完一批释放一批,内存稳定很多。
from docling.document_converter import DocumentConverter from docling.datamodel.pipeline_options import PdfPipelineOptions opts = PdfPipelineOptions() opts.num_pages = 20 # 每次只跑20页 converter = DocumentConverter(pipeline_options=opts) converter.convert("./huge_document.pdf")分段转Markdown再拼接。如果想保留整篇的连续标题层级,可以处理完一个批量后把Markdown片段缓存到磁盘,最后统一拼接。注意拼接时不要简单做字符串相加,最好在批间插入一个分页符占位。
批量任务不要开太多并发。docling转化本身比较吃CPU/GPU资源,多进程并行时显卡显存会不够用。我测试过,同样一台机器跑4个并发任务,比串行跑效率只提升了不到1倍,但内存直接翻了3倍,得不偿失。串行加分批切片是最稳的。
4.3 批处理脚本的稳健写法
批处理PDF的时候,不能假设每个文件都能转换成功。实测下来,某些加密PDF、损坏PDF、扫描质量极差的PDF都会让convert过程抛异常。我的批处理脚本从一开始就套了完整的基础保障逻辑:
import logging from pathlib import Path from docling.document_converter import DocumentConverter logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) def process_pdf(pdf_path, output_dir, max_retries=3): output_path = output_dir / f"{pdf_path.stem}.md" if output_path.exists(): logger.info(f"skip existing: {pdf_path.name}") return converter = DocumentConverter() for attempt in range(1, max_retries + 1): try: result = converter.convert(str(pdf_path)) md = result.document.export_to_markdown() output_path.write_text(md, encoding="utf-8") logger.info(f"done: {pdf_path.name}") return except Exception as exc: logger.warning(f"attempt {attempt} failed for {pdf_path.name}: {exc}") if attempt >= max_retries: logger.error(f"give up: {pdf_path.name}") # 记录失败名单,便于事后人工处理 with open("failed.txt", "a", encoding="utf-8") as f: f.write(f"{pdf_path.name}\n")这套逻辑被我跑了上千次,基本没有翻车的情况。处理失败的文件输出到failed.txt,事后单独人工检查,远比全量重跑效率高。
5. 踩坑记录:实际跑批遇到的几个典型问题
这部分是全文最有价值的章节,全部来自真实使用中的血泪教训。我把这一个月里遇到的最典型、最坑、社区里也被反复讨论的问题整理出来。
5.1 首次运行卡死在模型下载环节
我第一次运行docling转换命令,等了快十分钟都没有反应,一度以为程序卡死了。后来打开缓存目录一看,是模型文件一直在下载,进度也没打印。docling首次运行会下载版面分析模型、TableFormer模型以及OCR相关的组件,几个模型加起来有好几百MB,网络差一点要等很久。
解决方式有两个:一是耐心等首次下载完成,后续使用都会命中本地缓存;二是提前手动下载模型文件放到缓存目录。手动下载这个方案需要知道模型的托管地址,比较折腾,我最后是找了一台网络环境好的机器跑了一次转换,然后把整个~/.cache/docling目录打包拷到内网机器,生物质疑全部搞定。
5.2 中文扫描件OCR效果差,根因是语言没配
有个朋友照我的脚本处理中文扫描PDF,发现OCR出来的内容全是乱码和英文,只能识别数字,中文全丢。我第一反应就是OCR语言没配置。docling的OCR默认语言是英文,不指定中文的话中文字符会被识别成无意义的英文或干脆丢弃。
正确的配置是用RapidOCR并指定语言:
from docling.datamodel.pipeline_options import PdfPipelineOptions, RapidOcrOptions pipeline_options = PdfPipelineOptions() pipeline_options.do_ocr = True pipeline_options.ocr_options = RapidOcrOptions(lang=["ch", "en"])配完以后再跑,中文识别率基本能到95%以上。注意RapidOCR和EasyOCR是两套方案,配置方式类似但语言参数格式有差异,别记混了。
5.3 大尺寸PPTX转换时内存飙到系统崩溃
我以为docling的大文件挑战只会出现在超长PDF上,结果一份三百多页带高清大图的PPTX直接让我服务器内存耗尽,进程被系统杀掉。
排查后发现,docling在处理PPTX时会把每个幻灯片渲染成图像用于版面分析,高清大图会占大量内存。我的应对方案是先用脚本压缩PPTX里的图片分辨率,再把大PPT按章节拆分成多个小文件分批处理。虽然治标不治本,但实际场景里足够用了。
5.4 跨页表格被拆成两个表格
行业研报里大量存在一个表格从页面底部跨到下一页的情况。docling输出时,这类表格有时会被拆成两个独立的Markdown表格,每个只包含部分行列,给下游数据整合带来麻烦。
这个问题我目前没有找到参数化的完美解决方案。我的处理策略是:不苛求docling在转换阶段解决跨页表格合并,而是在JSON输出的基础上写一段后处理逻辑,检测两个表格的列数完全一致且文本连续性匹配,就自动拼接到一起。虽然在复杂表头场景会误合并,但处理常规数据表效果不错。
5.5 OCR模式下速度骤降,需要一个兜底方案
开了OCR以后处理时间会拉长很多倍。我处理一份120页的扫描研报,纯文本型PDF大概十来秒,开了OCR直接跑了差不多十五分钟。这个速度差异主要来自OCR要对每一页的图像逐字识别,页面上每个字都要过一次模型,计算量非常大。
我在生产流程里的兜底方案是:先快速跑一遍无OCR转换,如果文本内容已经完整就没有必要开OCR;只有检测到文本缺失严重时,才对缺失页面单独开OCR重跑。这样既保证了质量又控制了整体耗时。
6. 进阶应用:把docling接入结构化数据流水线
跑通基础转换之后,docling的真正价值在于接入下游数据流水线。这个章节分享几个我实际应用过的进阶玩法。
6.1 JSON中间层的应用价值
docling默认生成的JSON往往是很多新用户忽略的金矿。它不只是“内容的另一种表示”,而是把整个文档的组织结构显式表达出来了。
JSON里每个区块都有类型标签、文本内容、坐标信息和层级关系。基于JSON,你可以做:
- 精准抽取表格,比如只提取全文中所有
table类型区块,喂给下游做数据入库; - 过滤噪音内容,把
header、footer、page_number类型区块直接丢弃,只保留正文; - 按文档原貌还原阅读顺序,不需要依赖Markdown渲染就能拿到符合逻辑的文档流;
- 定制化格式转换,不限于Markdown,比如根据JSON生成自定义的XML、LaTeX或者其他下游格式。
我在一个知识库项目里就是基于JSON层写了一个抽取逻辑:把每份文档的表格区块提取出来,做OCR清理后,直接写入结构化数据库,供业务系统查询。这部分逻辑完全绕开了Markdown,直接消费JSON结构化信息。
6.2 结合LangChain/LlamaIndex做RAG预处理器
做过RAG应用的朋友都懂,PDF文本抽取质量直接决定了检索效果的底线。docling输出干净的Markdown,对RAG链路是天然友好的。
在LangChain里可以这样接入:
from docling.document_converter import DocumentConverter from langchain_core.documents import Document as LCDocument converter = DocumentConverter() def load_docling_document(pdf_path: str): result = converter.convert(pdf_path) md = result.document.export_to_markdown() return LCDocument(page_content=md, metadata={"source": pdf_path})拿到干净Markdown以后,chunk策略可以做基于结构的切分,比如按二级标题切块,而不是简单按固定字符数切。这样切出来的每个chunk内部语义相对完整,检索召回的效果会好很多。
实测下来,同一批法律文书,用docling预处理后的chunk做检索,比用PyPDF2抽取文本做检索,语义相似度Top-5命中率提升非常明显。原因不复杂:docling把标题层级、表格行列、段落边界都还原了,chunk不跨主题、不把表格拦腰切断。
6.3 自动监控文件夹触发的批处理小工具
批量处理文档多了以后,我写了一个非常实用的小工具:监控一个文件夹,新放入的PDF自动转成Markdown并存入指定目录。这个工具帮我省去了手动跑批的功夫,日常新到的文档丢进输入目录就行。
核心逻辑用的是watchdog库监听文件系统事件:
import time from pathlib import Path from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from docling.document_converter import DocumentConverter class PdfHandler(FileSystemEventHandler): def __init__(self, input_dir, output_dir): self.input_dir = Path(input_dir) self.output_dir = Path(output_dir) self.output_dir.mkdir(exist_ok=True) self.converter = DocumentConverter() def on_created(self, event): if event.is_directory: return src_path = Path(event.src_path) if src_path.suffix.lower() == ".pdf": time.sleep(2) # 等文件写完,避免读一半 try: result = self.converter.convert(str(src_path)) md = result.document.export_to_markdown() out_path = self.output_dir / f"{src_path.stem}.md" out_path.write_text(md, encoding="utf-8") print(f"converted: {src_path.name}") except Exception as exc: print(f"error: {src_path.name} -> {exc}") if __name__ == "__main__": handler = PdfHandler("./inbox", "./outbox") observer = Observer() observer.schedule(handler, "./inbox") observer.start() print("watching...") try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join()这个小工具跑了半个月,稳定没出过问题。后面我甚至把失败重试、通知提醒都加了进去,变成了一个完整的轻量级文档处理服务。
6.4 什么时候不要用docling
最后说点劝退的话。docling不是万金油,我实际测试中遇到以下情况就不太适合硬上:
- 纯文本提取场景,只需要把文字拿出来做关键词检索,不需要标题层级和表格结构,直接上PyPDF2或pdfplumber,速度和资源占用都友好得多;
- 复杂填报表单,尤其是带手写内容、勾选框、每行都有大量非结构化布局的,docling的版面分析会把这些识别成普通文本块,抽取效果并不理想;
- 实时性要求极高的在线解析服务,docling首次加载模型的内存和耗时都不小,在线场景需要单独做模型常驻和服务预热,否则单次请求延迟用户接受不了;
- 需要逐字级坐标定位的场景,比如传统OCR管线的字段级抽取,docling的JSON坐标精度不如专用的OCR服务。
工具选型永远是场景驱动的。我的原则是:文档版面整洁、结构丰富、表格密集、需要保留阅读顺序的,用docling收益最大;文本纯、结构简单、重速度轻结构的,传统方案更轻快。
我个人上个月最爽的一次应用,是拿docling清洗完一批供应商合同后,把JSON里的表格区块直接映射成了数据库字段,合同里的金额、期限、条款编号全部翻了进来,后续检索、统计、风险提醒全都顺了。这种体验是传统PDF解析库给不了的。如果你手头也有一批旧文档要洗成干净的结构化数据,docling值得花一晚上试跑一遍。建议第一次跑千万不要一上来就全量处理,先拿十份不同风格的文档跑通、调好参数,再放大到全量。这个习惯,能帮你避开我踩过的绝大部分坑。