首页
/ Test Table Slide

Test Table Slide

2026-09-06 18:56:55作者:彭桢灵Jeremy
Class1 Class1 Class1 Class2 Class2 Class2
A merged with B A merged with B C A B C
R1 True False False True True
R2 True False
R3 False False
R3 True True
R4 False False
R4 True True False False
R4 True False True False True False

With footnote


从这份输出中至少能提炼出 docling 表格导出的四个行为特征:

- **标题独立成标题行**:幻灯片标题 `Test Table Slide` 被识别为标题文本,导出为 `#` 标题,而不是表格内文本;
- **合并单元格按“复制填满”策略输出**:观察第二行 `| A merged with B | A merged with B | C | ...`,这是一个横向合并两个单元格的标题单元。为保持 Markdown 表格的矩形栅格结构,合并后的文本在跨越的每一列上都被重复写出;竖向合并同样如此——`R3` 连续出现两行、`R4` 连续出现三行,正是行方向合并单元格在扁平的 Markdown 网格中被重复填充的结果;
- **空单元格保留占位**:表格中大量 `|  |` 空段是合并结构中被覆盖的栅格位,被如实保留为空单元格;
- **表格与文本分离**:表下方的 `With footnote` 是独立段落,说明该文本不属于表格体,而是独立于表格、紧随其后的正文元素。

### 2. 第二张幻灯片:图形文本与列表

紧接着是第二张幻灯片的内容:

```markdown
# Second slide title

A rectangle shape with this text inside.

Let’s introduce a list

- With foo
- Bar
- And baz things

这里体现了 docling 对幻灯片“形状内文本”的抽取:A rectangle shape with this text inside. 原本是绘制在矩形形状(shape)文本框里的文字,转换后被还原为普通段落;随后 Let’s introduce a list 是引导语段落,紧跟的是无序列表演示。

3. 列表与段落如何在连续 Markdown 中并存

基准文件的剩余部分展示了一个更常见也更“麻烦”的幻灯片排版场景——同一页面上多组不同样式的列表、带引导文案的列表交错出现:

1. List item4
2. List item5
3. List item6

- I1
- I2
- I3
- I4

Some info:

- Item A
- Item B

Maybe a list?

1. List1
2. List2
3. List3

- l1
- l2
- l3

这些内容在源幻灯片中分属不同文本框与不同段落组。注意两点输出细节:

  • 编号列表与符号列表被区分:凡是源文档使用了自动编号(buAutoNum)的段落组,导出为有序列表;使用字符符号(buChar)或无符号列表标记的段落组,导出为无序列表。即便多个列表紧挨着排布,docling 也按各自独立的 list group 处理,不会合并成一个列表;
  • Markdown 输出中不显式标注幻灯片分界:与结构化文档(JSON/itxt,见下节)不同,纯文本 Markdown 导出是“顺排”的——从基准输出中只能通过标题与内容变化推测幻灯片的切换,没有类似 --- 的分页符。

二、对照“结构化视图”:JSON 与 itxt 揭示的真实层级

仅看 Markdown 会漏掉一个重要信息——这些内容到底如何被组织成文档树。同目录下的两份基准给出了答案。

legacy_sample.ppt.jsonDoclingDocument 的完整序列化结果,其 schema_nameDoclingDocument。从 JSON 的 bodygroups 节点可以看出,该 .ppt 被组织为 3 个 chapter 分组的幻灯片slide-0(含标题文本、表格、脚注段落)、slide-1、以及第三个含多个列表的 group。每个文本、表格都被挂在对应 slide-N 分组之下,表格在第 0 张幻灯片,多组列表落在后续幻灯片——这解释了为什么 Markdown 中表格与列表会处于不同位置。

legacy_sample.ppt.itxt 则用缩进文本直观展示了树形层级:

item-0 at level 0: unspecified: group _root_
  item-1 at level 1: chapter: group slide-0
    item-2 at level 2: title: Test Table Slide
    item-3 at level 2: table with [9x7]
    item-4 at level 2: paragraph: With footnote
  item-5 at level 1: chapter: group slide-1
    item-6 at level 2: title: Second slide title
    ...
    item-9 at level 2: list: group list
      item-10 at level 3: list_item: With foo

从这里可以确认几个与 Markdown 输出严格对应的关键事实:

  • 表格规模为 9 行 × 7 列table with [9x7]),与 Markdown 中表头 2 行 + 数据 7 行的 9 行一致;
  • 幻灯片标题在文档树中的 label 是 title,列表被识别为 list: group list,其下每个条目是 list_item;段落 label 为 paragraph
  • 每一张幻灯片是一个 chapter 分组,演示文件被表示为“章—节”式的文档层级,这与 docling 对演示文稿的通用建模方式一致(itxt 基准文件JSON 基准文件)。

三、为什么 .ppt 能转:后端先做“旧到新”的格式桥接

要理解这份基准输出的产生过程,需要看 docling 的 PowerPoint 后端实现 docling/backend/mspowerpoint_backend.py。该后端的类文档字符串明确写道:

Legacy .ppt files (binary PowerPoint 97-2003 format) are first converted to .pptx via LibreOffice before parsing.

在构造函数的初始化逻辑里可以看到这一桥接的直接代码(docling/backend/mspowerpoint_backend.py#L148-L149):

if in_doc.format == InputFormat.PPT:
    path_or_stream = convert_to_modern_format(path_or_stream, "ppt", "pptx")

也就是说:docling 自身并不直接解析二进制 .ppt,而是先调用 LibreOffice 将其无头转换为 .pptx,再交给 python-pptx(Presentation)加载解析。真正的 LibreOffice 调用封装在 docling/backend/docx/drawingml/utils.pyconvert_to_modern_format(source, source_suffix, target_suffix, timeout_s=120) 函数中,其实现要点包括:

  • 同时接受文件路径与内存字节流(BytesIO);传入流时,会先按源扩展名命名写入临时文件,以便 LibreOffice 探测格式;
  • --headless --convert-to <target_suffix> --outdir <tmp> 调用 LibreOffice 子进程;
  • 为规避多进程/并发转换时 LibreOffice 对用户配置目录的独占锁冲突,转换会使用 -env:UserInstallation=<uri> 指向一个一次性临时 profile(见 _isolated_libreoffice_profile);
  • 转换产物以 BytesIO 返回,供上层直接解析。

该后端支持的格式集合是 {InputFormat.PPTX, InputFormat.PPT}supported_formats(),见 docling/backend/mspowerpoint_backend.py#L201-L202),转换后通过 _walk_linear 将每一张幻灯片上的标题、段落、表格、列表、图片等形状抽取为 DoclingDocument 结构。因此,你在 legacy_sample.ppt.md 中看到的表格与列表质量,本质上取决于“LibreOffice 重排出的 .pptx 能否保真还原原版版式”。

四、这份基准是如何被验证的:端到端回归测试

legacy_sample.ppt.md 并非孤立的展示样例,它是自动化回归测试的“答案”。打开端到端测试 tests/test_backend_legacy_msoffice.py,可以看到 .doc.xls.ppt 三种旧版二进制 Office 格式共享同一套验证逻辑:

_CASES: list[tuple[InputFormat, str]] = [
    (InputFormat.DOC, "tests/data/doc/sources"),
    (InputFormat.XLS, "tests/data/xls/sources"),
    (InputFormat.PPT, "tests/data/ppt/sources"),
]

该测试的关键行为包括:

  1. 整个测试文件在 LibreOffice 不可用时被跳过pytest.mark.skipif(get_libreoffice_cmd() is None, ...)),印证了格式桥接对 LibreOffice 的硬依赖;
  2. 使用 DocumentConverter(allowed_formats=[fmt]) 转换 sources 目录下的每一个样本,然后与 groundtruth 目录下同名文件比较三类输出:
    • doc.export_to_markdown(compact_tables=True).md 基准比对;
    • doc._export_to_indented_text(max_text_len=70, explicit_tables=False).itxt 基准比对;
    • verify_document(doc, ...).json 基准做带模糊容差的比对;
  3. 由于 LibreOffice 在不同平台上渲染出的图片尺寸略有差异,bbox 类比对使用了模糊容差(fuzzy tolerance);
  4. 测试文件头部引用了 test_data_gen_flagGEN_TEST_DATA——当该标志开启时,比对会进入“重新生成基准”模式,允许维护者用当前版本重新固化输出。

这意味着任何 PR 只要改变了 .ppt 的转换逻辑,就必须让上述三个基准文件保持一致,否则测试失败。所以 legacy_sample.ppt.md 的稳定,直接守护了旧版 PowerPoint 转换功能不被回归破坏。

五、动手复现:如何亲自把 .ppt 导出成这份 Markdown

你可以用 docling 在本地重现与基准完全一致的输出。环境前提是安装 LibreOffice 且保证 libreoffice/soffice 在 PATH 中,否则后端会抛出 “LibreOffice is required to convert a .ppt file to .pptx” 的 RuntimeError。

from pathlib import Path

from docling.document_converter import DocumentConverter
from docling.datamodel.base_models import InputFormat

# 只允许 PPT 输入,强制走 LibreOffice 桥接链路
converter = DocumentConverter(allowed_formats=[InputFormat.PPT])

result = converter.convert(Path("legacy_sample.ppt"))
doc = result.document

# 与 tests/data/ppt/groundtruth/legacy_sample.ppt.md 的生成方式一致
print(doc.export_to_markdown(compact_tables=True))
登录后查看全文
热门项目推荐
相关项目推荐