この記事でできるようになること
Google AI Studioでキーを発行し、Pythonからpdfや写真をGemini APIに渡して、発行日・金額・取引先のような項目をJSONで受け取れるところまでを書きます。読み取った結果はそのまま登録せず、人が確認してから使う前提です。
対象は、請求書や領収書、点検記録などの紙・画像をExcelやシステムに手入力している担当者です。コードはそのまま動く形で載せますが、実際に動かすと詰まる場所が5つほどあるので、そこに多めに紙面を使います。
APIキーの発行と最初の1回
最初の1回は、キー発行と課金設定さえ済ませれば数分で終わります。
- Google AI StudioにGoogleアカウントでログインし、APIキーを発行します。この時点でプロジェクトが自動作成されます。
- 発行したキーを環境変数に置きます。ターミナルで
export GEMINI_API_KEY="発行したキー"のように設定します。コードにキーを直接書き込まないようにします。 - ライブラリをインストールします。
pip install google-genaiを実行します。古いgoogle-generativeaiパッケージとは別物なので、検索で出てくる記事がどちらを使っているか確認してから進めます。 - テキストだけの最小呼び出しで疎通を確認します。
from google import genai
client = genai.Client() # 環境変数 GEMINI_API_KEY を自動で読み込む
response = client.models.generate_content(
model="gemini-2.0-flash",
contents="日本語で一言、動作確認の返事をしてください。"
)
print(response.text)
ここでエラーが出る場合は、キーの権限か課金設定のどちらかが原因であることがほとんどです。詳しくは次の章で扱います。
PDFと写真を渡す
ファイルの渡し方は、そのままバイト列として渡す方法(インライン)と、Files APIにアップロードしてから参照する方法の2つがあり、ファイルサイズで使い分けます。
- 小さいPDFや写真(目安は1リクエストあたり合計20MB未満)は、インラインでそのまま渡します。
- スキャン画質が高いPDFや、複数ページ・複数ファイルをまとめて渡す場合は、合計20MBを超えやすいのでFiles APIに切り替えます。Gemini APIの公式ドキュメントでも、リクエスト全体のサイズが20MBを超える場合はFiles APIの利用が案内されています。
- 出力する項目(発行日・金額・取引先)をPythonのクラスとして定義し、応答の形式をそのスキーマに固定します。
import pathlib
from google import genai
from google.genai import types
from pydantic import BaseModel
class Invoice(BaseModel):
issue_date: str
amount: int
vendor: str
client = genai.Client()
pdf_bytes = pathlib.Path("invoice.pdf").read_bytes()
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=[
types.Part.from_bytes(data=pdf_bytes, mime_type="application/pdf"),
"この請求書から発行日・金額・取引先を抜き出してください。",
],
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=Invoice,
),
)
invoice = response.parsed # Invoice型として直接受け取れる
print(invoice.issue_date, invoice.amount, invoice.vendor)
Files APIに切り替える場合は、渡す中身が変わるだけでスキーマ指定の部分はそのまま使えます。
myfile = client.files.upload(file="scan.pdf")
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=[myfile, "この書類から発行日・金額・取引先を抜き出してください。"],
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=Invoice,
),
)
Gemini APIはPDFをページ画像として内部で解釈するため、ページ数が多い書類でも1回の呼び出しで渡せます。この挙動はドキュメントの理解のページで解説されています。
詰まる場所
ここに挙げる5つは、コードが間違っていなくても起きるので、エラーメッセージだけ見て原因を探すと時間を取られます。
1. キーの権限と課金設定
初回呼び出しで PERMISSION_DENIED や課金関連のエラーが返る場合は、キーを発行したプロジェクトで課金が有効になっていないことが原因です。Google Cloud ConsoleでAPIキーに紐づくプロジェクトを開き、請求先アカウントが設定されているか確認します。無料枠の範囲でも、課金設定自体は必要になる場合があります。
2. ファイルサイズの上限
インラインで渡したPDFが高解像度スキャンだと、1ファイルだけで20MBを超えることがあります。この場合は 400 系のエラーか、応答が返らずタイムアウトします。対処は前の章で書いた通り、Files APIへの切り替えです。切り替えの判断を毎回手動でするのが面倒なら、ファイルサイズを読んでから分岐するコードにしておきます。
import os
def call_gemini(path, prompt, schema):
size_mb = os.path.getsize(path) / (1024 * 1024)
if size_mb < 15:
data = pathlib.Path(path).read_bytes()
content_part = types.Part.from_bytes(data=data, mime_type="application/pdf")
else:
content_part = client.files.upload(file=path)
return client.models.generate_content(
model="gemini-2.0-flash",
contents=[content_part, prompt],
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=schema,
),
)
20MBちょうどで切るのではなく15MB程度を境目にしておくと、プロンプト文やレスポンス分の余裕ができて安全です。
3. JSONの前後に説明文が付く
response_mime_type="application/json" と response_schema を指定していれば、ほとんどの場合は response.parsed でそのままオブジェクトが取れます。それでもまれに余計な文字列が混ざる場合に備えて、取得側に例外処理を入れておきます。
import json
try:
invoice = response.parsed
except Exception:
text = response.text.strip()
text = text.removeprefix("```json").removesuffix("```").strip()
invoice = Invoice.model_validate(json.loads(text))
4. 手書き文字や影で桁を誤読する
手書きの金額や、スキャン時の影で数字の一部が欠けている場合、桁を誤って読むことがあります。これはプロンプトの工夫だけでは完全には防げないため、スキーマに読み取りの自信度を表す項目を足して、低い場合は人の確認に回す仕組みにします。
class Invoice(BaseModel):
issue_date: str
amount: int
vendor: str
confidence: str # "high" / "low" をモデル自身に判断させる
プロンプト側に「数字が不鮮明、手書き、影がかかっている場合はconfidenceをlowにしてください」と明示すると、lowの行だけを抜き出して確認対象にできます。
5. レート制限(429)で止まる
まとめて大量のファイルを処理すると、途中で429が返って止まることがあります。待ち時間を増やしながら再試行する処理を入れておきます。
import time
from google.genai import errors
def call_with_retry(**kwargs):
for i in range(5):
try:
return client.models.generate_content(**kwargs)
except errors.ClientError as e:
if e.code == 429:
time.sleep(2 ** i)
continue
raise
raise RuntimeError("429が続いたため処理を中断しました")
何度再試行しても止まる場合は、待ち時間の問題ではなく、1分あたりのリクエスト数の上限そのものに達していることが多いです。処理件数が多い日は、バッチの間隔を空けて実行します。
Claudeやその他の読み取りとの使い分け
書式が取引先ごとにバラバラなら汎用のLLMで対応し、同じ様式の帳票を大量に流すだけなら専用のOCRやテンプレート型のサービスの方が速くて安定します。
| 判断の観点 | LLM(Gemini・Claudeなど)が向く場合 | 専用OCR・テンプレート型が向く場合 |
|---|---|---|
| 書式のばらつき | 取引先ごとにレイアウトが違う | 毎回同じフォーマットの帳票 |
| 項目の変更頻度 | 抽出したい項目が増減しやすい | 項目が固定されている |
| 導入の速さ | プロンプトとスキーマの変更だけで対応できる | テンプレート設定に時間がかかる |
| 手書き・崩れた書類 | 文脈から補えることがある | 想定外のレイアウトに弱い |
GeminiとClaudeのどちらを使うかは、読み取り精度そのものよりも、すでに使っている基盤やコストの契約状況で決めて問題ありません。コードの構造はどちらも「ファイルを渡す・スキーマを指定する・結果を受け取る」という形なので、呼び出し部分を差し替えるだけで移行できます。PDFの読み取り精度については、Document AIと組み合わせる方法とGemini API単体を比較した検証記事もあり、書類の種類によって結果が変わることが報告されています。iret.mediaの検証記事では、帳票の種類ごとに精度差が出ることが示されています。自社で使う帳票に近いものがあれば、そちらも参考になります。
人の確認を残す形
JSONのまま登録せず、一度CSVに出してから人が目を通す工程を挟むと、誤読をそのまま流し込む事故を防げます。
- 読み取り結果をCSVに書き出します。列は、ファイル名・発行日・金額・取引先・要確認フラグの5列にします。
- confidenceがlowの行、または金額が0や空になっている行に「要確認」のフラグを立てます。
- 確認担当者は、要確認フラグが付いた行だけを元ファイルと突き合わせます。フラグなしの行まで毎回全件見直すと、自動化した意味が薄れるので対象を絞ります。
- 修正した値は同じCSVに上書きし、登録用のシステムやExcelに取り込みます。
import csv
def write_csv(invoices, path="output.csv"):
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["ファイル名", "発行日", "金額", "取引先", "要確認"])
for item in invoices:
flag = "要確認" if item.confidence == "low" or item.amount == 0 else ""
writer.writerow([item.source_file, item.issue_date, item.amount, item.vendor, flag])
明細が複数行ある帳票では、合計金額と明細行数が一致しているかもチェック項目に加えておくと、途中の行を読み落としたまま合計だけ合っているような誤りに気づけます。まずは少数のファイルで一通り動かし、要確認フラグがどのくらいの割合で立つかを見てから、本番の件数に広げるとよいです。

OpenAI APIの使い方を自社の実費と運用実例から説明する
Geminiとは何に強いのか、自社の自動化運用から使いどころを整理する
非構造化データはLLMと専用モデルのどちらで処理すべきか判断基準