Docling Document(DoclingDocument)深度解析:Docling 统一文档表示的字段结构、层级模型与源码实践
Docling v2 引入的统一文档表示 DoclingDocument,是 Docling 把 PDF、DOCX、HTML、Markdown 等数十种格式收敛到同一数据模型的核心。本文围绕仓库文档 DoclingDocument 概念说明 展开:先讲清它的字段体系与树状层级模型,再基于仓库中真实的转换样例逐行解读序列化结构,最后结合 DocumentConverter 与各 Backend 源码,说明如何获取、构建与复用这种统一表示,读完即可在 RAG、检索、下游处理等场景中对文档结构做精确操作。
1. 什么是 DoclingDocument,它解决了什么问题
DoclingDocument 是一个用 Pydantic 定义的文档数据类型,可表达各类文档的公共特征(引自概念文档):
- 文本(Text)、表格(Tables)、图片(Pictures)等内容项;
- 以章节和分组组织的文档层级(document hierarchy with sections and groups);
- 正文与页眉/页脚等"版面装饰"(furniture)之间的区分;
- 每个内容项的版面信息(bounding boxes,如果可用);
- 溯源信息(provenance information)。
需要注意一个实现边界:Pydantic 类型定义本身位于独立仓库 docling-core 的 docling_core.types.doc 模块中,本仓库通过依赖 docling-core>=2.91.0 引入(见 pyproject.toml 的 dependencies 段),因此 DoclingDocument、TextItem、TableItem 等类不在本仓库目录内,而由 docling_core.types.doc 提供;本仓库的所有 Backend、Pipeline 和 CLI 都围绕这套类型构建。
它的价值在于:上游几十种输入格式(见 DocumentConverter._get_default_option 中的 _get_default_option,覆盖 DOCX、PDF、HTML、Markdown、CSV、EPUB、LaTeX、邮件、专利 XML、音频等)都被转换成同一个结构,下游无论做 Markdown 导出、JSON 持久化、分块检索还是版面分析,都只需面对一种数据模型。
2. 顶层字段:内容项与内容结构两大类
概念文档把 DoclingDocument 的顶层字段分为两类,这是理解整个模型的关键。
2.1 内容项(content items)
这些字段存放"有内容"的节点:
texts:所有具有文本表示的项(段落、章节标题、公式……),基类为TextItem;tables:所有表格,类型TableItem,可携带结构标注;pictures:所有图片,类型PictureItem,可携带结构标注;key_value_items:键值项。
以上字段都是列表,其中存放继承自 DocItem 类型的实例,通过 JSON pointer(如 #/texts/4)互相引用父节点和子节点。
2.2 内容结构(content structure)
这些字段不存放内容本身,只组织内容:
body:主文档正文的树结构根节点;furniture:所有不属于正文的项(页眉、页脚等)的树结构根节点;groups:不代表内容、而作为其他内容项容器的项(例如一个列表、一个章节)。
body/furniture/groups 中只存放 NodeItem 实例,同样通过 JSON pointer 引用子节点与父节点。
阅读顺序的载体:文档的阅读顺序封装在 body 树中,具体是树中每个节点的 children 列表顺序。也就是说,"先读什么、后读什么"不是隐式的,而是显式地写在树结构里——这正是它相对于纯线性文本流的本质区别。
3. 实例剖析:word_sample.docx 的转换结果
概念文档以 tests/data/word_sample.docx 的转换结果为例,左侧展示 YAML 片段、右侧对照 MS Word 原文。本仓库中该样例及其 ground truth 真实存在:
- 源文件:word_sample.docx
- 序列化 JSON:word_sample.docx.json
- Markdown 对照:word_sample.docx.md
以下按 JSON 序列化逐段拆解(字段名与真实文件一致)。
3.1 文档头:schema、版本与 origin(溯源)
{
"schema_name": "DoclingDocument",
"version": "1.10.0",
"name": "word_sample",
"origin": {
"mimetype": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"binary_hash": 5964280909995938039,
"filename": "word_sample.docx"
},
...
}
origin 就是概念文档所说 provenance 在文档层面的体现:格式 MIME 类型、内容哈希、文件名。这使一份 DoclingDocument 序列化后可以自我描述"我从哪里来",对审计和去重都很实用。
3.2 body 与 furniture:两个树根
"body": {
"self_ref": "#/body",
"children": [ { "$ref": "#/texts/0" }, { "$ref": "#/texts/1" } ],
"content_layer": "body",
"name": "_root_",
"label": "unspecified"
},
"furniture": {
"self_ref": "#/furniture",
"children": [],
"content_layer": "furniture",
"name": "_root_",
"label": "unspecified"
}
body 与 furniture 各自是独立的树根(self_ref 指向自身),每个节点带 content_layer 字段标明属于哪一层。这份 DOCX 样例没有页眉页脚,所以 furniture.children 为空——但在 PDF 等版式文档的转换中,页眉页脚项会被挂到这棵树上,从而在导出时可以被选择性地排除。children 按 JSON pointer 引用 texts 列表中的项,顺序即阅读顺序。
3.3 标题树:正文项嵌套在 title 之下
概念文档指出:示例文档第一页的所有项都嵌套在 title 项(#/texts/1)之下。JSON 中可以看到:
"texts": [
{
"self_ref": "#/texts/0",
"parent": { "$ref": "#/body" },
"children": [],
"content_layer": "body",
"label": "text",
"prov": [],
"orig": "Summer activities",
"text": "Summer activities",
"formatting": {
"bold": false, "italic": false, "underline": false,
"strikethrough": false, "script": "baseline"
}
},
{
"self_ref": "#/texts/1",
"parent": { "$ref": "#/body" },
"children": [
{ "$ref": "#/texts/2" },
{ "$ref": "#/pictures/0" },
{ "$ref": "#/texts/3" },
{ "$ref": "#/texts/4" }
],
"content_layer": "body",
"label": "title",
"prov": [],
"orig": "Swimming in the lake",
"text": "Swimming in the lake"
},
...
]
从中可以读出三层信息:
label区分角色:#/texts/0是普通text,#/texts/1是title。title的children里混排了文本项(#/texts/2"Duck")、图片项(#/pictures/0)和图注文本(#/texts/3"Figure 1: This is a cute duckling")——正文、图片和图注的层级归属一目了然。prov与formatting并存:每个文本项都有prov(provenance 列表)和formatting(粗体、斜体、下划线、删除线、上下标基线)。formatting保留了源文档的文字样式,prov则记录项在原始页面中的位置。orig与text双字段:orig是源文档中的原始文本,text是规范化后的文本,便于对照原始内容与处理结果。
关于 prov 的一个值得注意的细节:这份 DOCX 样例的 prov 均为空列表。从仓库代码结构看,DOCX 等结构化格式本身没有版面坐标,溯源信息自然缺位;而在 csv_backend.py 这类 Backend 中,表格单元格会显式传入 prov=[ProvenanceItem(page_no=1)] 等位置信息。这说明 provenance 是"有则填、无则空"的可选字段,不同输入格式下信息密度不同,属于概念文档中"layout information, if available"的准确含义。
3.4 分组(Grouping):groups 如何组织列表
概念文档的第二张图展示:标题 "Let's swim"(#/texts/5)之下的所有项都作为其子节点,其中既包含文本项,也包含装列表元素的分组。JSON 中对应的 groups 字段是:
"groups": [
{
"self_ref": "#/groups/0",
"parent": { "$ref": "#/texts/4" },
"children": [
{ "$ref": "#/texts/6" },
{ "$ref": "#/texts/7" },
{ "$ref": "#/texts/8" }
],
"content_layer": "body",
"name": "list",
"label": "list"
},
{
"self_ref": "#/groups/1",
"parent": { "$ref": "#/texts/4" },
"children": [
{ "$ref": "#/texts/10" },
{ "$ref": "#/texts/11" },
{ "$ref": "#/texts/12" }
],
"content_layer": "body",
"name": "list",
"label": "list"
},
...
]
可以看到 groups/0 的 parent 是 #/texts/4,其三个列表项(#/texts/6~8)被打包成一个 label: "list" 的容器;父节点 #/texts/4(即标题 "Let's swim" 一级的节点)的 children 则同时引用了直接子文本和这个 group。这就是概念文档所说的"children of 'Let's swim' are both text items and groups"的具体形态:
- 列表项不直接挂在标题下,而是先聚合成
list组,再挂在标题下; - 组本身不是内容,它的作用是保持"哪些项属于同一个列表/章节"这一结构语义,供导出与分块使用;
- 每个 group 通过
parent字段指回树中位置,通过children列出成员,双向引用均使用 JSON pointer,任何节点都可以独立定位。
4. 文档如何产生:从 DocumentConverter 到 DoclingDocument
理解字段之后,再看 DoclingDocument 在实际转换链路中的位置。
DocumentConverter 是主入口。其 convert / convert_all 方法返回 ConversionResult,其中 document 属性就是一份 DoclingDocument;转换流程为:按 InputFormat 查表选出 Backend 与 Pipeline(见 _get_default_option),Pipeline 调用对应 Backend 的 convert() 构建出 DoclingDocument。DOCX 这类结构化格式走 SimplePipeline + MsWordDocumentBackend,因此不依赖版面模型即可保留标题、列表等结构,这也是上面 JSON 中 title/list 标签能准确出现的原因。
最小使用示例可参考 minimal.py:
from docling.document_converter import DocumentConverter
source = "https://arxiv.org/pdf/2408.09869"
converter = DocumentConverter()
result = converter.convert(source)
# result.document 即 DoclingDocument
print(result.document.export_to_markdown())
(注意:转换 PDF 需要安装含版面模型的依赖;转换 DOCX/HTML 等结构化格式只需结构化格式对应的 extras。运行前提以 pyproject.toml 中定义的 optional-dependencies 为准。)
5. 文档如何被消费与复用
5.1 JSON 往返:model_validate_json
DoclingDocument 作为 Pydantic 模型,序列化/反序列化是其原生能力。仓库中的 JSON Backend docling_json_backend.py 展示了消费侧的权威实现:读取文件(兼容 UTF-8 BOM)后直接 DoclingDocument.model_validate_json(json_data) 还原对象,convert() 再原样返回该文档。这意味着上面第 3 节展示的 JSON 文件本身就是一种一等输入格式:DocumentConverter 可直接把 JSON_DOCLING 格式转回来(见 document_converter.py 中的 InputFormat.JSON_DOCLING 注册),实现"转换一次、到处复用"的管线模式。
5.2 导出:export 方法与序列化器
DoclingDocument 自带导出方法(如 export_to_markdown()),它们是便捷封装,内部委托给序列化器。各导出格式对表格跨行跨列(rowspan/colspan)的处理差异,完整记录在 序列化概念文档 中:
| 格式 | 跨行跨列处理 |
|---|---|
| JSON | 无损保留,完整序列化 TableData(含全部 span 字段) |
| Doclang | 保留,OTSL 用显式续表标记(colspan 为 LCEL、rowspan 为 UCEL、两者兼有为 XCEL) |
| DocTags | 保留,OTSL 原生编码 span 结构 |
| HTML | 保留,输出原生 rowspan / colspan 属性 |
| Markdown | 扁平化:单元格文本只写在起点位置,其余覆盖格渲染为空 |
| LaTeX | 扁平化:tabular 环境暂不输出 \multirow / \multicolumn |
| WebVTT | 不适用,表格不序列化 |
因此若下游依赖精确表格结构,优先选 export_to_html() 或 export_to_dict() 而不是 export_to_markdown();序列化器体系(BaseDocSerializer / BaseTableSerializer 等)还允许子类化并注入自定义表格序列化器,示例见 serialization.ipynb。
5.3 构建 API:从空文档搭建 DoclingDocument
概念文档还提到,DoclingDocument 附带一套"从零构建"的构造 API。本仓库各 Backend 就是这套 API 的用户,源码中可以确认以下方法签名:
doc.add_text(...):追加文本项;asciidoc_backend.py 中用它写入各级标题,ebcdic_backend.py中用于写入字段描述;doc.add_table(data=..., parent=...):追加表格项并可指定父节点;csv_backend.py 用它把整张 CSV 表格挂入文档树,boxnote_backend.py 的_add_table同理;doc.add_group(...)/doc.add_inline_group(...):创建分组容器;asciidoc_backend.py 用它把列表项聚合成 group,boxnote_backend.py同时展示了嵌套分组(行内组与块级组)的用法。
一个与 add_text / add_group 源码一致的构建示意(对应 AsciiDoc/列表处理中的实际模式):
from docling_core.types.doc import DoclingDocument, DocItemLabel
doc = DoclingDocument(name="my_doc")
title = doc.add_text(label=DocItemLabel.TITLE, text="Let's swim")
group = doc.add_group(label="list", name="list", parent=title)
for item_text in ("backstroke", "breaststroke", "butterfly"):
doc.add_text(label=DocItemLabel.ITEM, text=item_text, parent=group)
以上参数名与调用方式与 asciidoc_backend.py、boxnote_backend.py 中的真实调用保持一致(label、text、parent 为通用参数);完整的构造 API 清单以 docling-core 的类型定义为准。
6. 小结:一份表示,三种用法
对照概念文档的要点,DoclingDocument 的能力在本仓库中都有对应的代码落点,可以归纳为三种用法:
- 作为转换目标:
DocumentConverter.convert()返回ConversionResult.document,数十种输入格式汇入同一模型; - 作为持久化与交换格式:
model_dump_json/model_validate_json无损往返,JSON Backend 让已转换结果可以重新进入转换管线; - 作为可编程的数据结构:
body/furniture/groups树 + JSON pointer 引用,使"标题下有哪些列表""哪些项属于页眉"这类问题变成可直接遍历的结构化查询,而非文本正则;构造 API 则允许在自定义 Backend 中从零搭建同构文档。
进一步阅读建议:word_sample.docx.json(真实序列化全文)、serialization.md(导出格式与 span 行为)、chunking.md(基于该表示的 RAG 分块)。
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

