Skip to content

Latest commit

 

History

14 Commits

Folders and files

Repository files navigation

复杂表格 PDF 的结构化提取:思路与方法

一篇技术处理分享:问题拆解、方法选择与验证路径

摘要

在知识库(RAG)建设中,复杂表格进向量库会面临切块截断与召回失败;把这类表格统一结构化,就是本分享要解决的问题。一个看似单一的"PDF 转 JSON"任务,真正的难度不在转换工具本身,而在三个决策:

  1. 把问题拆成两个子问题:结构还原(PDF→Markdown)与语义提取(Markdown→JSON)分离,各自独立选型、独立替换;
  2. 用数据建模定义"正确的输出":将最小数据单元定义为"定额编号 × 规格",而不是"一张表"——输出粒度决定了数据可用性;
  3. 用评估矩阵做工具选型,用平行对比做质量验证:贯穿全程的三个原则——先定评估维度再选工具、先归纳模式再写算法、先跑小样本再规模化。

最终产出:12 个PDF复杂表格文件全部结构化,12,714 条数据,下游问答对 CSV 8,527 条,端到端管线可重复运行。

本文的另一个目的是说明这套方法的边界:它解决的是"印刷语义与数据语义存在系统性偏差的复杂矩阵表"这一类问题。1.4 节用真实结构对照的方式,展示供应链运价表、上市公司财报中的同构输出——同样的结构难题(分档拆分、合并单元格、括号负数、跨页断表)在不同行业反复出现,方法也因此可迁移。


一、问题定义:先拆对问题,再谈方案

1.1 任务的表象与本质

表象:"把 PDF 里的预算定额表转成 JSON"。 本质是两个性质完全不同的问题:

子问题 内容 性质
结构还原 把 PDF 中的表格在 Markdown 里还原出来(行、列、合并单元格) 工具能力问题,有上限
语义提取 把还原出的表格按业务语义映射为数据模型 设计问题,可无限改进

这个拆分决定了整个架构:转换环节只负责"看得清",提取环节只负责"解得对"。环节间解耦换来的好处是任意一环可单独替换——后来云端解析工具出现时,只替换了转换环节,其余全部保留,验证了架构的合理性。

1.2 关键抽象:什么是"一条数据"

如果以"一张表"为输出单位,下游没法查"DN400 的基价是多少"。所以必须定义最小数据单元:

一条数据 = 主键 × 分档维度(在定额语境即"定额编号 × 规格",跨行业映射见 1.4)

横向展开的多规格表(DN300~DN800 并列多列),每列拆成一个独立对象。这个决策带来三个连锁要求,后来全部成为提取规则:

  • 拆分后每条都需独立完整(项目名称、工作内容、单位随行复制)——所以拆页表必须合并回完整表再拆;
  • 明细(人工/材料/机械)必须嵌套在父条内,不可独立浮出;
  • 数量为 0 的消耗项也保留——"有这行但数量为 0"本身就是信息(该定额不含此消耗)。

1.3 数据现实:四类"印刷语义 ≠ 数据语义"的坑

印刷品转数据存在四类系统性偏差——注意,它们不是工程造价的特产,而是跨行业的通病:

系统性偏差 工程造价定额表 供应链运价表 上市公司财报
分档多列拆分 DN300~DN800 多规格并列 重量档 0-45kg / 45-100kg / 100-300kg 并列 期间列 2024 / 2023 / 同比% 并列
合并单元格 项目名称跨多行 货物大类跨多行 科目大类("其中:主营业务")跨行缩进
跨页拆表 一张逻辑表断成两截 航线费率长表跨页断截 年报附注长表(如账龄表)跨页断截
括号/特殊符号语义 (0.200) = 冲减量 -0.200 (15%) = 附加费冲减、— = 不承运 (16,930) = 负数——财报世界通行的"括号即负数"惯例
  1. 分档多列拆分:横向 DN300~DN800 多规格并列,每列拆成一个独立对象(即 1.2 的"主键 × 规格");
  2. 合并单元格:项目名称跨多行;转换工具还会把同格多个条目黏成一个文本块——多个"名称+单位+单价"挤一格,靠 <br>、空格、中文括号分隔,拆不出独立条目;
  3. 跨页拆表:一张表跨页断成两截,下半截没有主键列,仅以明细表头开头;有时标题/表头行还被完整复制一次,产生重复段;
  4. 括号负数:全角括号表负数或冲减,(0.200) 实义 -0.200,与财报 (16,930) 是同一类编码惯例。

