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

Pythonによる請求書解析:REST API活用ガイド

Pythonを使って請求書から構造化データを抽出。シンプルなREST APIを呼び出し、あらゆる請求書画像からJSON形式の項目、明細、そして検証可能な座標情報を取得する手順を解説します。

7 分で読了· 2026-08-31

請求書から構造化データを抽出するのは、よくある作業ですが、手間がかかりがちです。スキャンした領収書や取引先の請求書がJPEGやPNGで大量にあり、そこから請求書番号、合計金額、各明細項目を経理や分析のために抜き出す必要がある、といった状況です。多くのOCRツールはテキストの羅列を出力するだけで、結局は不安定な正規表現を駆使して構造を復元するしかありません。

もっと直接的な方法があります。それは、必要な構造化JSONを返すREST APIを呼び出すことです。このAPIを使えば、抽出された全ての値が、元の帳票上のどの位置にあるのかまで正確に追跡できます。以下のインタラクティブデモがその最終結果です。左側の項目をクリックすると、請求書上で該当箇所がハイライトされます。

Invoice with extracted-field bounding boxes
Verified fields
Invoice

Each value with a box carries a verified on-page location — in data.cells[path], that is box + 4-point quad + evidence.match_ratio — on a 0–1000 normalized grid (0,0 top-left → 1000,1000 bottom-right), the same shape the live API returns. Hover a field to trace it back to the pixels it came from.

OCR処理の対象となる請求書のサンプル
請求書画像 — これが/ocr/fieldsへの入力となります。

space-ocrはインストールするライブラリではなく、純粋なHTTPサービスです。手順はシンプルです。請求書画像をPOST /ocr/fieldsエンドポイントに送信し、取得したいデータのスキーマを定義するだけです。invoice_numberやtotal_dueといった項目や、品名と価格の列を持つline_itemsテーブルなどを指定できます。APIは宣言したスキーマそのままのJSONをdata.valuesに返し、座標や検証結果は同階層の別キーに分けて格納します。

抽出された全ての値は、元画像上のバウンディングボックス(位置座標)に紐付けられており、検証可能な監査証跡を提供します。

レスポンスはデータと検証結果を別々の場所に分けて返します。data.valuesは宣言したスキーマそのもので、invoice_numberの"20250430-001"も定義した位置にそのまま入ります。data.cellsはパスをキーにしたフラットなマップで、cells["total_due"]にはその値の紙面上のboxとquad、そしてverified判定が入ります。data.review.flaggedは、もう一度確認する価値のあるパスをreasons付きで並べた短いリストです。下記のスキーマは日付と数値を宣言しているため、data.normalizedにも解析済みの値が返り、values側は読み取ったままの表記を保持します。以下のPythonスクリプトは、これらを順に取り出します。

請求書解析スクリプト (parse_invoice.py)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import os
import base64
import json
import requests

# Get your API key from an environment variable
API_KEY = os.environ.get("SPACE_OCR_API_KEY")
IMAGE_PATH = "./path/to/your/invoice.jpg" # Path to your invoice image
API_URL = "https://api.space-ocr.com/ocr/fields"

if not API_KEY:
    raise ValueError("API key not found. Set the SPACE_OCR_API_KEY environment variable.")

# Declare the schema you want back.
# 'type' adds a deterministic parse under data.normalized; 'required' and 'pattern'
# are checked after extraction and show up in the review list when they fail.
# Declarations never reach the model: they do not change the value, only the signals.
fields_schema = [
    {"name": "supplier", "type": "string", "description": "The name of the company that issued the invoice."},
    {"name": "invoice_number", "type": "string", "required": True, "pattern": "^[0-9]{8}-[0-9]{3}$"},
    {"name": "issue_date", "type": "date", "required": True},
    {"name": "total_due", "type": "number", "required": True},
    {
        "name": "line_items",
        "type": "array",
        "description": "All items listed in the invoice table.",
        "children": [
            {"name": "item_description", "type": "string"},
            {"name": "unit_price", "type": "number"},
            {"name": "quantity", "type": "integer"},
            {"name": "line_total", "type": "number"}
        ]
    }
]

