自动从发票中提取明细行
自动从发票和收据中提取明细行并整理成结构化的行。定义一个数组字段,每个明细就成为一行可验证的数据——每行都带有边界框——并可导出为 CSV。
发票和收据是大家最想数字化的单据,可最难啃的部分从来都不是表头。供应商名称、日期、发票号这些都是单值,OCR 模型一次就能抓出来。真正让人头疼的是中间那张表:明细行数量不固定,每行都带着品名、数量和单价,得整理成干净的行,才能用来合计、对账,再导入账簿。
本文就讲怎么用 space-ocr 自动从发票中提取明细行——不是把它压成一团文本,而是抽取成结构化数组,每一行都是独立的一条记录,而且每个单元格都能精确指回它在页面上被读取的那个位置。如果你要提取的是整份单据,而不只是表格,建议先看更全面的发票与收据 OCR 实操指南。
诀窍:把明细行声明为 array 字段
大多数 OCR API 都只能让你把整张表当成一个字符串提取出来,再自己去解析。space-ocr 则允许你把明细表写进 schema 里直接描述清楚。一个带 children 列表的 type: "array" FieldSpec,等于在告诉引擎:这块区域会重复出现,每次重复都包含这几个子字段。
下面是一张收据的 schema 示例。商品("items")字段是一个数组,它的子字段分别是 商品名(name)、数量(quantity)和 単価(unit price):
{
"fields": [
{ "name": "店舗名", "type": "string", "description": "store name" },
{ "name": "日付", "type": "string", "description": "date" },
{ "name": "合計", "type": "string", "description": "total" },
{
"name": "商品",
"type": "array",
"description": "one row per line item",
"children": [
{ "name": "商品名", "type": "string", "description": "item name" },
{ "name": "数量", "type": "string", "description": "quantity" },
{ "name": "単価", "type": "string", "description": "unit price" }
]
}
]
}把它连同图片一起 POST 到 POST /ocr/fields,数组字段就会以列表形式返回。这张收据解析出 10 条明细行:ポッカレモン100 价格 359,シール割引 价格 -34(折扣行,正负号原样保留),エキストラBオリー 价格 698,依此类推。你没写任何行解析器、列拆分器,也没碰正则,只是把结构声明了一次而已。
curl -s https://api.space-ocr.com/ocr/fields \
-H "Authorization: Bearer $SPACE_OCR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://example.com/receipt.jpg",
"imageType": "url",
"fields": [
{ "name": "total", "type": "string" },
{ "name": "items", "type": "array",
"children": [
{ "name": "description", "type": "string" },
{ "name": "qty", "type": "string" },
{ "name": "unit_price", "type": "string" }
] }
]
}'每条明细行都能独立核验
明细行提取通常就栽在这里:模型返回了一张看似工整的表,实则有细微错位——某个价格往上串了一行,某条描述跟下面那条粘到了一起。而用 space-ocr 时,每个数组项都带着自己的 bbox、vertices、match_ratio,以及一份针对子字段的 field_bboxes 映射。收据里单独的一行长这样:
{
"商品名": "ポッカレモン100",
"単価": "359",
"match_ratio": 1.0,
"bbox_source": "vision_symbol_match",
"field_bboxes": {
"単価": {
"bbox": { "xmin": 450, "ymin": 356, "xmax": 484, "ymax": 378 },
"vertices": [
{ "x": 450, "y": 360 }, { "x": 483, "y": 356 },
{ "x": 485, "y": 374 }, { "x": 452, "y": 378 }
],
"match_ratio": 1.0
}
}
}所以一个价格并不只是 359——它是落在 0–1000 normalized 网格上某个具体矩形里的 359(xmin/ymin/xmax/ymax,原点在左上角),还带着随单据倾斜角度走向的四个 vertices 顶点,以及一个 match_ratio,告诉你这段文本到底有多少真正在页面上被找到了。match_ratio 为 1.0 表示每个字符都定位到了;引擎把 ≥ 0.85 视为可信匹配。你可以按匹配率给提取出的行排序,只用肉眼复核最弱的那几行。完整机制可参见用边界框校验 OCR。
那些坐标不是模型编出来的。 语言模型返回的是每条明细行的文本,外加它用到了哪些词元(word token)的提示,但从不返回这些框。随后引擎会拿这段文本,跟视觉 OCR 在页面上实际检测到的符号逐字符比对,并用一个 match_ratio 报告每个值被找到了多少。模型给的词元提示在重复的行之间往往不太稳,所以系统不会盲目采信,而是用列一致性和行一致性检查来校验——这一点在一张 30 行、各行长得都很像的表上尤为关键。这正是让这张表可核验、而不只是看着合理的原因:每一行都带着一个分数,说明它跟页面匹配得有多好。
点一下某行,直接定位到像素
因为每条明细行都知道自己在哪儿,抽查一张表就成了点一下的事。在应用里点击任意单元格——某个描述、某个数量、某个单价——原图就会高亮出这个值的来源区域,还附上一个放大裁切。哪怕是一张三十行的发票,你的视线也能直接落到那个看起来不对劲的地方,不用整页一行行扫过去。
从明细行到账务工具能读的 CSV
明细行一旦存进表格,导出时数组结构的优势又一次显现出来。space-ocr 在导出时会展开数组字段:表头变成 # 加上各个标量列,再为每个数组子字段各加一列,列名为 colName.childName(也就是 商品.商品名、商品.数量、商品.単価)。每条明细行都各自成为一条子行——一张有 10 个商品的收据生成 10 行,每行都重复带着同样的店铺名和日期。这正是电子表格和账簿导入工具想要的那种又长又扁的格式。
把这张收据导出后,精简一下大致是这样:
| # | 店舗名 | 日付 | 商品.商品名 | 商品.単価 |
|---|---|---|---|---|
| 1 | KINSHO | 2019年08月17日 | ポッカレモン100 | 359 |
| 2 | KINSHO | 2019年08月17日 | エキストラBオリー | 698 |
| 3 | KINSHO | 2019年08月17日 | シール割引 | -34 |
文件是带 BOM 的 UTF-8,所以日文、韩文和中文的商品名在 Excel 里都能正常打开。你手动改过的任何值,在导出时都会覆盖 OCR 原值,而原值依旧留存备查。关于从图片文件夹到电子表格的完整流程,参见扫描件转 CSV。
几步搞定
- 为明细行定义一个数组字段在你的 fields[] schema 里加一个 type 为 "array" 的字段,并配上 children 列表,例如 description、qty、unit_price。这就等于告诉引擎:明细行区域会带着这些子字段重复出现。
- 把发票发送到 /ocr/fields把图片(以 URL 或 base64 形式)连同 imageType 和你的 fields[] 一起 POST 到 https://api.space-ocr.com/ocr/fields。数组字段会以列表形式返回,每条明细行对应一个对象。
- 核验每一行每个数组项都带着自己的 bbox、vertices 和 match_ratio。可以按 match_ratio 排序,或在应用里点击某个单元格,跳到图片上的精确区域确认值对不对。
- 导出为 CSV导出表格时,数组子字段会展开成 colName.childName 列,每条明细行各自成为一行,并重复带上单据级字段,可以直接交给你的账务工具使用。