方法论启示:处理任何"印刷品 → 数据"的转换,要先盘点源格式的系统性偏差,把它们当成必须显式处理的清单,而不是遇到一个修一个。本项目把这四类全部写进了算法与提示词的硬规则。

1.4 同构验证:三个行业的结构化输出对照

下面的三份 JSON 中,第一份取自真实交付数据的结构(项目/材料名称已脱敏,字段名与数值精度保留原样);后两份为供应链运价表、上市公司利润表按同一模板映射出的对应结构。先看字段级映射:

定额字段 供应链运价表 财报利润表
定额编号 运价编号 行次
项目名称 货物类别 报表项目
规格 重量档(0-45kg…) 会计期间(2024 / 2023)
计量单位 票 / kg 百万元
基价 基础运价 金额
费用构成(人工费/材料费/机械费) 附加费构成(燃油/旺季/偏远) 明细行("其中"嵌套)
人工 / 材料 / 机械[](名称/单位/单价/数量四字段) 附加费明细[] 明细[](名称 + 金额)
括号负数 → 数字负号 费率冲减 → 数字负号 财报括号惯例 → 数字负号
_source 溯源字段 _source 溯源字段 _source 溯源字段

① 工程造价定额表:

{
  "定额编号": "9-78",
  "项目名称": "某某工艺 · 某某工序",
  "工作内容": "施工准备、拆除、清洗、回装、检验、清理现场。",
  "规格": "DN400",
  "计量单位": "处",
  "基价": 1496.35,
  "费用构成": { "人工费": 261.22, "材料费": 663.05, "机械费": 572.08 },
  "人工": [
    { "名称": "综合工日", "单位": "工日", "单价": 183.86, "数量": 1.419 }
  ],
  "材料": [
    { "名称": "材料A", "单位": "套",  "单价": 2212.39, "数量": 0.025 },
    { "名称": "材料B", "单位": "片",  "单价": 10.96,   "数量": 0.100 },
    { "名称": "材料C", "单位": "m",   "单价": 118.60,  "数量": -0.200 }
  ],
  "机械": [
    { "名称": "机械甲", "单位": "台班", "单价": 287.42, "数量": 1.811 },
    { "名称": "机械乙", "单位": "台班", "单价": 68.85,  "数量": 1.773 }
  ],
  "_source": { "chapter": "第一章 某某工程", "h3": "十、某某项目" }
}

注:材料C 的 -0.200 来自原表 (0.200)——括号即冲减量;数量为 0 的消耗项同样保留对象。

② 供应链跨境物流分区运费费率表(同一模板推导的一般结构):

{
  "运价编号": "7-1",
  "货物类别": "普货",
  "规格": "0-45kg",
  "计量单位": "票",
  "基础运价": 85.0,
  "费用构成": { "燃油附加费": 12.75, "旺季附加费": 8.5, "偏远附加费": 0.0 },
  "附加费明细": [
    { "名称": "燃油附加费率", "单位": "%", "单价": 15.0, "数量": 1.0 },
    { "名称": "旺季附加费率", "单位": "%", "单价": 10.0, "数量": 1.0 }
  ],
  "_source": { "version": "分区费率表 Zone A~K", "segment": "普货 0-45kg 档" }
}

注:0-45kg / 45-100kg / 100-300kg / 300kg+ 每个重量档 = 一个独立对象(同"编号 × 规格");原表 —(该档不承运)= 定额的"数量为 0 也保留";原表 (15%) 冲减 = 括号负数问题;分区版本 = 多区域定价。

③ 上市公司年度利润表(同一模板推导的一般结构):

