Docling 序列化体系详解:从 DoclingDocument 多格式导出到自定义 Serializer
Docling(Get your documents ready for gen AI)在完成 PDF、DOCX 等文档解析后,会产出统一的结构化中间表示 DoclingDocument。而如何把这份中间表示落回 Markdown、HTML、DocTags、JSON 等文本形态,则由一套独立的序列化(serialization)抽象承担。本文基于仓库文档 docs/concepts/serialization.md 及其配套示例 docs/examples/serialization.ipynb,系统讲解 Docling 序列化器的抽象层次、DoclingDocument 导出方法、各格式对表格跨单元格(span)的处理差异,以及如何配置和编写自定义 serializer,帮助你在 RAG、批量转换、文档入库等场景中精确控制导出结果。
1. 序列化抽象:从文档级到组件级
Docling 把「把文档变成文本」这件事拆成了三层抽象:
- 文档序列化器(document serializer):以一个
DoclingDocument实例初始化,负责产出整篇文档的文本表示(textual representation)。这是最常用的入口; - 组件序列化器(component serializers):面向文档的子构件,例如 text serializer、table serializer、picture serializer、list serializer、inline serializer 等。文档级序列化器内部会按组件类型委派给对应的组件序列化器;
- 序列化器提供者(serializer provider):进一步把「序列化策略」与「文档实例」解耦的包装层,便于下游应用统一替换序列化行为。
这一分层直接决定了后文的两个能力:你可以只替换某一种组件(比如把 Markdown 输出的表格换成 triplet 形式),也可以整体替换文档级序列化器,而不必触碰转换管线本身。
2. 基类体系与 serialize() 契约
为了兼顾下游应用的灵活性与开箱即用的便利,Docling 定义了一组序列化类层次(实现位于 docling-core 依赖包中,本仓库 pyproject.toml 声明其版本约束为 docling-core>=2.91.0,<3.0.0):
- 各抽象的基类:
BaseDocSerializer、BaseTextSerializer、BaseTableSerializer等组件基类,以及BaseSerializerProvider; - 上述基类之外的具体实现子类,例如
MarkdownDocSerializer、HTMLDocSerializer。
从客户端视角看,最核心的契约是 BaseDocSerializer.serialize():它返回该文档的文本表示,同时附带哪些文档组件实际参与(贡献)了这次序列化的元数据。这一点在做导出审计或调试「某段文本为什么没出现」时非常有用——你可以通过序列化结果中的 span 来源信息定位到具体组件。
3. DoclingDocument 的导出方法:序列化器的用户级快捷方式
Docling 预置了 Markdown、HTML、DocTags 等序列化器,并在 DoclingDocument 上以导出方法的形式直接暴露。文档说明得很明确:像 export_to_markdown() 这样的导出方法本质上是用户快捷方式(user shorthands),其内部就是直接实例化并委派给相应的 serializer。
在当前仓库中,可以观察到以下导出方法的真实使用分布(对 docling/ 与 tests/ 下 Python 代码的统计):
| 导出方法 | 使用频次(仓库内) | 典型场景 |
|---|---|---|
export_to_markdown() |
138 次 | 绝大多数示例、CLI、测试的默认输出 |
export_to_indented_text() |
16 次 | 纯文本/缩进结构输出(.itxt 地面真值校验) |
export_to_dict() |
9 次 | 稳定 schema 的结构化导出 |
export_to_html() |
4 次 | 保留表格 rowspan/colspan 等结构 |
export_to_doclang() / export_to_doctags() |
各 2 / 1 次 | Doclang 文本、DocTags 训练格式 |
例如 docs/examples/batch_convert.py 展示了批量转换时按目标格式分发导出:YAML 落地 export_to_dict() 结果、.doctags 文件用 export_to_doctags()、Markdown 则同时演示了 export_to_markdown() 与 export_to_markdown(strict_text=True) 两种形态;docs/examples/custom_convert.py 也以同样方式把 JSON、Markdown、DocTags 三种导出写入独立文件。而 docling/datamodel/document.py 在需要「保证稳定 schema」的场景下显式使用 export_to_dict() 做文档序列化,注释写明了其动机——这正是选择 dict 导出而非文本导出的一个代表性理由。
DoclingDocument 本体(及其全部 export_to_* 方法)定义在 docling-core 包中,本仓库通过 docling/datamodel/document.py 重新导出 DoclingDocument 等类型,方便用户统一从 docling.datamodel.document 引用。
4. 格式特定行为:表格单元格跨行跨列(span)如何处理
这是文档中最重要的「格式权衡」章节。Docling 的内部表格模型(TableData.grid)为每个单元格保留了完整的 span 元数据:row_span、col_span、start_row_offset_idx、start_col_offset_idx。不同输出格式对这些元数据的渲染策略如下(完整继承自原文档):
| 格式 | Span 处理 |
|---|---|
| JSON | 保留。完整的 TableData 模型无损序列化,包含全部 span 字段。 |
| Doclang | 保留。表格通过 OTSL 序列化,使用显式续格 token:跨列用 LCEL、跨行用 UCEL、两者兼有则 XCEL。 |
| DocTags | 保留。表格经 OTSL 序列化,OTSL 天然编码 span 结构。 |
| HTML | 保留。单元格直接输出原生 rowspan / colspan 属性。 |
| Markdown | 拍平(Flattened)。Markdown 表格没有 span 语法,序列化器只在原点位置写入单元格文本,span 覆盖的其他网格位置渲染为空单元格。 |
| LaTeX | 拍平。tabular 环境暂不输出 \multirow / \multicolumn 命令。 |
| WebVTT | 不适用。WebVTT 是字幕/说明格式,表格不会被序列化。 |
实践结论:如果下游流程依赖准确的表格结构(例如合并的表头单元格),应优先使用 export_to_html() 或 export_to_dict(),而不是 export_to_markdown()。当然,如果你确实需要 Markdown 中承载跨格信息,也可以子类化 BaseTableSerializer 实现自定义逻辑,并在实例化文档序列化器时传入(见第 7 节的配置示例,table_serializer 参数正是为这种替换设计的)。
5. 实战一:直接应用预置 Serializer
以下代码整理自 docs/examples/serialization.ipynb,演示「转换 + 序列化」的完整闭环。先转换得到 DoclingDocument,再对其施加任意 BaseDocSerializer:
from docling.document_converter import DocumentConverter
DOC_SOURCE = "https://arxiv.org/pdf/2311.18481"
converter = DocumentConverter()
doc = converter.convert(source=DOC_SOURCE).document
HTML 序列化——表格会以 <table> 结构输出(span 完整保留),图片以 <figure>/<figcaption> 呈现:
from docling_core.transforms.serializer.html import HTMLDocSerializer
serializer = HTMLDocSerializer(doc=doc)
ser_result = serializer.serialize()
ser_text = ser_result.text # 序列化结果同时携带组件贡献元数据
Markdown 序列化——表格转为标准管道表格(span 被拍平),图片按 MarkdownParams 配置输出占位符(默认 <!-- image -->)或引用/嵌入形式:
from docling_core.transforms.serializer.markdown import MarkdownDocSerializer
serializer = MarkdownDocSerializer(doc=doc)
ser_text = serializer.serialize().text
6. 实战二:配置 Serializer——替换表格序列化器与参数
同一 Notebook 演示了如何「重配置」Markdown 序列化,满足两类诉求:
- 使用不同的组件序列化器:例如把表格输出改为 triplet 形式(
行1, 列A = 值1. 行1, 列B = 值2. ...),笔记指出这种扁平键值对形式有助于向量检索场景下的表格表示; - 使用用户自定义参数:例如替换默认的图片占位符文本。
from docling_core.transforms.chunker.hierarchical_chunker import TripletTableSerializer
from docling_core.transforms.serializer.markdown import MarkdownParams
serializer = MarkdownDocSerializer(
doc=doc,
table_serializer=TripletTableSerializer(), # 替换组件级序列化器
params=MarkdownParams(
image_placeholder="<!-- demo picture placeholder -->",
# ... 其余 MarkdownParams 参数按需提供
),
)
ser_text = serializer.serialize().text
从 Notebook 的实际输出对比可以验证两处差异:同一张 IBM/Starbucks ESG 表格,默认配置下是 | Report | Question | Answer | 管道表格;换成 TripletTableSerializer 后变为 IBM 2022, Question = ...?. IBM 2022, Answer = ... 的连续文本串;同时图片占位符也从默认的 <!-- image --> 变成了配置中的 <!-- demo picture placeholder -->。这直接印证了第 1 节所说的组件级替换机制:只动一个子序列化器,文档其余部分的输出完全不受影响。
7. 实战三:编写自定义 Serializer
当现有实现都不满足时,你可以定义自定义序列化逻辑。Notebook 给出的例子是:让图片序列化额外带上 picture description(图像描述注解)。前提是转换管线开启了 picture description enrichment(通过 PdfPipelineOptions(do_picture_description=True, picture_description_options=PictureDescriptionVlmOptions(...), generate_picture_images=True, images_scale=2),并配合 PictureDescriptionVlmOptions 指定 VLM 模型与 prompt)。
自定义组件序列化器——继承 MarkdownPictureSerializer 并重写 serialize(),先调用父类拿到基础输出,再追加注解:
from docling_core.transforms.serializer.base import BaseDocSerializer, SerializationResult
from docling_core.transforms.serializer.common import create_ser_result
from docling_core.transforms.serializer.markdown import MarkdownPictureSerializer
from docling_core.types.doc.document import DoclingDocument, PictureItem
class AnnotationPictureSerializer(MarkdownPictureSerializer):
def serialize(self, *, item, doc_serializer, doc, separator=None, **kwargs):
text_parts = []
# 复用父类结果(占位符/图片引用部分)
parent_res = super().serialize(item=item, doc_serializer=doc_serializer, doc=doc, **kwargs)
text_parts.append(parent_res.text)
# 追加 picture description 注解
if item.meta is not None and item.meta.description is not None:
text_parts.append(f"<!-- Picture description: {item.meta.description.text} -->")
text_res = (separator or "\n").join(text_parts)
return create_ser_result(text=text_res, span_source=item)
注意 create_ser_result(text=..., span_source=item):它把序列出的文本与源组件(item)绑定,形成 serialize() 契约中「哪些组件贡献了输出」的元数据。然后把自定义图片序列化器挂到文档级序列化器上:
serializer = MarkdownDocSerializer(
doc=doc,
picture_serializer=AnnotationPictureSerializer(),
params=MarkdownParams(image_mode=ImageRefMode.PLACEHOLDER, image_placeholder=""),
)
ser_text = serializer.serialize().text
运行后,Markdown 输出中每个图片占位处都会带上一条 <!-- Picture description: ... --> 注释,内容即管线阶段由 VLM 生成的图片描述。
Notebook 还覆盖了另一个常见诉求——为每张图片生成唯一标识以便下游与原始 DoclingDocument 匹配:自定义 _serialize_image_part(),从 item.self_ref 解析出图片索引,并对 image_placeholder 中的 {index} 占位 token 做替换,配合 params=MarkdownParams(image_mode=ImageRefMode.PLACEHOLDER, image_placeholder="<!-- image_{index} -->"),即可让每张图在导出文本中拥有形如 <!-- image_2 --> 的独立编号。
8. 小结与延伸阅读
- 序列化是 Docling 转换管线之后的独立层:
DoclingDocument的export_to_*方法只是预置 serializer 的快捷封装,需要精细控制时直接实例化MarkdownDocSerializer/HTMLDocSerializer等并传入组件级序列化器与Params; - 选择导出格式的核心判据之一是表格 span 保真度:JSON/Doclang/DocTags/HTML 保留完整结构,Markdown/LaTeX 当前会拍平;
- 组件级 serializer 可任意替换(table、picture、list……),文档级策略可整体替换,扩展点是子类化而非修改管线。
进一步阅读:docs/concepts/docling_document.md(DoclingDocument 数据结构)、docs/examples/serialization.ipynb(完整可运行示例,含上述全部代码)、docs/examples/batch_convert.py 与 docs/examples/custom_convert.py(批量/自定义转换中的多格式导出落地)、docs/examples/export_figures.py 与 docs/examples/export_tables.py(图片与表格的独立导出)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00