# Read the image file and encode it in base64
with open(IMAGE_PATH, "rb") as image_file:
    base64_image = base64.b64encode(image_file.read()).decode("utf-8")

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

payload = {
    "image": base64_image,
    "imageType": "base64",
    "fields": fields_schema
}

print(f"Sending request for {IMAGE_PATH}...")
response = requests.post(API_URL, headers=headers, json=payload)

if response.status_code != 200:
    # Errors use one envelope: {"error": {"code", "message", "requestId"}, "details": {...}}
    try:
        err = response.json()["error"]
        print(f"HTTP {response.status_code} {err['code']}: {err['message']} (requestId {err['requestId']})")
    except (ValueError, KeyError):
        # A body over 32MiB is cut off before the app sees it and comes back as HTML
        print(f"HTTP {response.status_code}: {response.text[:200]}")
    if response.status_code == 429:
        print(f"Rate limited - Retry-After: {response.headers.get('Retry-After')} seconds")
    # 400 invalid_image is not charged and a retry will not help; 502 is refunded and worth
    # retrying; 504 means the 180s sync ceiling was hit, so send that page on its own.
    raise SystemExit(1)

data = response.json()["data"]
values = data["values"]                  # your schema, and nothing else
cells = data["cells"]                    # flat map: path -> box / quad / verified / review / evidence
review = data["review"]                  # what is worth a second look
normalized = data.get("normalized", {})  # deterministic parse of the types you declared
image = data["image"]                    # {"width", "height"} - the frame the coordinates use

print("\n--- Extracted Invoice Data ---")
print(json.dumps(values, ensure_ascii=False, indent=2))

# 'values' keeps the text as it was read; the parsed form lives here.
# A null leaf means the parse failed - the reason sits in cells[path]["normalized"]["error"].
print("\nissue_date parsed:", normalized.get("issue_date"))
print("total_due parsed :", normalized.get("total_due"))

# The review queue: every entry carries a path and a rank-ordered reasons array.
print(f"\n{len(review['flagged'])} of {review['declared']} declared fields were flagged:")
for item in review["flagged"]:
    path = item["path"]
    reasons = item["reasons"]      # reasons[0] is the primary one
    cell = cells.get(path)         # 'missing' and 'nobox' have no cell to point at
    box = cell["box"] if cell else None
    print(f"  {path}: {reasons[0]} ({', '.join(reasons)}) box={box}")

# Where a single value came from
total = cells.get("total_due")
if total:
    print("\ntotal_due")
    print("  box     :", total["box"])       # {"xmin", "ymin", "xmax", "ymax"}, 0-1000
    print("  quad    :", total["quad"])      # four {"x", "y"} points, following the page tilt
    print("  verified:", total["verified"])  # False if flagged, True if a check ran clean, None if there was nothing to check
    print("  evidence:", total["evidence"])  # text_match, source, match_ratio, printed_text ...
    left = total["box"]["xmin"] / 1000 * image["width"]
    top = total["box"]["ymin"] / 1000 * image["height"]
    print(f"  pixels  : ({left:.0f}, {top:.0f}) on a {image['width']}x{image['height']} page")

# Line items: values holds the rows, cells is keyed by the indexed path
print("\nline_items")
for i, row in enumerate(values.get("line_items") or []):
    cell = cells.get(f"line_items[{i}].line_total")
    verified = cell["verified"] if cell else None
    print(f"  row {i}: {row.get('item_description')} x{row.get('quantity')} "
          f"= {row.get('line_total')} (verified={verified})")
✓ Verified

space-ocrは、精度を保つために言語モデルの出力をそのまま信用することはありません。モデルはテキスト値に加え、ページ上のどの単語を使用したかのヒントを返します。次に、エンジンがその抽出値をページ上で実際にOCR認識された文字群と一文字ずつ照合します。この照合が信頼度スコアmatch_ratio(0.85以上で高信頼度の一致)と座標を生成し、いずれもdata.cells[path]に格納されます。boxはxmin・ymin・xmax・ymaxの整数キー、quadは紙面の傾きに追従する4点です。座標はすべて0〜1000に正規化されているため元画像の解像度に依存せず、ピクセル換算にはdata.imageのページ寸法を使います。照合そのものの結果はevidence.text_matchに入り、verifiedはセル全体の判定です。宣言した規則への違反を含め、何か指摘が立てばfalseになります。

