space ocr
ガイド記事料金ドキュメント
receipts

請求書から明細行を自動で抽出する

請求書やレシートの明細行を、構造化された行データとして自動で抽出。配列フィールドを宣言すれば、検証可能な行が1明細につき1行ずつ(それぞれにバウンディングボックス付きで)返り、そのままCSVに書き出せます。

7 分で読了· 2026-06-25

請求書やレシートはデジタル化したいという要望が最も多い書類ですが、いちばん厄介なのはヘッダー部分ではありません。取引先名、日付、請求書番号——こうした単一の値はOCRモデルが一発で拾えます。本当に大変なのは真ん中にあるテーブルです。明細行の数は書類ごとにバラバラで、各行に品名・数量・単価が並んでいて、それを合計・突合し、台帳に取り込める「きれいな行」として取り出さなければなりません。

このガイドでは、space-ocr を使って請求書から明細行を自動で抽出する方法を紹介します。フラットなテキストとしてではなく、1明細が1行になった構造化された配列として、しかも各セルが「ページ上のどこから読み取られたか」を指し示したまま取り出します。テーブルだけでなく書類全体を抽出したい場合は、まず請求書・レシートOCRの全体的な手引きから読み始めてください。

コツ: 明細行を array フィールドとして宣言する

たいていのOCR APIでは、テーブルを1本の文字列として抽出し、あとは自分でパースさせられます。space-ocr なら、明細テーブルの形をスキーマの一部として記述できます。type: "array"children リストを持つ FieldSpec が、エンジンにこう伝えます——この領域は繰り返され、繰り返しごとにこれらのサブフィールドを持つ、と。

以下は、あるレシートのスキーマ例です。商品("items")フィールドは配列で、その children は 商品名(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 /ocr/fields に投げると、配列フィールドがリストとして返ってきます。このレシートからは10件の明細行が得られます——ポッカレモン100359シール割引-34(値引き行。符号もそのまま保持)、エキストラBオリー698、といった具合です。行パーサーも、列の分割処理も、正規表現も書いていません。形を一度宣言しただけです。

1枚の請求書から明細行を抽出する
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" }
        ] }
    ]
  }'

各明細行は単独で検証できる

明細行の抽出がうまくいかなくなるのは、たいていここです——モデルが一見きれいなテーブルを返してくるのに、よく見ると微妙にずれている。価格が1行上にずれていたり、品名が下の行と混ざっていたり。space-ocr では、配列の各要素が、自身の bboxverticesmatch_ratio、そして children 用の field_bboxes マップを持っています。レシートの1行は、たとえばこんな形です。

商品 配列の1要素(抜粋)
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 のグリッド上の特定の矩形(xmin/ymin/xmax/ymax、原点は左上)に位置づけられた 359 であり、書類の傾きに沿った向きを持つ4つの vertices と、そのテキストが実際にページ上でどれだけ見つかったかを示す match_ratio を伴っています。match_ratio1.0 なら、全文字が特定できたという意味で、エンジンは ≥ 0.85 を信頼できるマッチとして扱います。抽出した行を match ratio で並べ替えて、低いものだけ目視確認すればよいのです。仕組みの詳細はバウンディングボックスによるOCR検証を参照してください。

✓ Verified

モデルはこれらの座標を作り出してはいません。 言語モデルが返すのは各明細行のテキスト——加えて、どの単語トークンを使ったかというヒント——だけで、ボックスそのものは返しません。エンジンはそのテキストを、ビジョンOCRが実際にページ上で検出したシンボルと文字単位で突き合わせ、各値がどれだけ見つかったかを match_ratio として報告します。モデルのトークンヒントは行が繰り返されると不安定になりがちなので、それを鵜呑みにせず、列方向・行方向の整合性チェックで検証します——これは、行同士が似通って見える30行のテーブルでこそ効いてきます。だからこそ、このテーブルは「もっともらしい」だけでなく検証できるものになるのです。各行は、ページとどれだけよく一致したかのスコアを携えています。

行をクリックすれば、そのピクセルへ

すべての明細行が「自分がどこにあるか」を知っているので、テーブルのスポットチェックはクリック一発になります。アプリ上で任意のセル——品名、数量、単価——をクリックすると、元画像でその値が読み取られた領域がそのまま拡大表示で強調されます。明細が30行ある請求書でも、ページ全体を見渡すのではなく、おかしく見える1行へ視線を直行させられます。

