自動從發票擷取明細品項
自動從發票與收據擷取明細品項並整理成結構化的列。定義一個陣列欄位,每個品項就成為一列可驗證的資料——每列都附帶定界框——並可匯出成 CSV。
發票和收據是大家最想數位化的文件,可是最難搞定的從來都不是表頭。廠商名稱、日期、發票號碼這些都是單一的值,OCR 模型一次就能抓到。真正麻煩的是中間那張表:明細品項數量不固定,每一項都有品名、數量和金額,得整理成乾淨的資料列,才能加總、對帳,再匯入帳本。
這份指南會示範怎麼用 space-ocr 自動從發票擷取明細品項——不是攤平成一團文字,而是抽成結構化的陣列,每一行都是獨立的一列,而且每一格都還能對應回它在頁面上被讀取的精確位置。如果你要擷取的是整份文件,而不只是表格,請先從更完整的發票與收據 OCR 教學開始。
訣竅:把明細品項宣告成 array 欄位
大多數 OCR API 都只能讓你把表格當成一整串字串擷取出來,再自己去解析。space-ocr 則讓你直接在 schema 裡就把明細表描述清楚。一個帶有 children 清單的 type: "array" FieldSpec,等於在告訴引擎:這個區域會重複出現,而且每次重複都包含這些子欄位。
以下是一張收據的 schema 範例。商品(「items」)欄位是一個陣列,它的 children 是 商品名(品名)、数量(數量)和 単価(單價):
{
"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,還有一份對應其 children 的 field_bboxes map。收據裡單獨一行長這樣:
{
"商品名": "ポッカレモン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 座標格(xmin/ymin/xmax/ymax,原點在左上角)上某個特定矩形裡的 359,附帶四個會跟著文件傾斜角度走的 vertices,以及一個 match_ratio,告訴你這段文字實際上有多少在頁面上被找到。match_ratio 為 1.0 代表每一個字元都定位到了;引擎把 ≥ 0.85 視為可信的比對。你可以依比對比率把擷取出來的資料列排序,只用肉眼檢查最弱的那幾筆。完整的運作機制請參考用邊界框驗證 OCR。
那些座標不是模型編出來的。語言模型回傳的是每個明細品項的文字,外加它用了哪些 word token 的提示,但從不回傳框。接著由引擎拿這段文字,去和視覺 OCR 在頁面上實際偵測到的符號做逐字元比對,並針對每個值回報一個 match_ratio,表示它找到了多少。模型給的 token 提示在重複的資料列之間往往不太穩,所以系統不會盲目信任,而是用欄一致性與列一致性檢查去驗證——這一點在 30 列、每行看起來都很像的表格上尤其重要。這就是讓表格可被檢查、而不只是看起來合理的關鍵:每一行都帶著一個分數,說明它和頁面比對得有多好。
點一下某一行,直接落在像素上
因為每個明細品項都知道自己在哪裡,抽查一張表就變成點一下的事。在 app 裡點任何一格——品名、數量、單價——原始圖片就會把這個值的來源區域精確地高亮起來,還附上放大裁切。就算是一張三十行的發票,你的視線也能直接落在看起來不對勁的那一行,不必整頁一行行掃過去。
從明細品項到會計工具讀得懂的 CSV
明細品項一旦存進工作表,匯出時陣列結構的好處又派上用場了。space-ocr 會在匯出時把陣列欄位展開:表頭變成 # 加上各個純量欄位,再為每個陣列 child 各加一欄,欄名為 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 排序,或在 app 裡點某一格,跳到圖片上的精確區域確認該值對不對。
- 匯出成 CSV匯出工作表時,陣列 children 會展開成 colName.childName 欄位,每個明細品項各自成為一列,並重複帶上文件層級的欄位,直接就能餵給你的會計工具。