請求書から明細行を自動で抽出する
請求書やレシートの明細行を、構造化された行データとして自動で抽出。配列フィールドを宣言すれば、検証可能な行が1明細につき1行ずつ(それぞれにバウンディングボックス付きで)返り、そのままCSVに書き出せます。
請求書やレシートはデジタル化したいという要望が最も多い書類ですが、いちばん厄介なのはヘッダー部分ではありません。取引先名、日付、請求書番号——こうした単一の値はOCRモデルが一発で拾えます。本当に大変なのは真ん中にあるテーブルです。明細行の数は書類ごとにバラバラで、各行に品名・数量・単価が並んでいて、それを合計・突合し、台帳に取り込める「きれいな行」として取り出さなければなりません。
このガイドでは、space-ocr を使って請求書から明細行を自動で抽出する方法を紹介します。フラットなテキストとしてではなく、1明細が1行になった構造化された配列として、しかも各セルが「ページ上のどこから読み取られたか」を指し示したまま取り出します。テーブルだけでなく書類全体を抽出したい場合は、まず請求書・レシートOCRの全体的な手引きから読み始めてください。
コツ: 明細行を array フィールドとして宣言する
たいていのOCR APIでは、テーブルを1本の文字列として抽出し、あとは自分でパースさせられます。space-ocr なら、明細テーブルの形をスキーマの一部として記述できます。type: "array" と children リストを持つ FieldSpec が、エンジンにこう伝えます——この領域は繰り返され、繰り返しごとにこれらのサブフィールドを持つ、と。
以下は、あるレシートのスキーマ例です。商品("items")フィールドは配列で、その children は 商品名(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 /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" }
] }
]
}'各明細行は単独で検証できる
明細行の抽出がうまくいかなくなるのは、たいていここです——モデルが一見きれいなテーブルを返してくるのに、よく見ると微妙にずれている。価格が1行上にずれていたり、品名が下の行と混ざっていたり。space-ocr では、配列の各要素が、自身の bbox・vertices・match_ratio、そして children 用の field_bboxes マップを持っています。レシートの1行は、たとえばこんな形です。
{
"商品名": "ポッカレモン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 であり、書類の傾きに沿った向きを持つ4つの vertices と、そのテキストが実際にページ上でどれだけ見つかったかを示す match_ratio を伴っています。match_ratio が 1.0 なら、全文字が特定できたという意味で、エンジンは ≥ 0.85 を信頼できるマッチとして扱います。抽出した行を match ratio で並べ替えて、低いものだけ目視確認すればよいのです。仕組みの詳細はバウンディングボックスによるOCR検証を参照してください。
モデルはこれらの座標を作り出してはいません。 言語モデルが返すのは各明細行のテキスト——加えて、どの単語トークンを使ったかというヒント——だけで、ボックスそのものは返しません。エンジンはそのテキストを、ビジョンOCRが実際にページ上で検出したシンボルと文字単位で突き合わせ、各値がどれだけ見つかったかを match_ratio として報告します。モデルのトークンヒントは行が繰り返されると不安定になりがちなので、それを鵜呑みにせず、列方向・行方向の整合性チェックで検証します——これは、行同士が似通って見える30行のテーブルでこそ効いてきます。だからこそ、このテーブルは「もっともらしい」だけでなく検証できるものになるのです。各行は、ページとどれだけよく一致したかのスコアを携えています。
行をクリックすれば、そのピクセルへ
すべての明細行が「自分がどこにあるか」を知っているので、テーブルのスポットチェックはクリック一発になります。アプリ上で任意のセル——品名、数量、単価——をクリックすると、元画像でその値が読み取られた領域がそのまま拡大表示で強調されます。明細が30行ある請求書でも、ページ全体を見渡すのではなく、おかしく見える1行へ視線を直行させられます。
明細行を、会計ソフトが読めるCSVへ
明細行をいったんシートに保存すれば、配列の形が活きるのが書き出しの場面です。space-ocr は書き出し時に配列フィールドを展開します——ヘッダーは # とスカラー列に加えて、配列の child ごとに colName.childName という名前の列が1つずつ付きます(つまり 商品.商品名・商品.数量・商品.単価)。各明細行はそれぞれ独立したサブ行になります——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[] スキーマに、type "array" と children リスト(例: description, qty, unit_price)を持つフィールドを追加します。これでエンジンに、明細行の領域がそれらのサブフィールドとともに繰り返されることを伝えられます。
- 請求書を /ocr/fields に送る画像(URL または base64)を imageType とあなたの fields[] とともに https://api.space-ocr.com/ocr/fields に POST します。配列フィールドが、1明細につき1オブジェクトのリストとして返ってきます。
- 各行を検証する配列の各要素は、それぞれの bbox・vertices・match_ratio を持っています。match_ratio で並べ替えるか、アプリでセルをクリックして画像上の正確な領域へ飛び、値を確認します。
- CSVへ書き出すシートを書き出すと、配列の children が colName.childName 列に展開され、各明細行がそれぞれ独立した行になり、書類レベルのフィールドが繰り返されます。そのまま会計ソフトに取り込める形です。