明細行のセルをクリック → 元の請求書上で対応する領域が光ります。

明細行を、会計ソフトが読めるCSVへ

明細行をいったんシートに保存すれば、配列の形が活きるのが書き出しの場面です。space-ocr は書き出し時に配列フィールドを展開します——ヘッダーは # とスカラー列に加えて、配列の child ごとに colName.childName という名前の列が1つずつ付きます(つまり 商品.商品名商品.数量商品.単価)。各明細行はそれぞれ独立したサブ行になります——10件の明細があるレシートは10行になり、いずれも同じ店舗名と日付を持ちます。これはまさに、表計算ソフトや台帳のインポーターが期待する縦長でフラットな形式です。

シートを書き出すと、配列の明細行が 1明細1行 に展開され、colName.childName 列が付きます。

このレシートを書き出すと、抜粋でこんな形になります。

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

ファイルはBOM付きのUTF-8なので、日本語・韓国語・中国語の品名もExcel(エクセル)できれいに開けます。手作業で修正した値があれば、書き出し時にはその修正がOCRの値を上書きしますが、元の値も記録として残ります。画像フォルダから表計算までの一連の流れは、スキャン書類をCSVへを参照してください。

数ステップでやってみる

  1. 明細行用の配列フィールドを定義する
    fields[] スキーマに、type "array" と children リスト(例: description, qty, unit_price)を持つフィールドを追加します。これでエンジンに、明細行の領域がそれらのサブフィールドとともに繰り返されることを伝えられます。
  2. 請求書を /ocr/fields に送る
    画像(URL または base64)を imageType とあなたの fields[] とともに https://api.space-ocr.com/ocr/fields に POST します。配列フィールドが、1明細につき1オブジェクトのリストとして返ってきます。
  3. 各行を検証する
    配列の各要素は、それぞれの bbox・vertices・match_ratio を持っています。match_ratio で並べ替えるか、アプリでセルをクリックして画像上の正確な領域へ飛び、値を確認します。
  4. CSVへ書き出す
    シートを書き出すと、配列の children が colName.childName 列に展開され、各明細行がそれぞれ独立した行になり、書類レベルのフィールドが繰り返されます。そのまま会計ソフトに取り込める形です。
請求書から明細行を自動で抽出するにはどうすればいいですか?
明細テーブルを type "array" と children リスト(例: description, qty, unit_price)を持つフィールドとして宣言し、画像を /ocr/fields に POST します。エンジンは配列を行のリスト——1明細につき1オブジェクト——として返すので、テーブルをパースするコードを書く必要はありません。各要素はそれぞれのバウンディングボックス・vertices・マッチ率も持っています。
請求書ごとに明細行の数が違っても、OCRで対応できますか?
はい。配列フィールドは行数を固定で仮定しません。デモのレシートからは10件、別の請求書なら30件返ってくることもあります。エンジンは検出したテキストのレイアウトから繰り返しの行をグループ化するので、ページにある行数ぶんだけ明細オブジェクトが得られ、それぞれが画像上で単独に位置づけられます。
明細行はCSV書き出しでどう表示されますか?
配列フィールドは書き出し時に展開されます。ヘッダーは '#' とスカラー列に加えて、配列の child ごとに colName.childName という名前の列が1つずつ付きます(例: items.description, items.qty, items.unit_price)。各明細行はそれぞれ独立したサブ行になり、取引先や日付といった書類レベルのフィールドが繰り返されます。これは台帳や表計算のインポーターが期待するフラットな形式です。ファイルはBOM付きのUTF-8なので、ExcelでCJK(日中韓)の文字もきれいに開けます。
明細行が正しく読み取れたかは、どうやって分かりますか?
配列の各要素には match_ratio(その値の文字のうちページ上で特定できた割合)とバウンディングボックスが付いています。match_ratio が 1.0 なら全文字が見つかったという意味で、エンジンは 0.85 以上を信頼できるマッチとして扱います。行を match ratio で並べ替えて低いものだけ確認したり、アプリでセルをクリックして、その値が来た領域を正確に強調表示したりできます。
英語以外の請求書でも動きますか?
はい。言語の検出は自動です——日本語・韓国語・中国語・英語が1つのエンジンで処理され、全角文字や縦書きのCJKテキストにも対応します。デモでは、日本語のレシートの 商品(items)配列を、商品名・数量・単価 の children とともに抽出しています。設定すべき言語フラグはありません。
関連記事