跳到主要内容

入口与三级分派:从 source 到流水线

30 秒导读: 你调 converter.convert("a.pdf"),Docling 怎么知道该用哪个后端、哪条流水线? 答案全在 DocumentConverter 这一层。它把一次转换拆成三级分派:先认出文件是什么格式, 再查表选出该格式的处理配方(FormatOption),最后把配方里指定的流水线实例取出来(能复用就复用), 交给它执行。本章只讲这套"识别 + 选型 + 调度",不进后端和流水线内部(那是 0203)。


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.executedocument_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:BatchConcurrencySettingsdoc_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 里取出该格式的 backendbackend_options_for_input(...),组装成一个 InputDocument yield 出去(document.py:docs)。 注意分工:后端(backend)在这一步就绑定到文档上了;但流水线(pipeline)还没取——那要等第三级。

3.4 第二级分派:选配方(InputFormatFormatOption)

**它要解决的小问题:**知道是 PDF 了,那用哪个后端读字节、走哪条流水线、带什么参数?

答案是 FormatOption 这个数据结构——一份格式的"处理配方",四个字段:

字段是什么例子
pipeline_cls用哪条流水线类StandardPdfPipeline / SimplePipeline / AsrPipeline
backend用哪个后端把字节变成可处理形态DoclingParseDocumentBackend / MsWordDocumentBackend
backend_options后端专属参数(可选)PdfBackendOptions / HTMLBackendOptions
pipeline_options流水线专属参数PdfPipelineOptions

FormatOption 继承自 BaseFormatOption(它持有 backend + pipeline_options),子类再补上 pipeline_cls(document_converter.py:FormatOptionbase_models.py:BaseFormatOption)。有个小巧的 默认值兜底:若没给 pipeline_options,set_optional_field_default 会调 pipeline_cls.get_default_options() 自动填上(document_converter.py:FormatOption.set_optional_field_default)。

每种格式都有一个预置子类,把 pipeline_clsbackend 钉死。看几个代表:

FormatOption 子类pipeline_clsbackend
PdfFormatOptionStandardPdfPipelineDoclingParseDocumentBackend
ImageFormatOptionStandardPdfPipelineImageDocumentBackend
WordFormatOptionSimplePipelineMsWordDocumentBackend
HTMLFormatOptionSimplePipelineHTMLDocumentBackend
AudioFormatOptionAsrPipelineNoOpBackend

看出规律了吗:图片复用 PDF 的流水线(都走 StandardPdfPipeline),而大多数"结构化文本"格式 (Word/HTML/CSV/MD/EPUB…)都走轻量的 SimplePipeline——这条分界正是 03 要讲的 "声明式直出 vs 多线程分阶段"。

这些配方汇总在一张大分派表 _get_default_option(format) 里 (document_converter.py:_get_default_option):一个 dict[InputFormat, FormatOption],查不到就抛 RuntimeError。用户在构造 DocumentConverter(format_options={...}) 时可以覆盖某格式的配方, 否则就用这张表的默认值(document_converter.py:__init__format_to_options 的构建)。

**一个兼容性小补丁:**构造时若发现有人给 InputFormat.IMAGE 配了非 ImageDocumentBackend 的后端,会发 DeprecationWarning 并自动纠正成 ImageFormatOption(document_converter.py:__init__)。

3.5 第三级分派:取流水线实例(FormatOption → 缓存的 pipeline)

它要解决的小问题:流水线(尤其 PDF 那条)初始化要加载模型、吃内存,不能每个文档都新建一个。

**思路:**用一个缓存字典 initialized_pipelines,(pipeline_cls, options 哈希) 复用实例

关键在 _get_pipeline(document_converter.py:_get_pipeline):

# document_converter.py:_get_pipeline(真实源码,节选)
options_hash = self._get_pipeline_options_hash(pipeline_options)
cache_key = (pipeline_class, options_hash)
with _PIPELINE_CACHE_LOCK:
if cache_key not in self.initialized_pipelines:
self.initialized_pipelines[cache_key] = pipeline_class(
pipeline_options=pipeline_options
)
return self.initialized_pipelines[cache_key]
  • 缓存键(pipeline 类, 选项哈希)。哈希由 _get_pipeline_options_hash 算:把 pipeline_options.model_dump() 转字符串后取 md5(document_converter.py:_get_pipeline_options_hash)。 这意味着同类流水线 + 同参数才复用;换了参数就是另一个实例。
  • 线程安全:整个查/建过程包在模块级锁 _PIPELINE_CACHE_LOCK 里 (document_converter.py 顶部),配合 3.2 的多线程批处理,避免并发下重复初始化同一条流水线。

想提前把某格式的流水线"热身"好?调 initialize_pipeline(format),它内部就是调一次 _get_pipeline (document_converter.py:initialize_pipeline)。

3.6 准入校验与交棒:_process_document / _execute_pipeline

**它要解决的小问题:**用户可能限制了 allowed_formats,不该处理的格式要拦下;无效文档要出错但别崩整批。

