space ocr
指南文章价格文档
receipts

自动从发票中提取明细行

自动从发票和收据中提取明细行并整理成结构化的行。定义一个数组字段,每个明细就成为一行可验证的数据——每行都带有边界框——并可导出为 CSV。

7 分钟阅读· 2026-06-25

发票和收据是大家最想数字化的单据,可最难啃的部分从来都不是表头。供应商名称、日期、发票号这些都是单值,OCR 模型一次就能抓出来。真正让人头疼的是中间那张表:明细行数量不固定,每行都带着品名、数量和单价,得整理成干净的行,才能用来合计、对账,再导入账簿。

本文就讲怎么用 space-ocr 自动从发票中提取明细行——不是把它压成一团文本,而是抽取成结构化数组,每一行都是独立的一条记录,而且每个单元格都能精确指回它在页面上被读取的那个位置。如果你要提取的是整份单据,而不只是表格,建议先看更全面的发票与收据 OCR 实操指南

诀窍:把明细行声明为 array 字段

大多数 OCR API 都只能让你把整张表当成一个字符串提取出来,再自己去解析。space-ocr 则允许你把明细表写进 schema 里直接描述清楚。一个带 children 列表的 type: "array" FieldSpec,等于在告诉引擎:这块区域会重复出现,每次重复都包含这几个子字段。

下面是一张收据的 schema 示例。商品("items")字段是一个数组,它的子字段分别是 商品名(name)、数量(quantity)和 単価(unit price):

fields[]——把明细行作为数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "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,依此类推。你没写任何行解析器、列拆分器,也没碰正则,只是把结构声明了一次而已。

从一张发票中提取明细行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 时,每个数组项都带着自己的 bboxverticesmatch_ratio,以及一份针对子字段的 field_bboxes 映射。收据里单独的一行长这样:

商品 数组中的一项(节选)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "商品名": "ポッカレモン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 网格上某个具体矩形里的 359xmin/ymin/xmax/ymax,原点在左上角),还带着随单据倾斜角度走向的四个 vertices 顶点,以及一个 match_ratio,告诉你这段文本到底有多少真正在页面上被找到了。match_ratio1.0 表示每个字符都定位到了;引擎把 ≥ 0.85 视为可信匹配。你可以按匹配率给提取出的行排序,只用肉眼复核最弱的那几行。完整机制可参见用边界框校验 OCR

✓ Verified

那些坐标不是模型编出来的。 语言模型返回的是每条明细行的文本,外加它用到了哪些词元(word token)的提示,但从不返回这些框。随后引擎会拿这段文本,跟视觉 OCR 在页面上实际检测到的符号逐字符比对,并用一个 match_ratio 报告每个值被找到了多少。模型给的词元提示在重复的行之间往往不太稳,所以系统不会盲目采信,而是用列一致性和行一致性检查来校验——这一点在一张 30 行、各行长得都很像的表上尤为关键。这正是让这张表可核验、而不只是看着合理的原因:每一行都带着一个分数,说明它跟页面匹配得有多好。

点一下某行,直接定位到像素

因为每条明细行都知道自己在哪儿,抽查一张表就成了点一下的事。在应用里点击任意单元格——某个描述、某个数量、某个单价——原图就会高亮出这个值的来源区域,还附上一个放大裁切。哪怕是一张三十行的发票,你的视线也能直接落到那个看起来不对劲的地方,不用整页一行行扫过去。

点击任意明细行单元格 → 原始发票上对应区域随即亮起。

从明细行到账务工具能读的 CSV

明细行一旦存进表格,导出时数组结构的优势又一次显现出来。space-ocr 在导出时会展开数组字段:表头变成 # 加上各个标量列,再为每个数组子字段各加一列,列名为 colName.childName(也就是 商品.商品名商品.数量商品.単価)。每条明细行都各自成为一条子行——一张有 10 个商品的收据生成 10 行,每行都重复带着同样的店铺名和日期。这正是电子表格和账簿导入工具想要的那种又长又扁的格式。

导出表格——数组明细行展开成每个商品一行,列名采用 colName.childName 格式。

把这张收据导出后,精简一下大致是这样:

#店舗名日付商品.商品名商品.単価
1KINSHO2019年08月17日ポッカレモン100359
2KINSHO2019年08月17日エキストラBオリー698
3KINSHO2019年08月17日シール割引-34

文件是带 BOM 的 UTF-8,所以日文、韩文和中文的商品名在 Excel 里都能正常打开。你手动改过的任何值,在导出时都会覆盖 OCR 原值,而原值依旧留存备查。关于从图片文件夹到电子表格的完整流程,参见扫描件转 CSV

几步搞定

  1. 为明细行定义一个数组字段
    在你的 fields[] schema 里加一个 type 为 "array" 的字段,并配上 children 列表,例如 description、qty、unit_price。这就等于告诉引擎:明细行区域会带着这些子字段重复出现。
  2. 把发票发送到 /ocr/fields
    把图片(以 URL 或 base64 形式)连同 imageType 和你的 fields[] 一起 POST 到 https://api.space-ocr.com/ocr/fields。数组字段会以列表形式返回,每条明细行对应一个对象。
  3. 核验每一行
    每个数组项都带着自己的 bbox、vertices 和 match_ratio。可以按 match_ratio 排序,或在应用里点击某个单元格,跳到图片上的精确区域确认值对不对。
  4. 导出为 CSV
    导出表格时,数组子字段会展开成 colName.childName 列,每条明细行各自成为一行,并重复带上单据级字段,可以直接交给你的账务工具使用。
如何自动从发票中提取明细行?
把明细表声明为一个 type 为 "array" 的字段,并配上 children 列表(例如 description、qty、unit_price),然后把图片 POST 到 /ocr/fields。引擎会把这个数组以行列表的形式返回,每条明细行对应一个对象,你无需编写任何表格解析代码。每一项还各自带有自己的边界框、vertices 和匹配率。
OCR 能处理每张发票明细行数量不固定的情况吗?
可以。数组字段并不假定固定的行数。演示里的收据解析出 10 项,另一张发票可能解析出 30 项。引擎会根据检测到的文本版面把重复的行归组,所以页面上有多少行,你就得到多少个明细行对象,而且每个都在图片上独立定位。
明细行在 CSV 导出中是怎么呈现的?
数组字段在导出时会展开。表头是 '#' 加上各个标量列,再为每个数组子字段各加一列,列名为 colName.childName(例如 items.description、items.qty、items.unit_price)。每条明细行各自成为一条子行,并重复带上供应商、日期等单据级字段,这正是账簿和电子表格导入工具想要的扁平格式。文件是带 BOM 的 UTF-8,能确保 CJK 字符在 Excel 里正常显示。
我怎么知道某条明细行被正确读取了?
每个数组项都附带一个 match_ratio(它的字符在页面上被定位到的比例)和一个边界框。match_ratio 为 1.0 表示每个字符都找到了;引擎把 0.85 及以上视为可信匹配。你可以按匹配率给行排序,只复核最弱的那几条,或者在应用里点击某个单元格,高亮出它精确的来源区域。
它能处理非英文发票吗?
可以。语言检测是自动的,日文、韩文、中文和英文都跑在同一个引擎上,连全角字符和竖排 CJK 文字也一并支持。演示提取的就是一张日文收据的 商品(items)数组,子字段为 商品名、数量 和 単価。无需设置任何语言标志。
相关文章