平时接数据分析和自动化处理的活儿,我大部分时间都耗在两类事情上:一是把 PDF、网页、图片里那些格式混乱的内容抽出来,二是把这些半结构化甚至完全非结构化的数据规整成能塞进数据表或数据库的样子。之前一直靠正则表达式和各种解析脚本凑合,直到最近一个项目里需要快速处理一批格式极其不统一的采购单据,我才真正把cua(Composable Unstructured Analytics,组合式非结构化数据分析)这套思路从头到尾用了一遍。这名字乍看有点抽象,说白了就是一套把“文档解析、信息抽取、语义理解、结构化输出”拆成可组合模块,再配合大模型能力进行分析的工程化方案。
如果你也经常被 PDF 表格、扫描件、网页数据折磨,或者正在为团队搭建数据处理管线,这篇内容可以帮你少走不少弯路。我会从工具选型逻辑、核心模块拆解、完整实操流程到常见坑点排查,把这次实践里能直接拿去用的细节通通写出来。
1. 先聊聊我为什么盯上 cua:非结构化数据分析的痛点
先说个背景。我接到的任务场景是这样的:某企业内部有上千份历史供应商单据,包括扫描 PDF、手写备注的 Excel、网页导出的 HTML 表格,甚至还有一部分图片形式的发货单。业务方希望把这些数据统一抽取出“供应商名称、品名、数量、单价、总价、采购日期、备注”七个字段,然后按月份汇总分析。最要命的是,这些文件的排版几乎没有两份长一样的,表格有的合并单元格,有的把表头竖着放,有的品名里混了一堆规格说明。
这种活儿搁以前,先让实习生人工录入几个月,或者写一堆针对特定模板的解析脚本,模板一变脚本就废。后面我采用cua的思路,原因很直接:它把整个分析流程拆成了可以独立替换的模块,每个模块各管一件事,数据和逻辑分别管理,新的文件格式来了不用推翻重来。这点特别关键——这类需求本质上没有一个能被正则表达式包打天下的解法,只能靠语义理解加灵活流水线。
还有一个现实原因:大模型擅长的“理解和抽取”与脚本擅长的“定位与转换”正好能互补。但要落地,必须解决一个问题——怎么把大模型输出稳定地映射回结构化字段?cua里模式映射和验证环节就是干这个的。我后面会详细展示这一块的处理细节,包括怎么处理表头异构、怎么在抽取结果里对齐日期格式这类实际问题。
2. 关于 cua,我从架构层面理解到的核心设计思路
面对非结构化数据,一般有三种处理路线:纯规则脚本、纯大模型抽取、规则加模型的组合式方案。纯脚本对于固化的模板确实快,但一遇到版式变化就崩;纯大模型回答看起来聪明,但输出不稳定,没法保证每次字段都对齐。cua的组合式思路走的是第三条路,也说不上多玄妙,核心思想是分而治之,再把各阶段的结果串起来。
2.1 解析、理解、转换三层分离的组合逻辑
我现在把这类任务都拆成三层来考虑。第一层是解析层,负责把 PDF、图片、网页、Word 这类物理格式转成带结构的信息,比如按页面顺序识别出的段落、表格、标题;第二层是理解层,通常是靠大模型对解析出来的文本做语义级抽取,回答“这一段里哪个词是供应商”“这个数字代表单价还是总价”;第三层是转换层,把大模型的输出做类型转换、字段对齐、去重合并,最后落成一张规规矩矩的结构化表。
cua的本质就是给这三层搭了一套规范的骨架。解析层可以换成不同的文档解析引擎,理解层可以切换不同的模型,转换层可以插各种验证函数。模块之间传递的是结构化的中间数据,不是零散的文本字符串,这样一来每一层都可以独立迭代。我之前试过那种“大模型一把梭”的方案,看着省事,但模型只要一升级,输出格式略微变化,下游就全部乱套。组合式设计最大的好处是隔离了这种风险。
2.2 为什么说流式分段比整篇抽取更稳
实际操作中,把整份文档一次性丢给大模型去抽取七个字段,效果往往不好。原因在于长文档里信息密度高、上下文互相干扰,模型容易“遗忘”前文内容,或者被中间一段相似文本带偏。我在项目里使用的是按区块处理的方式:解析层把文档切分成表头区块、行项目区块、备注区块,每个区块单独送到抽取环节。举个例子,一份采购单里“品名”字段可能在表格里,也可能在表头右侧的描述段落里,让模型整篇找容易出错,分别询问“这个区块里有没有品名信息,值是什么”,正确率明显更高。
区块切分也不是越碎越好。切得太碎会丢失上下文,比如“上述商品”这种指代词就失去了解释依据;切得太粗又会重新陷入整篇抽取的老问题。实际操作中我使用的是“先按表格结构切,再按段落语义合并”的策略:页面布局分析出表格物理边界,表格内部每一行作为一个独立抽取单元,表格外的文字按标题、正文、页脚分组,每组再作为一个单元。这套策略在我处理的那批单据上效果挺稳的。
2.3 中间数据的“模式映射”环节到底在解决什么
抽取模型给出来的结果,常常是“品名:3M 防尘口罩 KN95 耳戴式”这种自然语句,但下游数据表需要的是字段化的“品名”和“规格”。模式映射环节干的事,就是把大模型输出的自然语言描述,按照预定义的目标模式再整理一次。这一步我把它理解成“给模型定义一个边界清晰的出口”,让输出尽可能变成可以直接落库的值。
具体到七字段需求,我会定义每种字段的类型规则:日期必须转成 ISO 格式,金额必须由字符串转成数值,备注保留原始文本。映射器内部还可以支持基于正则的微调,比如从“单价 12.50 元 / 个”里抓取 12.50,然后由类型转换函数转成浮点数。这种灵活配置比让大模型自己决定输出格式要可靠得多,毕竟大模型擅长“看懂”,不擅长“严格执行格式纪律”。
3. 实操准备:环境、模型选择与首个文档解析管线搭建
这一部分直接进入实操。我用的环境以 Python 为主,大模型部分采用本地部署的开源模型加一个云端 API 的混合配置。两套环境我都需要跑通,因为工作里有不少数据涉及本地处理的要求,不能所有文件都送出去解析。
3.1 环境安装与初始配置
我的基础软件环境是 Python 3.11、pip 管理依赖。核心库包括:文档解析用的pdfplumber和paddleocr,向量化和语义分块用的langchain相关组件,大模型接口统一封装成 OpenAI 兼容的格式。项目里我还单独安装了pydantic做输出校验,这是我一直以来的习惯,因为数据进到下一个环节之前,先验证过总是更安心。
安装完依赖以后,我先做了一个最小的连通性测试:加载一个单页 PDF,解析出文本块,调用模型抽取两个字段,看看输出能不能被 pydantic 校验通过。实测这一步能筛掉大量环境问题——比如模型服务没起、接口格式不兼容、依赖版本冲突,往往在第一个测试文件上就暴露了。强烈建议你搭好环境后先做这种“最小闭环”,别一上来就跑完整流程,否则出了问题很难分清楚是哪一层导致的。
3.2 大模型选型时我重点看了什么
这个项目里抽取质量直接影响后续分析的准确性,模型选型不能拍脑袋。我的选型维度主要有四个:中文抽取准确率、对长文本上下文的支持、输出 JSON 的稳定程度、延迟和成本。开源模型我试了几个常见的中文能力较强的底座模型,抽取准确率基本够用但速度偏慢,云端 API 模型速度快了很多,代价是单价高一些。最后的取舍是本地模型处理敏感文档,云端处理非敏感的大批量文件。
另外一个容易被忽略的点是“温度参数”。抽取任务里我不需要模型发挥创造力,所以把温度调到了接近于零的值,减少随机性。项目里每类抽取节点的 temperature 值我都单独配置,比如行项目抽取可以稍微留一点点弹性,而日期、金额这类字段映射节点的 temperature 直接设为 0。这算是个很小但很实用的细节。
3.3 用少量样例先跑通一个直通式流程
我把三个有代表性的文件丢进管线:一个扫描版 PDF、一个网页导出的 HTML、一张发货单图片。直通式流程里没有做花哨的优化,就是最简单的“解析 → 文本块 → 模型抽取 → 格式转换 → 输出表”。跑通以后发现扫描版 PDF 的 OCR 文字里混了大量错别字,比如“供应商”被识别成“应商”,但语义抽取环节居然还能大致猜出正确意思,这是组合式设计的好处之一——语义纠错能力比硬规则强。
但这并不意味着 OCR 错字不用管。我在项目里发现,模型对关键数字的敏感度远高于对错别字的容忍度,金额“1250”被识别成“125O”时模型很容易出错。所以后来我加了一层数字校验:抽取结果里的每个金额字段都要过一道字符白名单,非数字字符标记为可疑。这一步让我后来排查数据异常时省了不少时间。
4. 文档解析层的关键配置与效果实测
文档解析这一层,我建议不要指望一个引擎吃遍所有格式。我的项目里是三个引擎协同工作:PDF 结构化解析、OCR 扫描识别、HTML 表格抽取。每个引擎都有它的适用边界和最佳配置参数。
4.1 PDF 解析:表格物理边界怎么拿才准确
对于数字版 PDF,pdfplumber的表格识别能力相当能打,但有个关键经验:直接用默认参数识别复杂表格时,表格边界经常被切断或合并。我的配置是调用页面对象的find_tables()时调整 vertical_strategy 为 text,这样会基于文本行来推断列边界,而不是单纯靠线条;对于没有明显边框的表格,这个配置比默认的线条策略稳定很多。
另外,遇到带旋转文本或页眉页脚的页面,我一般会在解析前先把页面裁剪掉页眉区,再切分正文。页眉页脚如果混进表格数据区,抽取结果里会出现重复的页码和公司名,给后续字段映射带来干扰。裁剪和区块划分逻辑写在解析层里,对上层完全透明。这个小改动让表头识别准确率提升了大概两成。
4.2 OCR 识别:质量、速度与格式三者的取舍
扫描件的解析没有捷径,OCR 引擎的选型直接决定上游文本质量。我实测下来,PaddleOCR 中文识别表现不错,缺点是对低分辨率图片里的表格线条识别不够稳定,偶尔会漏掉列边界。处理这类问题我用的补救方案是:不依赖 OCR 直接还原表格结构,而是先把整页 OCR 成带坐标的文本块,再用坐标聚类的方式重建行列关系。
图片分辨率设置在 300 DPI 是一个经验值:太低会导致小字号文字识别不全,太高会显著拖慢解析速度。对于发票、发货单这类元素密集的文档,300 DPI 基本能在质量和速度间取得平衡。如果图片本身拍歪了,我会先做透视校正再送 OCR,这一步对结果影响很大,但很多人容易忽略。
4.3 HTML 表格提取:应对不规则嵌套
网页导出的 HTML 表格是另一种麻烦,结构里经常嵌套着行内标签、合并单元格、注释节点。直接用 pandas 的read_html能处理规整的表格,但遇到复杂嵌套时会丢数据或错位。我的处理方式是先解析 DOM 树,把table节点下的tr、td结构标准化成统一的二维数组。遇到rowspan和colspan时,我会根据行列坐标把合并单元格的值填充到对应位置。
这一步里有一个容易翻车的点:HTML 表格里空单元格和合并单元格在视觉上是两回事,但解析结果里都可能是空值。业务上如果只处理普通空格,合并单元格的填充逻辑就会出问题。后来我在标准化层里给两类空值加了不同标记,这样后续转换层可以区分处理。
5. 语义抽取与模式映射,把模型输出变成可信数据
解析层解决的是“把文档变成文本块”,真正出成果的是语义抽取与模式映射的组合。这层也是我花时间最多的部分,因为模型输出的不可控性,所有坑都在这里等着你。
5.1 构建一个可复用的行项目抽取模块
行项目抽取是采购单据处理里最核心的一环。每一行都要抽取“品名、数量、单价、总价”。我设计这个模块时没有用“整表抽取”的方式,而是把表格按行切分,每一行作为一个输入单元。每行送入模型时附带列头信息,作为语义上下文。例如“品名:xxx,数量:10,单位:箱”这种模型友好型的文本,抽取准确率远高于把整张二维表丢进去。
具体做法是:先把二维数组的每一行拼接成带列名的文本,再交给模型。模型的任务简化为“根据键值对文本提取指定字段”,难度小了很多。字段结果出来后,还要经过一个金额格式标准化模块,把人民币符号、千分位分隔符、全角数字统一处理成标准浮点数。这个模块里有字符白名单、正则兜底和异常标记三层保护,确保脏数据不会静默流到下游。
5.2 借助 Pydantic 做响应式校验
大模型输出的 JSON 偶尔会出现字段缺失、值类型错误、多出解释性文字等情况。我的做法是给每个抽取节点定义好 Pydantic 模型,比如日期字段必须是date类型、金额字段必须是float,并且允许额外字段被忽略。这样只要模型返回的 JSON 结构不对,校验就会直接报错,我可以在管线里捕获异常并触发重试逻辑。
这里有一个实践上的经验:重试时不要让模型重复看同样的内容,而是稍微改变一下提示的表达方式,比如加上“只输出 JSON,不要解释”,这样很多输出结构问题在第二轮就被修正了。偶尔也会遇到连续重试都不通过的情况,这种数据我会单独标记为“需人工复核”,而不是任由它阻塞整个批处理流程。
5.3 表头异构怎么用映射配置解决
项目里很多单据的表头不叫“品名”,而是“物料名称”“产品描述”,甚至有的把品名写在“规格型号”列里。这种语义异构问题,正是组合式架构的优势所在:我把常见的列名同义词配置成映射规则,模型在抽取时也会参考这些映射提示。例如定义了一个谓词“goods_name”,它关联的同义词有“品名、物料名称、产品名称、货物描述”。模型看到这些同义词后,就能把不同表头的列统一映射到目标模式。
这一步要特别小心,别把同义词设得过于宽泛,否则会产生误抽。比如把“名称”直接设置为“品名”的同义词,很可能把“供应商名称”也抽进去。我最后使用的是“基于上下文的同义词映射”,即同义词匹配要考虑所在列的相邻信息,而不只是看表头字符串本身。这个细节让字段对齐准确率上了一个台阶。
6. 实操过程复盘:一趟完整的批量数据处理实录
前面讲的偏模块化,现在挑一个完整的处理批次,把整个流程从前到后串起来。这批数据有 120 份 PDF,混合数字版和扫描版,全部来自同一个供应商但版式有三种。
6.1 批次处理前的样本质检
正式跑批前,我先从 120 份里挑了 6 份不同类型的样本,人工核对了一遍期望字段值,形成标准答案。这是我一直坚持的做法:不知道正确答案,就没法评估管线的准确率。样本里我特意挑了版式差异最大的几份,因为它们最容易暴露解析层和抽取层的问题。确认样本抽检通过后,才放开做全量跑批。
全量跑批的过程其实并不顺利。第一批跑完后,我发现总体字段抽取准确率大约是 92%,但其中“备注”字段的准确率明显偏低,原因是有几份单据的备注里有手写批注,OCR 识别率不高。数字版 PDF 的字段准确率到了 97%,扫描版只有 86%。这个对比说明,解析层质量对最终结果的影响比模型选择更大。
6.2 参数调整后,从 92% 到 96% 我做了什么
看到准确率差距后,我针对扫描版做了三处调整。第一,把 OCR 的文本检测阈值调低一点,让更多边缘字被识别出来,虽然会增加噪声,但配合模型语义纠错反而提升了整体效果;第二,对手写备注区域单独走一次放大预处理,识别率提升很明显;第三,在抽取层的提示词里专门加入了“如果文本里有疑似 OCR 错误,根据上下文猜测正确值”的说明。这三处调整让扫描版的准确率从 86% 提升到了 93%,全量综合准确率到了 96% 左右。
剩下的 4% 错误大多集中在极端低质量扫描件和手写模糊的备注上。这类数据我没有强求模型搞定,而是加入了“置信度”的概念,低置信度的记录自动进人工复核队列。别小看这个设计,它能避免把错误数据写进最终统计表,这在业务场景里可能是致命问题。
6.3 最终输出物长什么样
流程跑完,输出的是一张标准化的 CSV 表,有固定表头、格式统一的日期和金额。同时我额外生成了一条数据质量报告,记录了每份文件的解析状态、字段缺失情况、置信度均值和复核建议。业务方拿到这张表之后可以直接做透视分析,不再需要人工清理。整个过程从人工录入需要数周缩短到了一小时内完成首轮处理。
我给业务方交付的时候特意加了一个说明:这套流程适合处理“格式多样、语义确定”的数据,如果是完全开放式的语义理解任务,比如分析合同里的风险条款,这套组合式架构也能跑,但模型选型和抽取模式建议需要重新设计。
7. 常见问题与排查技巧实录
处理这类任务时,我遇到了不少规律性问题。下面整理成一张速查表,直接对着看就行。
| 问题现象 | 常见原因 | 排查与解决方案 |
|---|---|---|
| 表格列错位 | PDF 表格解析时列边界推断失败 | 调整 vertical_strategy 为 text,检查页面是否存在旋转文本 |
| 金额数字错误 | OCR 将数字识别成相似字母 | 增加金额字段白名单校验,对可疑数据自动标记复核 |
| 日期格式不统一 | 原始单据存在多种日期表达方式 | 在模式映射层统一转 ISO 格式,解析失败时触发重试 |
| 品名抽取到供应商名称 | 同义词映射设置过宽 | 改用基于列上下文的同义词匹配,避免误抽 |
| 模型输出非 JSON | 温度参数过高或提示约束不强 | 降低温度接近 0,重试提示改为“只输出 JSON” |
| 扫描件识别率低 | 分辨率不足或图像倾斜 | 确保 300 DPI,先透视校正再送 OCR |
| 合并单元格内容丢失 | 二维数组标准化未处理 colspan | 解析 DOM 时按坐标填充合并单元格的值 |
7.1 一个容易忽略的性能瓶颈
批量处理时,我发现文档解析的速度瓶颈不在 PDF 解析,而在模型调用。每一行项目如果都调一次模型,1000 行就是 1000 次请求,耗时非常可观。优化思路是批量请求,把多个行项目文本拼成一次模型调用,每个字段用编号隔开,让模型一次返回多条结果。这个优化把批处理时间缩短了将近一半。
但批量请求也有代价:单条输出格式错误的概率会因为长度增加而上升。我的处理策略是按 5 到 10 条一组分批,并且每组之间留一条空行做分隔。实测下来,错误率和单条调用差不多,但耗时少了很多。如果你处理的数据量很大,这个优化值得做。
7.2 提示词里最容易踩的坑
提示词写不好,后面所有环节都受影响。我踩过的坑是给模型的指令里有太多“不要”句式,比如“不要输出额外解释”。模型经常对否定指令把握不准,反而更容易输出解释。后来我把提示改成肯定式表达,“仅输出符合 schema 的 JSON”,效果好很多。这个小变化让我意识到,提示词设计的逻辑是“告诉模型要什么”,而不是“告诉模型不要什么”。
另外,提示词里给的示范样例也要谨慎。模型会模仿样例的风格,但也会过度模仿样例里的具体内容。我的办法是每个字段给出一个明显不同领域的示例,比如品名的示例不要总是“口罩”,偶尔给个“轴承、润滑油、维修服务”,防止模型对示例产生偏好。
7.3 数据校验环节的优先级排序
校验环节我分了三层:结构校验、类型校验、业务规则校验。结构校验确保 JSON 字段齐全,类型校验确保日期是日期、金额是数字,业务规则校验是自定义的,比如“总价 = 数量 × 单价,误差在 0.01 元以内”。三层校验逐级执行,任何一层失败都会标记数据。业务规则校验最有实用价值,它能把解析层和抽取层的隐性错误兜出来。
举个例子,有一批数据里数量是 10,单价是 12.5,但总价却是 150 而不是 125,这种错误在类型校验里根本发现不了,只有业务规则校验能抓住。我还在校验结束后单独输出一份“异常汇总”,内容包括异常类型、异常字段、可能原因和原始文本片段。这份汇总对人工复核特别有用,不用回原文件翻找。
8. 把这套流程迁移到其他场景时,我的体会
这次实践虽然围绕采购单据展开,但组合式非结构化数据分析的思路可以平移到很多场景。比如客服工单的自动分类和字段抽取、合同关键条款的比对、简历信息的结构化录入,甚至调研问卷开放题的归类和汇总,架构上都是同一个套路:解析成文本块、语义抽取、模式映射、校验输出。
我实际迁移过一次客服工单的场景,区别主要在字段定义和提示词上。工单字段没有采购单那么明确的数值关系,业务规则校验也不太好写,我更多的还是依赖模型的语义判断。但组合式架构的优势在于,新增一个场景不需要重写解析层,只需要重新定义模式模板和映射规则,开发量小很多。
最后再分享一个小技巧:无论处理什么数据,务必保留每一份原始文件与抽取结果之间的对应关系。这样一旦出现争议,可以快速回溯到原始文本,而不是面对着结构化数据干瞪眼。我在项目里给每条抽取记录都保留了来源文件 ID 和文本块序号,这个设计在后期排查时帮了大忙。
根据我个人这次的经验,做非结构化数据处理最忌讳的就是追求一个一劳永逸的万能方案。文档形态千变万化,工具和模型也在快速迭代,组合式架构的价值恰恰在于它能以不变应万变——解析层、理解层、转换层各自演进,互不拖累。建议你在自己的项目里也试着先搭一个最小闭环,再逐步往里面加模块,实际效果会比一上来就想搭一个完美系统好得多。