GeminiのAI-OCRでレシートを読み取り、仕訳CSVまで自動でデータ化してみる
・ 読了 約15分

レシートや請求書など、帳票データ化の悩みは多くの企業で発生している課題かと思います。
今回は、Gemini 3.6 FlashをAI-OCRとして利用し、レシートの画像や動画から仕訳データのCSVを作成する簡単なアプリを作ってみました。
今回作成したアプリでは、レシートの画像または動画に加えて、出力したい形式のCSVサンプルをアップロードします。
アプリ側でCSVからヘッダーとサンプル行を取得し、Geminiがその列構成や表記を参考に、読み取ったレシートを同じ形式へ変換します。今回はMoney Forwardから出力した仕訳CSVをテンプレートとして利用しました。

一般的なOCRでも文字を読み取ること自体はできます。しかし実務では、読み取った文字を日付、金額、勘定科目、税区分などに分類し、利用するシステムの形式へ並べ直す必要があります。
今回は、文字認識だけでなく、内容の解釈から既存のCSV形式への変換までをGeminiでどこまで自動化できるのか試してみます。
AI-OCRは「文字を読んだ後」まで任せられる
従来のOCRと、GeminiのようなマルチモーダルAIをOCRとして利用する場合の大きな違いは、文字を認識するだけでなく、その意味まで解釈できることです。
レシートには店名、商品名、日付、金額、税率などさまざまな情報が含まれています。会計処理では、それぞれを読み取るだけでなく、「この数字は税込合計」「この番号はインボイス登録番号」と判断し、さらに購入内容から勘定科目や税区分を決める必要があります。
今回のアプリでは、この処理を抽出とCSVへのマッピングの2段階に分けました。
最初にGeminiがレシートを読み取り、仕訳に必要な情報を共通のデータ構造へ変換します。その結果と、CSVテンプレートから取得したヘッダーやサンプル行をGeminiへ渡し、、最終的なCSV形式へ変換します。
今回使用した3枚のレシートはこちらです。
※ちょうど良いレシートが手元になかったため、今回は子ども用品など私的な支出のレシートを使用しています
Gemini Developer APIで仕訳データを抽出する
今回はGoogleのGemini Developer APIからGemini 3.6 Flashを利用し、モデルの呼び出しにはInteractions APIを使っています。
現在のInteractions APIでは、画像や動画などのマルチモーダル入力とStructured Outputsを同じインターフェースで扱えます。
まずレシートの内容を抽出する
画像の場合はBase64へ変換し、プロンプトと一緒にInteractions APIへ渡しています。
parts = [
{
"type": "image",
"data": base64.b64encode(item.data).decode("ascii"),
"mime_type": item.mime_type,
}
for item in media
]
return self._request(
[
{"type": "text", "text": EXTRACTION_PROMPT},
*parts,
],
ReceiptExtraction,
)ここで使用しているEXTRACTION_PROMPTは以下のような内容です。
EXTRACTION_PROMPT = """\
入力された画像または動画から、互いに異なるレシートを出現順にすべて検出してください。
1枚のレシートを1件として source_index を1から連番にします。
日付、店舗、登録番号、税込合計、税率別の内税、支払方法、購入内容を読み取り、 単一仕訳として妥当な借方・貸方勘定科目、税区分、摘要を推定してください。
根拠が見えない値は推測で埋めず null にし、 具体的な曖昧さだけ uncertainties に記録します。
金額はすべて整数の日本円で返してください。
"""プロンプトでは、単に「レシートを読み取ってください」とは指示していません。
何を抽出するか、どこまでAIに判断させるか、判断できない場合にどうするかまで指定しています。
特に今回は、読み取れない値を無理に推測させずnullにすること、曖昧な部分をuncertaintiesとして残すことを明示しています。
生成AIを業務で利用する場合、すべての項目を何らかの値で埋めてもらうより、AIが判断できなかったこと自体をデータとして取得できる方が扱いやすいケースがあります。
Structured Outputsでデータ構造を固定する
GeminiのレスポンスにはStructured Outputsを利用しています。
interaction = self._client.interactions.create(
model=MODEL_ID,
input=input_data,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": schema.model_json_schema(),
},
)
return schema.model_validate_json(interaction.output_text)Pydanticで定義したモデルからmodel_json_schema()でJSON Schemaを生成し、response_formatへ渡しています。
GeminiのStructured Outputsでは、application/jsonとJSON Schemaを指定することで、そのSchemaに沿ったJSONを返すよう制約できます。
今回のようなOCRでは、AIから自然文を受け取って後から解析するのではなく、最初からアプリケーションで扱えるオブジェクトとして取得できるのが便利です。
動画はFiles APIへアップロードする
画像とは処理方法を変え、動画の場合はGeminiのFiles APIを利用しています。
with TemporaryDirectory() as directory:
path = Path(directory) / "receipt-video.mp4"
path.write_bytes(video_bytes)
uploaded = client.files.upload(file=str(path))
try:
deadline = time.monotonic() + 120
while state_name(uploaded) != "ACTIVE":
if state_name(uploaded) == "FAILED":
raise ValueError("動画を処理できませんでした")
if time.monotonic() >= deadline:
raise TimeoutError("動画処理がタイムアウトしました")
time.sleep(2)
uploaded = client.files.get(name=uploaded.name)
video_part = {
"type": "video",
"uri": uploaded.uri,
"mime_type": uploaded.mime_type,
}
result = request_gemini([
{"type": "text", "text": EXTRACTION_PROMPT},
video_part,
])
finally:
client.files.delete(name=uploaded.name)Files APIへアップロードした動画は、その場ですぐ推論に利用できるとは限りません。
ファイルにはPROCESSING、ACTIVE、FAILEDといった状態があり、ACTIVEになると推論に利用できます。今回のコードでもfiles.get()を定期的に呼び出し、ACTIVEになるまで待っています。
処理後は今回の用途ではファイルを再利用しないため、finallyで削除しています。