APIの料金は、項目数やページの複雑さではなく、リクエストごとに課金されます。画像1枚あたり¥10で、結果の生成に失敗したリクエストは課金対象外です。新規アカウントには、毎月100回分の無料スキャンが付与されます。

  1. APIキーの取得
    space-ocrの無料アカウントにサインアップし、ダッシュボードの設定画面でAPIキーを確認します。キーは'spocr_'で始まります。
  2. 請求書画像の準備
    請求書をJPEGやPNGなどの一般的な画像ファイルとして保存します。スクリプトから参照できるよう、ファイルパスを控えておきます。
  3. データスキーマの定義
    Pythonスクリプト内で、抽出したい項目の名前と型を定義するJSON配列を作成します。明細テーブルもここで定義します。
  4. Pythonスクリプトの作成
    requestsライブラリを使い、画像を読み込んでbase64に変換し、AuthorizationヘッダーにAPIキーを設定してhttps://api.space-ocr.com/ocr/fieldsにPOSTするスクリプトを作成します。
  5. 実行とJSONの処理
    スクリプトを実行します。`data.values`をデータベースや会計ソフト、分析ツールへ取り込み、`data.review.flagged`に挙がった値だけを先に確認します。
SDKやライブラリのインストールは必要ですか?
いいえ、space-ocrは標準的なHTTP RESTサービスです。cURLやPythonのrequestsライブラリなど、HTTPリクエストが可能なあらゆる言語やツールから呼び出すことができます。
PDFの請求書は処理できますか?
APIエンドポイントはJPEG、PNG、WebPなどのラスター画像形式を受け付けます。PDFファイルを処理するには、APIに送信する前に、まずご自身のコードで各ページを画像にレンダリングする必要があります。space-ocrのWebアプリケーションでは、この変換は自動的に行われます。
レスポンスに含まれる座標情報について教えてください。
APIは0から1000の範囲で正規化された座標を返します。左上が(0,0)、右下が(1000,1000)です。`data.cells`の各エントリには、`xmin`・`ymin`・`xmax`・`ymax`という整数キーを持つ`box`と、紙面の傾きに追従する4点の`quad`が並んで入ります。ピクセルに換算する際は、読み取り時のページ寸法を示す`data.image`(width / height)を基準にしてください。
明細項目やテーブルはどのように扱いますか?
fieldsスキーマで、`type: 'array'`を持つ項目を定義します。次に、ネストされた`children`プロパティの中に、各行から抽出したい列を指定します。APIはその項目に対してオブジェクトの配列を返します。
事前に請求書のレイアウトがわからない場合はどうすればよいですか?
決まったスキーマを事前に用意できない場合は、`fields`を渡す代わりにリクエストボディで`autoFields: true`を設定できます。サービスが文書の構造を検出し、見つかった項目を返します。返ってきた項目名が分かったら、以降は`fields`で明示的に宣言しておくと安定します。
どの値を人手で確認すべきか、どう判断しますか?
`data.review.flagged`を見てください。各エントリには`path`と、ランク順に並んだ`reasons`配列(先頭が代表的な理由)が入り、`missing`・`text_mismatch`・`pattern_mismatch`といったコードが指摘の理由を示します。件数は`flagged`の長さそのもので、別のカウンタはありません。値ごとに見る場合は`cells[path].verified`が同じ判定を真偽値で返し、文字レベルの詳細は`cells[path].evidence`に入ります(`match_ratio`は0.0〜1.0で、0.85以上を信頼度の高い一致とみなします)。二つのエンジンが同じ誤読で一致した値はそのまま通ることもあるため、この一覧は目視確認をなくすものではなく、範囲を絞るためのものです。

わずか数分で請求書の解析を始めましょう。

APIキーを取得して、毎月100回分の無料スキャンをご利用ください。クレジットカードの登録は不要です。

関連記事