一、问题的发现
做财务系统时,发票解析是硬需求:上传 PDF/图片,自动抽出购销方、价税合计、明细行(品名、规格、数量、单价、金额、税率、税额)。我接入的是阿里云官方 RecognizeInvoice(增值税发票识别)。抬头、价税合计这类字段通常很稳;真正难的是 明细行特别多的电子发票——一页四五十行货品时,接口会把多行「粘」成一行。
本文记录的是:这个问题怎么暴露、我走过哪些弯路、最后为什么用「安全空隙对半识别」解决,以及方案里的几个关键设计点。
二、现象:什么叫「粘行」
正常明细大致是:
| 教具某某商品A | 1 | 12.00 | 12.00 |
| 教具某某商品B | 1 | 8.50 | 8.50 |
粘行后,同一条 invoiceDetails 里会出现:
- 品名:多个税收分类前缀连在一起,例如 *教具*A…*教具*B…
- 数量:数字被拼成长串,例如 111111135226
- 单价:一个字段里出现多个小数点
业务上很致命:不是「识别差一点」,而是 行边界丢了。后面无论怎么映射字段、怎么展示表格,货品行数和金额对不上。
三、定位:先证明不是自己写错了
出问题后很容易怀疑:
我做了对照实验(同一账号、同一接口):
| 整页密集 PDF / 整页 PNG | 粘行 |
| 同一张发票里 明细稀疏的一页 | 正常 |
| 把密集页 人为裁成上下半页 再识别 | 粘行消失 |
结论很明确:
不是接入问题,是模型在「单次视觉密度过高」时行切分失败。
密度降下来(接近稀疏页),RecognizeInvoice 就恢复正常。
这决定了后续方向:修输入形态,而不是修输出 JSON。
四、走过的弯路
1. 后处理丢弃「脏行」
写规则检测:多个 *分类*、数量位数异常、单价多个小数点 → 直接丢掉。
问题:脏行里往往塞着 好几行真实货品。丢掉等于少记成本,财务场景不可接受。
2. 正则「拆开」粘在一起的品名
看起来能救一部分,但品名、规格、数量纠缠在一起,规则极脆,边界 case 无穷。而且本质是在 猜测模型已经弄丢的行边界,治标不治本。
3. 固定中线裁切 / 选「更好」的半页结果
固定对半裁切,容易 横切半行,造出残片品名、残片金额。
若再用「行数更少 / 更干净」的结果当赢家,还会误删上半页真实明细。
4. 用 VL 模型先裁「发票区域」
实测框几乎就是整页,密度没降,粘行依旧。
这些弯路共同指向一个原则:
行切分错误发生在 OCR 内部;后处理只能掩盖,不能恢复信息。
五、最终方案:安全空隙对半 → 双次 RecognizeInvoice → 接缝指纹去重
管道命名:
page_split_halves → aliyun_RecognizeInvoice
单页流程:
PDF / 图片
→ 渲染高清 PNG
→ 在「两行之间」找安全落刀点(文字层优先,墨迹密度兜底)
→ 裁上半带 + 下半带(接缝极小重叠)
→ 分别调用 RecognizeInvoice
→ 指纹匹配,剥掉接缝重复行
→ 抬头以上半为主,缺字段用下半补
多页再:选信息最全的抬头 + 按页顺序拼接明细
核心思想只有两点:
六、三个关键设计点
1. 安全空隙怎么找?
有 PDF 文字层时(首选)
- 取文字块的 Y 坐标
- 优先带 * 的词(增值税明细常见 *税收分类*品名)
- 在相邻两行之间找空隙
- 空隙太窄(如 < 4pt)跳过,避免切到字
- 落刀点限制在页面中部附近(避免切到抬头/价税合计区)
- 打分:空隙大小 – 偏离目标中线的距离 × 权重 → 既要空隙大,又尽量靠近页中
无文字层 / 纯图片(兜底)
- 把图缩小,按行统计「墨迹密度」
- 平滑后,在页中附近找最「空」的一行当落刀点
这样就把「对半」从粗糙的几何中线,升级成 语义上的行间切割。
2. 接缝重叠怎么控?
对半之后,接缝处需要一点点重叠,防止漏行;但重叠太大,重复行又变多。我的策略:
- 默认重叠约 1% 页高
- 若已知文字空隙高度,重叠再压到 不超过空隙的 1/3
- 硬上限很小(几个百分点量级)
目标:重叠只活在「空隙」里,不侵入上下两行正文。
3. 重叠去重怎么避免误删真重复货品?
发票里完全可能出现「同品名、同数量」的多行真实明细。
如果做全局 Set 去重,会误删。
我们的 merge_split_details:
这保证了:
- 只清理「接缝区」的重复
- 同页后面真实重复货品不会被误杀
- 一条上半行不会连环删掉多条下半行
七、效果与代价
在密集样张上的对照(示意):
| 整页一次识别 | 出现灾难性粘行 |
| 安全空隙对半 + 接缝去重 | 粘行消失,行数回到可用区间 |
代价也很诚实:
- 每页约 2 次 RecognizeInvoice 调用(成本与时延翻倍)
- 需要维护裁切、空隙检测、接缝去重逻辑
对我们这种「财务明细必须准」的场景,这个 trade-off 是划算的:多花一次 OCR,换的是可入账的结构化明细。
另外补一层可观测性:每页记录 split_ratio、空隙检测方法、上下半行数、合并后行数、丢弃的重叠行。出问题能直接看是「切歪了」还是「去重误伤」。
八、架构位置(系统视角)
前端上传 → Java 业务服务 → Python 解析服务 /parse/invoice
PDF → 逐页安全空隙对半识别
图片 → 文字层/墨迹找空隙后对半
OFD → 受格式限制,暂走单次识别
→ 映射为业务 JSON(抬头 + items + 表格 blocks) → 用户确认后入库
工程上刻意把发票解析做成独立服务:裁切、OCR、合并都在解析层完成,业务侧只消费「一行一项」的结果。
九、可沉淀的方法论
这事对我后续做 OCR/文档结构化很有启发:
一句话收束:
粘行不是字段映射 bug,是高密度下的行切分失败;
解法是把一页拆成模型能吃得动的两段,并且保证刀落在行与行之间。
网硕互联帮助中心






评论前必须登录!
注册