[
  {
    "行次": "1-1",
    "报表项目": "营业总收入",
    "规格": "2024年度",
    "计量单位": "百万元",
    "金额": 18420.0,
    "明细": [
      { "名称": "其中:主营业务收入", "金额": 17900.0 },
      { "名称": "其中:其他业务收入", "金额": 520.0 }
    ],
    "_source": { "report": "年度报告", "chapter": "财务报告" }
  },
  {
    "行次": "2-1",
    "报表项目": "营业总成本",
    "规格": "2024年度",
    "计量单位": "百万元",
    "金额": -16930.0,
    "_source": { "report": "年度报告", "chapter": "财务报告" }
  }
]

注:每个会计期间(2024 / 2023)= 一个独立对象(同规格拆分);-16930.0 来自原表 (16,930)——财报世界通行的"括号即负数";"其中"缩进行 = 嵌套明细。

结论:同一套数据模型(主键 × 分档维度 → 主数值 + 三分类费用 + 四字段明细 + 溯源),加同一套边界规则(分档拆列、括号转负、空档保留),可以不做任何结构性修改地覆盖三个行业。工具的评估维度、切分的模式归纳、提取的边界规则,全部建立在这类"印刷语义与数据语义偏差"上,与行业无关。这也是本文以"复杂表格"而非"工程定额表"为题的原因。


二、总体做法:把复杂表格从 RAG 管道中单独拎出来

2.1 场景与失效:普通 RAG 对复杂表格有两处失效

表格数据的最终去处是问答知识库。知识库建设的常规路线是:PDF → Markdown → 切块 → 向量化入库,问答时靠向量检索召回相关片段。这套流程对密集文本是成熟的,但对复杂表格有两处失效:

  1. 切块截断表格:切分按长度进行,跨页长表被拦腰截断,进库的是一堆残表;
  2. 向量召回失败:Markdown 表格结构复杂,向量化后语义丢失——问"DN400 基价是多少",召回的可能是相邻章节的文字,而不是这张表。

2.2 三条路线怎么选

先说适用边界:表格量少时,普通 RAG 加人工核对完全够用,不需要任何结构化处理。本方案瞄准的是"大批量、复杂、同类型表格数据"。在此边界下,三条路线:

路线 做法 问题
A 普通 RAG 直接切块入库 表格与正文一起向量化 截断 + 召回失败;量少尚可,批量场景不可用
B 大模型直接读 PDF 逐份提取 跳过转换环节 单份文档超出上下文容量;批量调用成本过高
C(采用)先转、再分、再提取 PDF 低成本转 Markdown → 算法区分表格与正文 → 表格统一结构化入库 需要一条专用管线——本文的全部内容

选 C 的理由:转换走批量异步、按量计费,成本可控;提取只处理表格,把钱花在刀刃上。更重要的是治理思路的差别——表格结构化入库后,问答走精确查询(按编号/规格查字段),不再依赖向量召回。这是对 RAG 失效点的根治,而不是修补。

2.3 管线四步各安其位

PDF ──①格式转换──→ Markdown ──②切分/分区──→ 表格片段 ──③提取──→ JSON ──④验证与批处理──→ 结构化入库
环节 做什么 解决什么问题
① 格式转换 把排版还原成表格 后续一切的地基,还原质量决定上限
② 切分/分区 表格与正文分离;长文档切成可处理的小片段且不切断任何表 截断问题;单次处理的容量问题
③ 提取 表格 → 第一章定义的统一数据模型 召回失败的根因(把"模糊召回"替换为"精确结构")
④ 验证与批处理 多路对比、重试、监控 批量场景下单条正确 ≠ 整体可靠

后续第三~六章沿这四步展开——每步先讲做法逻辑,再讲选型与做法考虑。


三、格式转换:三类转换器怎么选

这一步把 PDF 还原成 Markdown 表格。工具用错了,后面环节全错——所以先看失败长什么样,再谈评估维度与选型。如果数据比较敏感,建议用本地 OCR 模型——它同样把表格输出成 HTML,方便后续用算法识别定位。

3.1 失败先行:先看用错工具长什么样