今回は3枚のレシートを順番に映した動画をアップロードしています。写真を1枚ずつ撮影しなくても、一つの動画から複数のレシートを認識し、それぞれ別の取引として抽出できます。
アップロードしたCSVの形式へ変換する
ここが今回のアプリで少し工夫した部分です。
Geminiから直接「Money ForwardのCSVを作って」と依頼するのではなく、ユーザーがアップロードしたCSVをテンプレートとして利用しています。
今回はMoney Forwardから出力した既存の仕訳CSVをアップロードしました。
CSVからヘッダーと数行のサンプルデータを取得し、先ほどGeminiが抽出したレシート情報と一緒に、もう一度Geminiへ渡します。
このとき使っているのがMAPPING_PROMPTです。
MAPPING_PROMPT = """\
あなたは抽出済みレシートを任意の仕訳CSVテンプレートへ転記する変換器です。
headers の順序を絶対に変更せず、各レシートにつき values を1行返してください。
values の要素数は headers と完全に一致させます。
sample_rows は列の意味、日付、勘定科目、税区分などの表記例としてだけ利用します。
分からない列は空文字にし、テンプレートにない列を追加しません。
source_index は入力レシートの番号を維持してください。
入力データ:
"""実際には、テンプレートのヘッダー、サンプル行、抽出済みのレシート情報をJSONとして追加しています。
payload = json.dumps(
{
"headers": template.headers,
"sample_rows": template.sample_rows,
"receipts": extraction.model_dump(mode="json")["receipts"],
},
ensure_ascii=False,
)
return self._request(MAPPING_PROMPT + payload, MappedCsv)ここでサンプル行も渡しているのがポイントです。
同じ意味の項目でも、システムによって日付形式や税区分、勘定科目の表記は異なります。ヘッダーだけでなく既存のデータも見せることで、AIがそのCSVで使われている表記に合わせて出力できます。
そのため、アプリケーション側にMoney Forward専用の列名をすべてハードコードしなくても、テンプレートとなるCSVを変更すれば別の形式へ対応できる余地があります。
AIが返した結果をそのままCSVにするのではなく、出力前に通常のプログラムでも整合性を検証しています。実際には以下のように、レシート数、CSVの列数、source_indexの重複や欠落などをチェックしています。
def _ordered_rows(
headers: list[str],
extraction: ReceiptExtraction,
mapped: MappedCsv,
) -> list[list[str]]:
# 検出したレシート数と、Geminiが生成したCSVの行数が一致するか確認
if len(mapped.rows) != len(extraction.receipts):
raise PipelineError(
"検出したレシート数と生成されたCSV行数が一致しません。"
)
mapped_by_index = {}
for row in mapped.rows:
# 同じレシート番号が複数行に重複していないか確認
if row.source_index in mapped_by_index:
raise PipelineError(
"生成されたCSVに重複したレシート番号があります。"
)
# Geminiが生成した各行の列数が、
# アップロードされたCSVテンプレートの列数と一致するか確認
if len(row.values) != len(headers):
raise PipelineError(
"生成されたCSVの列数がテンプレートと一致しません。"
)
mapped_by_index[row.source_index] = row.values
expected = [
receipt.source_index
for receipt in extraction.receipts
]
# 抽出されたレシート番号と、
# CSVへマッピングされたレシート番号が一致するか確認
# 欠落したレシートや、存在しない番号の追加を検出する
if set(mapped_by_index) != set(expected):
raise PipelineError(
"生成されたCSVのレシート番号が抽出結果と一致しません。"
)
# Geminiの返却順に依存せず、
# 元のレシートの出現順(source_index順)でCSV行を並べる
return [
mapped_by_index[index]
for index in expected
]
3枚のレシートから、Money Forwardの仕訳CSVと同じ27列のデータを生成できました。取引日や金額だけでなく、勘定科目、税区分、インボイス登録番号、摘要なども入力されています。
※画像サイズの関係で後ろ数カラムを省略しています
普通のGeminiでもCSVは作れる。それでもAPIを使う理由
今回のような処理は、レシートを数枚読み取ってCSVを一度作るだけなら、通常のGeminiから依頼することもできます。
専用のアプリケーションを作る意味が出てくるのは、同じ処理を業務として何度も繰り返す場合です。
今回のアプリでは、抽出ルールをプロンプトとして固定し、Structured Outputsでデータ構造を決め、アップロードしたCSVに合わせて出力形式を変換しています。
さらに、列数や行数の検証、動画ファイルの処理、CSVファイルの生成までを一連の処理として実行できます。
今後はここに対話的な操作を加えるのも面白そうです。
Interactions APIはprevious_interaction_idを利用して、それまでの会話状態を引き継いだ処理にも対応しています。
最初に生成された仕訳に対して「この取引は消耗品費ではなく旅費交通費に変更して」「この店舗では今後この勘定科目を使って」と指示し、結果を修正するUIも考えられます。
一度のAI実行ですべてを正解させるのではなく、AIが最初のデータを作り、必要なところだけ人が対話して修正する形にすると、単純なOCRよりもう少し業務エージェントに近い使い方になりそうです。
まとめ
今回はGemini 3.6 FlashをAI-OCRとして利用し、レシートの画像や動画から仕訳データを抽出し、CSVへ変換するアプリを作ってみました。
画像はInteractions APIへ直接入力し、動画はFiles APIへアップロードして処理しています。また、Structured Outputsを利用することで、Geminiの出力をあらかじめ定義したJSON Schemaに沿った形式で取得し、そのままCSV生成などの後続処理につなげられます。
単発でレシートを読み取ってCSVを作るだけであれば、通常のGeminiからでも可能です。一方、業務として繰り返し利用する場合には、プロンプトやSchemaを固定したり、マスタとの照合や検証処理を加えたり、読み取り結果を対話しながら修正したりと、アプリケーションとして実装する意味が出てきます。
今回のようなレシートの仕訳データ化だけでなく、請求書や注文書から必要な項目を抽出する、複数の見積書を比較表にまとめる、約款や契約書などのPDFを読み取って条件の違いを整理する、といった業務にも同じ考え方を応用できます。
AI-OCRで重要なのは、文字を読み取ることだけではなく、読み取った内容を解釈し、業務で利用できるデータへ変換するところまで含めて設計することだと感じています。
AI導入・業務自動化のご相談
弊社では、AI導入や業務自動化の支援を行っています。
また、今回のような帳票の読み取りから、データの構造化・加工・CSV出力までを一連の業務として自動化するサービス「Subel」を開発しています。
Excel・CSV・PDFなどを使った定型業務や、AI-OCRを実際の業務フローに組み込みたい企業さまは、お気軽にご相談ください。

Resonalエンジニアリング部
Google Cloudや生成AIの導入、プロダクト開発における役立つコンテンツをお届けします。