分两道关(都在 document_converter.py):

  1. _process_document:检查 in_doc.format in self.allowed_formats不在白名单就直接产出一个 status=SKIPPEDConversionResult,并附一条 FailureCategory.POLICYErrorItem, 根本不碰流水线(document_converter.py:_process_document)。

  2. _execute_pipeline:只有 in_doc.valid 才取流水线并执行:

    # document_converter.py:_execute_pipeline(真实源码,节选)
    if in_doc.valid:
    pipeline = self._get_pipeline(in_doc.format)
    if pipeline is not None:
    conv_res = pipeline.execute(in_doc, raises_on_error=raises_on_error)

    这一行 pipeline.execute(...) 就是 DocumentConverter交棒点——从此进入流水线的世界。 取不到流水线 → 视 raises_on_error 抛错或产出 FAILURE;文档无效 → 产出带 build_invalid_input_errors(...)FAILURE(document_converter.py:_execute_pipeline)。

准入分诊表:

情况结果状态谁产出
格式不在 allowed_formatsSKIPPED(POLICY)_process_document
文档无效(如识别失败/超限)FAILURE_execute_pipeline
无可用流水线抛错 或 FAILURE_execute_pipeline
一切正常pipeline.execute 决定流水线

4. 另一个并行入口:DocumentExtractor(一句带过)

除了"转换"入口,Docling 还有个抽取入口 DocumentExtractor (document_extractor.py:DocumentExtractor)。它的骨架和 DocumentConverter 几乎一模一样—— 同样是 extract → extract_all → _extract 三层、同样复用 _DocumentConversionInputInputDocument、 同样按 settings.perf 做分块与可选并发、同样按 (类, 选项哈希) 缓存流水线——区别只在于它走的是 抽取流水线、按用户给的 template 抽结构化字段、产出 ExtractionResult 而非 ConversionResult。 可以把它理解成"同一套分派调度骨架的第二份实例",细节不在本章展开。


5. 巧妙之处(可借鉴的技术)

  • 入口收敛成单路径。 convert/convert_string 都最终落到 convert_all → _convert,逻辑只写一份, 单文档只是"批大小为 1"的特例(document_converter.py:convert)。
  • 三级分派各自独立、可替换。 格式识别、配方选型、流水线取用是三张解耦的表/函数,用户只需覆盖 format_options 就能换后端或调参,不用碰引擎(document_converter.py:_get_default_option)。
  • 按"类 + 参数哈希"缓存实例。 既复用了昂贵的模型加载,又能让不同参数的同类流水线并存,键设计得很省 (document_converter.py:_get_pipeline_options_hash)。
  • 准入前置、错误不炸批。 白名单拦截产出 SKIPPED、无效输入产出 FAILURE,而不是抛异常中断整批 (document_converter.py:_process_document)。

6. 边界与局限

  • 并发多为摆设。 线程池受 GIL 限制,标准 Python 下 doc_batch_concurrency 基本无加速,官方注明为 实验特性(datamodel/settings.py:BatchConcurrencySettings)。
  • convert_string 只认三种格式。 仅 MD / HTML / XML_DOCLANG,其它抛 ValueError (document_converter.py:convert_string)。
  • 格式识别可能猜错或猜不出。 无扩展名、非常规 MIME 时靠内容嗅探兜底,失败则返回 None 并按无效输入处理 (datamodel/document.py:_guess_format)。
  • 缓存无淘汰。 initialized_pipelines 只增不减,长生命周期进程里多种参数组合会持续占用内存(inferred, 基于 _get_pipeline 只写不删)。

7. 横向对比(同组其它章)

  • 想知道 backend 拿到文档后如何把原始字节变成可处理形态?看 02-backends
  • 想知道 SimplePipeline(声明式直出)与 StandardPdfPipeline(多线程分阶段)的差别?看 03-pipelines
  • 想深入 PDF 那条流水线逐阶段的模型?看 04-pdf-recognition-stages
  • 想了解最终的 DoclingDocument 数据模型与面向 RAG 的输出?看 05-datamodel-output-rag
  • 项目总览见 index

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

主题文件路径符号名
转换入口(单个)docling/document_converter.pyDocumentConverter.convert
转换入口(批量)docling/document_converter.pyDocumentConverter.convert_all
转换入口(字符串)docling/document_converter.pyDocumentConverter.convert_string
批量引擎:分块 + 并发docling/document_converter.pyDocumentConverter._convert
准入校验docling/document_converter.pyDocumentConverter._process_document
取流水线并执行docling/document_converter.pyDocumentConverter._execute_pipeline
流水线缓存docling/document_converter.pyDocumentConverter._get_pipeline
选项哈希(缓存键)docling/document_converter.pyDocumentConverter._get_pipeline_options_hash
缓存锁docling/document_converter.py_PIPELINE_CACHE_LOCK
格式→默认配方表docling/document_converter.py_get_default_option
配方数据结构docling/document_converter.pyFormatOption / PdfFormatOption / WordFormatOption / HTMLFormatOption
配方基类docling/datamodel/base_models.pyBaseFormatOption
格式枚举docling/datamodel/base_models.pyInputFormat
扩展名/MIME 映射docling/datamodel/base_models.pyFormatToExtensions / FormatToMimeType / MimeTypeToFormat
source→InputDocument + 格式识别docling/datamodel/document.py_DocumentConversionInput.docs / _guess_format
批处理开关docling/datamodel/settings.pyBatchConcurrencySettings(doc_batch_size / doc_batch_concurrency)
分块工具docling/utils/utils.pychunkify
并行抽取入口docling/document_extractor.pyDocumentExtractor