同一张定额表,两种工具的两种典型失败(早于一切评估):

  • OCR 视觉解析类:合并单元格的结构还原不错,但一个单元格里的多个材料条目被黏成一个文本块——提取环节无法拆出独立条目,5 条材料变 1 条;
  • PDF 底层对象解析类:每条数据保持独立,但跨行跨列的合并单元格层级还原错位,表头对不上数据列——条目全在,位置全错。

两种工具各对一半、各错一半。把"对在哪、错在哪"抽象成两个正交的评估维度:

维度 含义 为什么关键
复杂结构还原 rowspan/colspan、合并单元格的层级能否还原 定额表合并单元格极多,结构错了语义必错
独立条目分辨 同一单元格内的多个数据条目能否保留边界 边界丢了,拆不出"一码一规格",后患无穷

3.2 按维度实测选型

转换器 结构还原 条目分辨 结论
OCR 视觉模型解析类 ✅ ❌ 同格多条目被合并 单独用必错
PDF 底层对象解析类(opendataloader) ❌ ✅ 天然保留条目独立 单独用必错
云解析类(MinerU) ✅ ✅ 两端同时满足

两类工具的差异根源:OCR 类是从页面渲染图里"看"出结构,容易把同一单元格的多条文本黏成一个块;底层对象解析类直接读 PDF 的文本对象流,每条数据天然独立,但不理解视觉布局,表头层级易错。这正是选型困难的根源——常见工具没有一个两个维度都强。

3.3 两阶段策略:先融合补短,后做减法

  • 第一阶段(无云解析类时):前两类能力互补 → 用底层对象类的条目边界去修复 OCR 视觉类的结构。这是"没有满分工具时,用组合逼近"的常规思路;
  • 第二阶段(云解析类可用后):引入实测对比,发现其两个维度都满分,且自带完整上下文。此时果断删除融合后处理——管线少一环,代码更少、失败面更小。

方法论启示:工具选型要建立评估维度矩阵,用实测数据说话;候选集合变化时敢于推翻既有方案做减法。"删掉的复杂度"和"新增的能力"同样有价值。


四、切分/分区:把表格完整地切出来

目的很单纯:把 Markdown 里的表格数据完整地划分成一块块,方便逐个传给模型,保证任何一张表都不被切断;同时把表格和正文区分开——正文不进提取环节,只处理表格。OCR 模型或云解析工具输出的表格基本都是 HTML,天然便于按标签切分。

4.1 切分规则

切分规则不复杂。一开始试了按固定长度切,发现会切断表格、数据错位;于是把全部转换产物过了一遍,归纳出实际存在的几种表格排布,逐一定下切法:

排布 切法
一节就一张表 直接切出
同一张表跨页拆成两截 按定额编号一致先合并
按规格/条件分档的一组表 表与表之间切
一章里几十上百张同类表 按表边界切
纯文字说明 跳过,不进提取

"一张表是否完整"的判定同样简单:表里含"基价"行就是完整表;表头是"名称/单位"又没有基价行,就是拆页表的下半截。识别代码直接交给 AI 写——把需求和判定规则讲清楚,AI 生成算法,再拿真实样本验证一遍就行,这部分没有多少需要费脑子的地方。


五、提取:从规则匹配到"自动结构发现"

这一步负责把摆好位置的表格,映射成第一章定义的数据模型(主键 × 分档维度)。格式层面的麻烦在前两个环节已经解决,这里剩下的全是语义边界:一条数据从哪里开始、到哪里结束、哪些算一条。

5.1 数据模型设计的三个关键决策

多轮提示词迭代揭示了一条经验:大量提取错误来自"边界情况没说清",而不是模型能力不足:

决策 内容 失败教训来源
一码一规格拆对象 横向多列规格 → 每列独立对象 不拆则无法按规格查询
明细必须嵌套 名称/单位/单价/数量必须入父条 人工/材料/机械 数组 不嵌套则明细浮出成垃圾顶层条目
边界规则显式化 括号→负数、0 值保留、补零规则 长尾错误几乎全是这类边界

5.2 规则引擎的定位:零成本的确定性底座

