数据截至 (上游 commit c4b5ed6202d6)
入口与三级分派:从 source 到流水线
30 秒导读: 你调
converter.convert("a.pdf"),Docling 怎么知道该用哪个后端、哪条流水线? 答案全在DocumentConverter这一层。它把一次转换拆成三级分派:先认出文件是什么格式, 再查表选出该格式的处理配方(FormatOption),最后把配方里指定的流水线实例取出来(能复用就复用), 交给它执行。本章只讲这套"识别 + 选型 + 调度",不进后端和流水线内部(那是 02、03)。
1. 这是什么(零基础也能懂)
DocumentConverter 是 Docling 对外的总开关——你想把任何文档变成统一的 DoclingDocument,
都从它开始。它自己不解析任何东西,只做一件事:把你给的东西,正确地派给合适的干活组件。
打个比方:它像医院的分诊台。病人(文档)进来,分诊台不看病,只做三步:
- 你得的是什么病(PDF?Word?HTML?)—— 格式识别
- 该挂哪个科(用哪个 后端 + 哪条流水线)—— 选配方
- 把你领到那个科室,而且同一个科室的医生不用重新到岗(流水线实例缓存复用)
用起来就一行:
# 示意,非源码
from docling.document_converter import DocumentConverter
converter = DocumentConverter() # 建一次,可反复用
result = converter.convert("path/to/paper.pdf") # 单个文档
print(result.document.export_to_markdown()) # 拿到统一文档,导出 Markdown
它对外暴露三个入口方法,应对三种"东西从哪来":
| 方法 | 输入 | 典型场景 |
|---|---|---|
convert | 单个 source(路径 / URL / 流) | 转一个文件 |
convert_all | 一批 source(可迭代) | 批量转,支持并发 |
convert_string | 一段字符串 + 指定格式 | 手里只有 Markdown/HTML 文本,没有文件 |
一句话直觉:
DocumentConverter= 分诊台 + 一张"格式→配方"的查号表 + 一个"科室不重复到岗"的缓存。 真正看病的是后端和流水线,它只负责把病人送对地方。
2. 顶层全景(它大概怎么转)
一次转换,数据从 source 流到 DoclingDocument,中间正好穿过三道分派门。先看这张图——
从上往下读,每一层把上一层的输出翻译成下一层认识的东西:
你给的 source: Path / str(URL) / DocumentStream / HttpSource
│
▼
┌──────────────────────────────────────────────────────────┐
│ DocumentConverter.convert ──委托──► convert_all ──► _convert │
│ (单个) (批量+并发) (真正的引擎) │
└──────────────────────────────────────────────────────────┘
│ _convert 内部,对每个文档依次过三级分派:
│
▼ ① 认格式 _DocumentConversionInput._guess_format
source ──────────────► InputFormat.PDF / DOCX / HTML / …
│
▼ ② 选配方 _get_default_option(或用户覆盖)
InputFormat ─────────► FormatOption{ pipeline_cls, backend,
backend_options, pipeline_options }
│
▼ ③ 取流水线实例 _get_pipeline(按 options 哈希缓存复用)
FormatOption ────────► 一个 BasePipeline 实例
│
▼ ④ 校验 + 执行 _process_document / _execute_pipeline
allowed_formats 放行 ─► pipeline.execute(in_doc) ──► ConversionResult
**怎么读这张图:**门 ①②③ 把"你给的东西"一步步翻译到"能执行的流水线",门 ④ 做准入检查后交棒。
DocumentConverter 的全部职责就在 ①→④;pipeline.execute 之后的世界属于 03-pipelines。
各部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
convert / convert_all / convert_string | 三个对外入口 | document_converter.py:convert / convert_all / convert_string |
_convert | 批量引擎:分块 + 可选并发 | document_converter.py:_convert |
_DocumentConversionInput | 把 source 迭代成 InputDocument,内含格式识别 | datamodel/document.py:_DocumentConversionInput |
InputFormat / FormatToExtensions | 格式枚举 + 扩展名/MIME 映射表 | datamodel/base_models.py:InputFormat |
FormatOption 及各子类 | "格式→配方"的数据结构 | document_converter.py:FormatOption |
_get_default_option | 格式→默认配方的大分派表 | document_converter.py:_get_default_option |
_get_pipeline | 取/建流水线实例,按哈希缓存 | document_converter.py:_get_pipeline |
_process_document / _execute_pipeline | 校验 allowed_formats 后调 pipeline.execute | document_converter.py:_process_document |
3. 核心原理(逐个机制,由浅入深)
3.1 三个入口,只有一条真路:convert → convert_all → _convert
**它要解决的小问题:**既想支持"转一个",又想支持"转一批"和"转一段字符串",还不想写三套逻辑。
思路:把"转一批"当成唯一的真实路径,另外两个都收敛到它上面。
-
convert(单个)其实是把 source 包成单元素列表,丢给convert_all,再取第一个结果:# document_converter.py:convert(真实源码,节选)all_res = self.convert_all(source=[source], ...)return next(all_res)这段说明
convert没有独立逻辑——它只是convert_all的单文档外壳(document_converter.py:convert)。 -
convert_string(字符串)则更外层:它把字符串按格式补上扩展名、编码成字节、包进DocumentStream,再调convert。只支持 MD / HTML / XML_DOCLANG 三种,其它格式抛ValueError(document_converter.py:convert_string)。 -
convert_all自己也不是引擎,它负责建限制条件 + 包输入 + 处理错误汇报,真正干活的是_convert:# document_converter.py:convert_all(真实源码,节选)conv_input = _DocumentConversionInput(path_or_stream_iterator=source, limits=limits, headers=headers)conv_res_iter = self._convert(conv_input, raises_on_error=raises_on_error)它是个生成器:逐个
yield结果;当raises_on_error=True且某文档状态不是SUCCESS/PARTIAL_SUCCESS时,立刻抛ConversionError(document_converter.py:convert_all)。
一句话:三个入口,一条真路。 所有 source 最终都变成 _DocumentConversionInput,流进 _convert。
3.2 批量分块与可选并发:_convert 的调度
**它要解决的小问题:**一批文档可能成千上万,不能一次全读进内存;有时又想多线程加速。
思路:先用 chunkify 把输入切成固定大小的批(batch),再决定每批串行还是并发处理。
两个开关都在 settings.perf 里(datamodel/settings.py:BatchConcurrencySettings),默认都是 1:
| 设置 | 含义 | 默认 |
|---|---|---|
settings.perf.doc_batch_size | 每批多少个文档(chunk 大小) | 1 |
settings.perf.doc_batch_concurrency | 并行线程数 | 1 |
判定逻辑很直白——只有当批大小和并发数都 > 1 才真正开线程池,否则老老实实串行 map:
# document_converter.py:_convert(真实源码,节选)
if (settings.perf.doc_batch_concurrency > 1
and settings.perf.doc_batch_size > 1):
with ThreadPoolExecutor(max_workers=settings.perf.doc_batch_concurrency) as pool:
for item in pool.map(process_func, input_batch):
yield item
else:
for item in map(process_func, input_batch):
...
yield item
这段是 _convert 的心脏(document_converter.py:_convert):chunkify(conv_input.docs(...), doc_batch_size)
产出每一批,process_func 是绑好 raises_on_error 的 _process_document。
关键细节/坑:并发用的是线程而非进程。源码注释直言不讳——没有 free-threaded Python
(即 GIL 仍在),并发基本无收益,属实验特性(datamodel/settings.py:BatchConcurrencySettings
的 doc_batch_concurrency 注释)。所以默认值是 1,别指望开线程就自动加速 CPU 密集的 PDF 识别。
3.3 第一级分派:认格式(source → InputFormat)
**它要解决的小问题:**用户可能给路径、URL、内存流,文件名甚至没扩展名——怎么可靠地判定这是什么格式?
基础设施是两张表(datamodel/base_models.py):
InputFormat:所有支持格式的枚举(PDF、DOCX、PPTX、HTML、MD、CSV、XLSX、图片、音频、 多种 XML、EPUB…),见base_models.py:InputFormat。FormatToExtensions/FormatToMimeType:格式 → 扩展名列表 / MIME 列表的正向映射 (base_models.py:FormatToExtensions)。由它们反推出MimeTypeToFormat(MIME → 格式)。
识别发生在 _DocumentConversionInput.docs(...) 迭代每个 source 时,调
self._guess_format(obj)(datamodel/document.py:_guess_format)。它是一条多级兜底的探测链,
命中即用下一步:
特殊扩展名(.dclg → XML_DOCLANG)
└─► filetype.guess_mime(魔数嗅探)
└─► _mime_from_extension(按扩展名查 FormatToExtensions)
└─► 读前 1KB/8KB 内容嗅探(zip→Office、gzip→METS、HTML/CSV 探测)
└─► 兜底 "text/plain"
最终:mime ──► MimeTypeToFormat ──► InputFormat(多候选时再按内容 _guess_from_content 消歧)
要点:不是只看扩展名。对无扩展名的 URL、把 .xlsx/.docx/.pptx 都报成 application/zip
的情况,它会进一步拆开 ZIP 看内部结构来区分 Office 家族(document.py:_guess_format 里对
application/zip 的特判)。识别不出就返回 None,该文档后续会被判为无效输入。
docs(...) 认出格式后,顺手从 format_options 里取出该格式的 backend 和
backend_options_for_input(...),组装成一个 InputDocument yield 出去(document.py:docs)。
注意分工:后端(backend)在这一步就绑定到文档上了;但流水线(pipeline)还没取——那要等第三级。