Docling 将 XLSM 宏工作簿解析为结构化 Markdown 表格:xlsx_02_sample_sales_data groundtruth 深度解析
【免费下载链接】doclingGet your documents ready for gen AI项目地址: https://gitcode.com/GitHub_Trending/do/docling
导读
本文以仓库测试数据中的黄金基准文件tests/data/xlsx/groundtruth/xlsx_02_sample_sales_data.xlsm.md为主线,拆解 Docling(面向 gen AI 的文档解析项目)如何把一个带宏的.xlsm工作簿转换为可进入大模型/RAG 管线的结构化结果:工作表被还原成带语义标注的表格,日期、数值、表头等单元格语义被逐项保留,并以 Markdown 表格形式稳定输出。读完本文,你将掌握 Excel/XLSM 文件在 Docling 中的完整处理链路、groundtruth 三重产物的验证机制,以及如何在本地复现与定制这一转换流程。
这份 groundtruth 是什么:XLSM 宏工作簿的标准转换答案
在 Docling 仓库中,tests/data/xlsx/目录下存放着一组 Excel 端到端测试样本,每个样本同时保留"输入文件"与"期望输出",形成可回归验证的黄金标准:
- 输入:sources/xlsx_02_sample_sales_data.xlsm,一个宏启用(macro-enabled)的 Excel 工作簿;
- 期望输出(三份同源基准产物):
- groundtruth/xlsx_02_sample_sales_data.xlsm.md —— Markdown 文本导出;
- groundtruth/xlsx_02_sample_sales_data.xlsm.itxt —— 缩进式文档结构树;
- groundtruth/xlsx_02_sample_sales_data.xlsm.json —— DoclingDocument 全量 JSON 序列化。
也就是说,这篇文章解析的.md并非普通文档,而是 Docling 官方回归测试为这个销售数据工作簿锁定的标准 Markdown 答案。它由测试 test_backend_msexcel.py 在端到端转换后通过verify_export与转换结果逐字节比对,任何后端行为变动都会在此暴露。
值得注意的另一点:.xlsm并不需要单独注册格式。在 base_models.py 中,InputFormat.XLSX的扩展名列表就是["xlsx", "xlsm"],因此宏工作簿天然归入 XLSX 处理通道;从 JSON 基准文件 的origin.mimetype也能看到它最终被识别为application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。
逐行解读.md基准:一张销售表的语义还原
.md基准文件的核心内容如下(为便于阅读,原样呈现其全部 20 行数据):
## SalesData | Product | Date | Quantity | Revenue | | - | - | - | - | | Widget A | 2024-01-01 00:00:00 | 5 | 5000 | | Widget B | 2024-01-02 00:00:00 | 10 | 12000 | | Widget C | 2024-01-03 00:00:00 | 3 | 3000 | | Widget D | 2024-01-04 00:00:00 | 8 | 8000 | | Widget A | 2024-01-05 00:00:00 | 7 | 7000 | | Widget B | 2024-01-06 00:00:00 | 6 | 6000 | | Widget C | 2024-01-07 00:00:00 | 12 | 15000 | | Widget D | 2024-01-08 00:00:00 | 9 | 9000 | | Widget A | 2024-01-09 00:00:00 | 4 | 4000 | | Widget B | 2024-01-10 00:00:00 | 11 | 11000 | | Widget C | 2024-01-11 00:00:00 | 5 | 5000 | | Widget D | 2024-01-12 00:00:00 | 8 | 8500 | | Widget A | 2024-01-13 00:00:00 | 6 | 6200 | | Widget B | 2024-01-14 00:00:00 | 7 | 7100 | | Widget C | 2024-01-15 00:00:00 | 10 | 10500 | | Widget D | 2024-01-16 00:00:00 | 3 | 3200 | | Widget A | 2024-01-17 00:00:00 | 9 | 9400 | | Widget B | 2024-01-18 00:00:00 | 12 | 12500 | | Widget C | 2024-01-19 00:00:00 | 6 | 6100 | | Widget D | 2024-01-20 00:00:00 | 8 | 8900 |这份输出虽然简短,却在编码丰富的结构信息,可归纳为四层语义:
第一层:工作簿 → 文档标题。JSON 基准中顶层文档name为xlsx_02_sample_sales_data,取自源文件名去除扩展名后的 stem(见 msexcel_backend.py 中DoclingDocument(name=self.file.stem...))。
第二层:工作表 → 二级标题。输出以## SalesData开头,SalesData就是工作簿中唯一工作表的名字。Docling 对 Excel 采取"一表一页、一页一组"的建模:转换时每个工作表先建一页(page),再在该页下建立一个label=GroupLabel.SHEET、name=工作表名的分组(msexcel_backend.py),Markdown 序列化器再把这个分组渲染成二级标题。
第三层:连续单元格区域 → 一张表格。标题之下是标准的 GFM 管道表格,表头行被单独列出。.itxt基准文件用一行点出它的结构层级:
item-0 at level 0: unspecified: group _root_ item-1 at level 1: sheet: group SalesData item-2 at level 2: table with [21x4]21x4表示"21 行 × 4 列",即 1 行表头 + 20 行数据、4 个数据列,与.md、.json完全自洽。JSON 基准中该表的prov记录显示它位于第 1 页、包围盒为l=0, t=0, r=4, b=21——包围盒单位正是单元格索引,再次印证表覆盖了 4 列 21 行的完整矩形区域。
第四层:单元格级语义标注。打开 JSON 基准文件 的tables[0].data.table_cells,每个单元格都被建模为TableCell,携带start_row_offset_idx/end_row_offset_idx、start_col_offset_idx/end_col_offset_idx精确行列区间,且首行单元格的column_header被标记为true——这是表头语义被显式保留的直接证据,也是后续面向 LLM 的问答、表格检索能正确识别列含义的前提。
Date 列格式的秘密:为什么是2024-01-01 00:00:00
基准数据中日期以YYYY-MM-DD HH:MM:SS文本形式呈现,这并非 Excel 的原生显示格式,而是转换管线的一个可复现细节:
- 后端加载工作簿时使用
openpyxl.load_workbook(..., data_only=True)(msexcel_backend.py),拿到的是公式计算后的缓存值,而非公式本身; - 工作簿中 Date 列的底层类型是
datetime对象,后端在抽取单元格文本时直接调用str(cell.value)(msexcel_backend.py),Python 的datetime.__str__因此产出2024-01-01 00:00:00这种"日期 + 零时分秒"的形态。
理解这一细节有助于处理真实业务文件:若你在自己的表格中看到相似的时间戳样式,说明源单元格存储的是 datetime 类型值。反过来,Quantity/Revenue两列是纯数值(如5、5000,无千分位),也被原样字符串化,没有引入任何千分位或货币格式化——Docling 保存的是单元格的逻辑值,而不是其在 UI 上套用数字格式后的显示外观。
从工作簿到 Markdown:背后的后端实现链路
要真正看懂这份基准,需要把视线投向负责解析 Excel 的后端MsExcelDocumentBackend(docling/backend/msexcel_backend.py)。它是"声明式后端 + 分页后端"的组合,处理步骤可以概括为:
- 依赖校验:解析依赖
openpyxl,若缺失会抛出带安装提示的ImportError,提示执行pip install 'docling-slim[format-xlsx]'(msexcel_backend.py)。顺带一提,若输入是旧版二进制.xls,后端会先经 LibreOffice 转换为.xlsx再解析(msexcel_backend.py)。 - 工作簿 → 文档骨架:
convert()创建DoclingDocument,写入文件名、MIME 类型与二进制哈希作为DocumentOrigin。 - 逐工作表转页:
_convert_workbook遍历全部工作表,每一页记录page.size,其宽高由该页所有条目的包围盒边界推导(单位即单元格索引)。 - 工作表内容抽取:
_convert_sheet依次执行"找表格 → 找图片 → 找原生图表",最后按包围盒的top坐标对组内子元素排序,保证视觉顺序与导出顺序一致。 - 表格检测:
_find_data_tables先从_find_true_data_bounds得到真实数据边界(而不是sheet.max_row——该值会被格式刷或幽灵格式撑到百万行),再对每个非空单元格执行泛洪填充(BFS),把四邻域连通的非空区域合并为一张表。这正是 4 列数据被识别为一张连续21x4表而非多张小表的原因。 - 单元格语义化:遍历矩形包围盒内每个单元格,首行(
row == 0)自动打上column_header=True(msexcel_backend.py),合并单元格会换算为row_span/col_span。若数据区左侧上方出现一个"横跨多列"的独立标题单元格,_split_leading_section_label还会把它拆出来单独作为正文文本,而不是并入表格。
因此,本文的.md基准可以看作整条链路的"可视化结果":## SalesData对应步骤 3/4 的分组,21x4表对应步骤 5 的连通区域识别,表头行对应步骤 6 的表头标注。
三重基准如何互相印证:测试代码视角
Docling 用"一份输入、三种断言"来锁定该文件的转换行为,相关逻辑全部位于 test_backend_msexcel.py 的test_e2e_excel_conversions:
pred_md: str = MsExcelMarkdownDocSerializer( doc=doc, params=MarkdownParams(compact_tables=True, layers=DEFAULT_CONTENT_LAYERS), ).serialize().text assert verify_export(pred_md, str(gt_path) + ".md", GENERATE), "export to md" pred_itxt: str = doc._export_to_indented_text(max_text_len=70, explicit_tables=False) assert verify_export(pred_itxt, str(gt_path) + ".itxt", GENERATE), "export to indented-text" assert verify_document(doc, str(gt_path) + ".json", GENERATE, fuzzy=True), "document document"其中GENERATE来自test_data_gen_flag,用于控制"直接生成基准"还是"严格比对基准"。三种断言分别覆盖:
- Markdown(
.md):校验面向消费端(RAG、LLM 上下文)的文本形态,使用 Excel 专用的MsExcelMarkdownDocSerializer,并开启compact_tables=True紧凑表格布局;这正是"零额外字符、纯管道表格"输出风格的来源; - 缩进文本(
.itxt):校验文档结构树(root → sheet 分组 → table [21x4]); - JSON(
.json):以模糊比对方式校验 DoclingDocument 的完整数据模型(分组、表格单元格、表头标记、provenance 包围盒)。
测试夹具documents会扫描 sources 目录 下所有*.xlsx与*.xlsm(自动剔除~$开头的 Excel 临时锁文件),逐一经DocumentConverter(allowed_formats=[InputFormat.XLSX])转换,再以同名文件拼接得到 groundtruth 路径(test_backend_msexcel.py)。你可以把这个夹具当作"如何成批转换 Excel 并做回归断言"的现成范本。
在本地复现这份输出
你可以用与测试完全一致的 API 在本地复现.md基准。先确认依赖就绪(docling-slim[format-xlsx]或完整docling安装均可,它们都带openpyxl),随后执行:
from docling.document_converter import DocumentConverter from docling.datamodel.base_models import InputFormat from docling_core.transforms.serializer.markdown import MarkdownParams from docling_core.transforms.serializer.markdown_excel import ( MsExcelMarkdownDocSerializer, ) from docling_core.types.doc.document import DEFAULT_CONTENT_LAYERS converter = DocumentConverter(allowed_formats=[InputFormat.XLSX]) result = converter.convert( "tests/data/xlsx/sources/xlsx_02_sample_sales_data.xlsm" ) serializer = MsExcelMarkdownDocSerializer( doc=result.document, params=MarkdownParams(compact_tables=True, layers=DEFAULT_CONTENT_LAYERS), ) print(serializer.serialize().text)输出即上文展示的## SalesData表格。layers=DEFAULT_CONTENT_LAYERS的含义值得说明:Excel 中的隐藏工作表会被归入ContentLayer.INVISIBLE(见 msexcel_backend.py),默认导出层不含 INVISIBLE,因而隐藏表不会污染 Markdown;而单元格批注位于NOTES层,需要显式包含该层才会进入导出。
如果不想手写序列化代码,也可以使用官方 CLI 一次性完成转换与多种格式导出(仓库 docling/cli/main.py 中即包含document.save_as_markdown(...)的导出逻辑)。批量处理多个工作簿时,可参考 examples/batch_convert.py 的循环转换思路。
通过 Backend Options 定制输出
真实业务里你未必想要"每个单元格区域都是一张表"的默认行为。Docling 为 Excel 后端提供了MsExcelBackendOptions(定义于 docling/datamodel/backend_options.py),可通过DocumentConverter的format_options传入:
| 参数 | 默认值 | 作用 |
|---|---|---|
treat_singleton_as_text | False | 把孤立的 1×1 单元格(周边全空)当作TextItem而非TableItem输出,避免单格"伪表格"泛滥 |
parse_charts | True | 解析工作表中嵌入的原生图表(柱状/折线/饼图/散点等),每个图表输出为带类型分类与底层数据表重建的PictureItem |
render_chart_images | False | 是否调用 LibreOffice 把图表栅格化为图片附加到PictureItem(依赖外部安装且会放大输出体积,故默认关闭) |
gap_tolerance | 0 | 允许跨过多少个空行/空列把邻近的数据簇并入同一张表,默认为严格模式0 |
sheet_names | None | 只转换名称匹配(大小写敏感)的工作表;None表示转换全部工作表 |
例如,只想导出名为SalesData的工作表、并把 1×1 孤立单元格降级为普通文本时:
from docling.document_converter import DocumentConverter from docling.datamodel.base_models import InputFormat from docling.datamodel.backend_options import MsExcelBackendOptions from docling.document_converter import ExcelFormatOption options = MsExcelBackendOptions( sheet_names=["SalesData"], treat_singleton_as_text=True, ) converter = DocumentConverter( allowed_formats=[InputFormat.XLSX], format_options={InputFormat.XLSX: ExcelFormatOption(backend_options=options)}, ) result = converter.convert("tests/data/xlsx/sources/xlsx_02_sample_sales_data.xlsm")其中gap_tolerance在底层直接驱动_find_table_bounds的 BFS 扩展步长:代码会在四方向上"跳读最多GAP_TOLERANCE步"去找连通单元格(msexcel_backend.py),因此调大它能让被空列隔开的区块合并成一张大表。对应地,仓库测试也覆盖了xlsx_07_gap_tolerance_*、xlsx_08_one_cell_anchor等专门样本,验证这些开关的实际效果。
局限与注意点(结合本样本的延伸思考)
基于源码与基准文件,有几点工程化使用时值得记住:
- 宏不会被执行:
.xlsm仅是"带宏的 XLSX",其 VBA 代码不会被 Docling 解析或执行;data_only=True读取的是工作簿中缓存的公式结果。若目标单元格的值由宏在打开时计算生成且未落盘,转换结果可能为空,此时需先由 Excel 端"打开并另存"刷新缓存值。 - 日期/数字格式化不保留 UI 外观:基准中的
2024-01-01 00:00:00说明输出是单元格逻辑值(datetime 的字符串形式)而非工作表的显示格式。货币符号、千分位等展示样式同样不会出现在导出的 Markdown 中。 - 无图无损输入输出:本样本不包含图片、图表与批注,因此
.md/.itxt/.json都相当"干净";一旦工作簿混入图片(含 openpyxl 无法解码的 EMF/WMF)、原生图表或单元格批注,输出结构会显著复杂化,Docling 也为此准备了图片抽取、LibreOffice 桥接和NOTES内容层等独立机制(对应测试样本为xlsx_emf、xlsx_03_chartsheet、xlsx_comments)。
总结
一份 20 行的销售数据groundtruth浓缩了 Docling Excel 通道的完整设计:.xlsm归入 XLSX 格式族,工作表被建模为"页 + sheet 分组",连通单元格区域经泛洪填充聚合成21x4表,首行自动获得表头语义,datetime 值以字符串形式稳定呈现,最终通过 Excel 专用 Markdown 序列化器输出## SalesData表格。以该基准为参照,你既能理解真实工作簿在转换中可能出现的各种细节(时间戳文本、隐藏层、表头标注),也能通过MsExcelBackendOptions与多重序列化精确控制交付给下游 LLM/RAG 系统的最终形态。
【免费下载链接】doclingGet your documents ready for gen AI项目地址: https://gitcode.com/GitHub_Trending/do/docling
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考