规则引擎(可调试、零 API 成本、确定性输出)先把通用逻辑跑通。其有价值的三处设计:

  • 多值单元格拆分策略 A/B:中文括号后切分为主,空格拆分+型号后缀合并为回退——先精准后兜底;
  • 规格配套材料补零:有的材料只在匹配当前规格时才有消耗量(如管径配套件只出现在对应管径的行里)——按规格判断,无消耗的记 0,而不是丢掉这条材料;
  • 适用条件提取:比较符 + 条件关键字匹配(如"蜡厚≤20mm"),输出到独立"条件"字段。

5.3 LLM 引擎的范式转变:不要发明字段名

规则引擎每遇到一种新表格式就要写一套提取器。LLM 引擎改变了范式:

"字段名使用表格中实际出现的标签文字——不要自己发明字段名。"

规则引擎的思路是"我定义 Schema,你去匹配";LLM 引擎的思路是"表格自己定义了 Schema,你把它读出来"。一套提示词因此适配了定额表 / 全费用清单 / 多区域定价三种差异极大的表型。这是从"规则匹配"到"结构模式识别"的转变。

配套的兜底机制(LLM 输出不可控,必须工程化兜底):

  • 续表识别:无编号行的表格 → 明细归入上文最近编号;
  • 孤立明细归附:输出解析后,把游离的明细项归附到父条;
  • 异常自动重试:解析失败的输出自动重试;
  • 健康监控:按节统计解析失败率、孤立计数、编号断档——批处理必须有可见性。

5.4 质量验证方法:同输入三后端平行对比

不依赖人工逐条标注(成本不可行),而是让三套后端(规则引擎 / LLM / 修复版——即规则引擎按第一轮差异暴露的问题修完 bug 后的版本)处理同一批输入,平行产出三套结果,用对比找差异:

  • 差异点 = 两类问题:规则引擎的漏网格式、LLM 的幻觉/遗漏;
  • 对比工具从条目数、类型分布、行/列数、页面覆盖等维度做差异定位;
  • 最终结果以对比结论为准整合,两条路径的结果收敛到完整覆盖。

方法论启示:质量评估不一定需要黄金标注集,"多路独立实现 + 差异分析"是低成本有效的替代,尤其适合人力标注昂贵的垂直领域数据。


六、验证与批处理工程化

大批量场景下,单条正确 ≠ 整体可靠:这一步把 5.4 的验证能力固化进批处理流程,保障过程可见、出错可修、结果可验。规模化时真正耗时的不是提取本身,而是批处理状态的可见性与故障恢复:

  • 任务拆解与并发:转换批量并发(5 路 / 3 路),并带任务完成通知,长任务不干等;
  • 断点进度:进度持久化,中断后能续跑,不必重来;
  • 单一 CLI 入口:convert / batch / validate 三子命令,干跑模式先验证解析再花 API 钱;
  • 配置外置:转换器、模型、密钥全部走环境变量,无硬编码。

七、可复用的方法论沉淀

# 方法 本项目中的体现
1 先小样本试点,再归纳共性,最后规模化 单册试点 → 全量文件模式归纳 → 全套批处理
2 先定评估维度,再选工具;候选变化时重估并敢做减法 转换器能力矩阵;云解析工具引入后删除融合环节
3 数据驱动设计:先归纳实际排布再定切法 按真实表格排布分 5 类逐一定策略
4 边界规则显式化是提示词质量的主要来源 括号负数、0 值保留、补零规则
5 LLM 输出不可控,兜底必须工程化 续表识别、孤立归附、自动重试、健康监控
6 用多路独立实现 + 差异分析替代黄金标注 三后端平行对比 + 差异定位工具
7 抽象层次循序渐进,消除重复 单脚本 → 多套提取器 → 适配器 + 单一提取引擎
8 语义完整性优先于形式约束 超限单表不切断;逻辑表合并优先于长度

About

复杂表格 PDF 结构化提取:LLM 理解 + Agent 校验双阶段,任意表格格式零预设字段,双转换引擎(MinerU/OCR)。Python · Streamlit

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages