OllamaとPythonで作る、領収書をローカルAIでCSV化するツール

本記事は、弊社エンジニアの Sopheaktra Yong が Medium に投稿した「A receipt-to-spreadsheet tool that never leaves your laptop」を元に、日本語向けに翻訳・再構成したものです。


小規模な事業を営んでいる方なら、箱いっぱいにたまった領収書、迫る月末の締め切り、といった状況がきっと身に覚えがあるかもしれません。そして、解決方法としてはどちらもあまり選びたくない二つの選択肢があります。すべてを自分でスプレッドシートに入力するか、無料の「請求書変換」サイトを使って、会社の財務データを見ず知らずの相手に預けるかです。

私もどちらも試してみたのですが、私は無料の変換サイトに会社の記録を渡すことにどうしても抵抗がありました。こうしたサイトが無料なのには理由があります。文書そのものに価値があるからです。学習データとして、マーケティングのシグナルとして、あるいは自分では制御できないサーバーに置かれるファイルとして。領収書からは、会社がどこから仕入れているか、利益率がどのようなものか、どのスタッフが昼食を経費精算したか、カード番号の下4桁まで分かります。

今は三つ目の選択肢があります。しわの寄った領収書でも問題なく読めるビジョンモデルを、一般的なノートPC上でオフラインかつ無料で動かすことです。アカウントも、APIキーも、アップロードも不要です。性能も、ようやく試してみる価値のある水準に達しました。この記事では、その構築方法を説明します。


このツールでできること

ブラウザで開く画面から、領収書の画像を選ぶかドラッグします。ボタンを一つ押すと、ExcelまたはSpreadsheetsで開ける表が表示され、CSVとしてダウンロードできます。

最初にアプリを開いたときの画面

画面の裏側では、自分のマシン上のビジョンモデルが各画像を読み取り、販売元、日付、請求書番号、小計、税額、合計、通貨、支払い方法を取り出します。

セットアップには約15分かかります。その後は、PCまたはノートPCの仕様にもよりますが、領収書1枚あたりおよそ5秒です。

最初に明言しておきます。これは入力作業を置き換えるもので、確認作業を置き換えるものではありません。帳簿に記録する前に、数値は引き続き自分で確認します。会計士の代わりになるわけではありませんが、面倒な入力作業はたぶん90%減らせます。


必要なもの

  • 過去4年ほどのMacまたはWindowsノートPC。モデルをローカルで動かせることが、ここでの唯一の要件です。
  • RAM 8 GB。16 GBあればより快適です。8 GBの場合は後述の小さいモデルを使います。
  • ダウンロード用に約7 GBの空きディスク容量。

先に確認しておきたい二つのこと

これはMacで構築しました。 Ollama、Python、StreamlitはいずれもWindowsで問題なく動くため、全体もWindowsで動くはずです。Windowsで異なる部分には次のような注記を付けています。

Windowsの場合: このような注記です。

これらは、私の知る限りのWindowsでの相当手順です。最初から最後まで確認した手順ではありません。

有償の仕事で使う前にライセンスを確認してください。 無料でダウンロードできることと、事業で無料で使えることは別です。手順の途中で何かへの同意を求められないため、両者は混同しがちです。ollama pull はただダウンロードするだけです。ライブラリの各モデルには個別の条件があり、その内容は大きく異なります。寛容なものも、研究目的限定のものも、収益やユーザー数のしきい値を超えるまで問題ないものも、出力の利用に条件を付けるものもあります。

見落としがちなのは、同じファミリー内でもサイズごとにライセンスが異なる場合があることです。qwen2.5vl:3b と qwen2.5vl:7b は別々のリリースなので、ファミリー名ではなく、実際に取得したタグを確認してください。ローカルでは次のようにします。

ollama show qwen2.5vl:7b --license

顧客の帳簿に使う前に、ここに表示される内容と、ベンダー自身のサイトにあるモデルカードを読んでください。


ステップ1: Ollamaをインストールする

Ollamaはコンピューター上でAIモデルを動かすためのソフトウェアです。最初のモデルダウンロード後は、インターネットと通信しません。

ollama.com から入手し、通常のアプリケーションと同じようにインストールしてから一度開いてください。Macではメニューバーに小さなラマが現れ、Windowsではシステムトレイに表示されます。これが、エンジンが動作している目印です。登録も、アカウントも、カードも不要です。

Windowsの場合: 提供されるのは OllamaSetup.exe 一つです。インストール先は自分のユーザーフォルダーなので、Program Files へのインストールのように管理者パスワードを求められることはありません。ollama は PATH に追加されますが、すでに開いていたターミナルウィンドウにはその変更が反映されません。ステップ2の前に閉じて新しいウィンドウを開いてください。そうしないと、'ollama' is not recognized as an internal or external command というエラーが出て、インストールに何か問題があったと思ってしまいます。


ステップ2: モデルをダウンロードする

ターミナルを開きます。Macでは Cmd+Space を押して「Terminal」と入力し、Enterを押します。Windowsでは、Windowsキーを押して「Command Prompt」と入力し、Enterを押します。

Windowsの場合: PowerShellもCommand Promptと同じように使え、通常のアプリケーションと同じく Ctrl+V で貼り付けられます。一方、古いCommand Promptでは右クリックが必要な場合があります。この記事のすべての操作にはどちらを使っても構いません。避けるべきなのはWSLです。WSL内にインストールしたOllamaは、WindowsにインストールしたOllamaとは別のエンジンです。そのため、アプリがモデルを見つけられない理由が分かるまでに6 GBを二度ダウンロードすることになります。

次を実行します。

ollama pull qwen2.5vl:7b

約6 GBあるので、コーヒーでも入れてきてください。この作業は一度だけです。

Windowsの場合: モデルは C:\Users\<you>\.ollama\models に保存されるため、7 GBの空き容量は具体的に C: に必要です。十分に空いている D: ドライブがあっても役に立ちません。Macでの相当パスは ~/.ollama/models です。

RAMが8 GBしかない場合は、代わりに小さいモデルを選んでください。

ollama pull qwen2.5vl:3b

ステップ3: Pythonライブラリをインストールする

同じターミナルで次を実行します。

pip install streamlit pandas ollama pillow

streamlitがウェブページを作り、pandasがスプレッドシートを作り、ollamaがエンジンと通信し、pillowがモデルに渡す前の画像を整えます。

pip が認識されない場合は、python.org からPythonを入手し、インストール時に「Add Python to PATH」へチェックを入れてください。

Windowsの場合: Pythonは事前インストールされていないため、ほぼ確実にそのダウンロードが必要です。また、最初のインストーラー画面にある「Add Python to PATH」は誰もが見落としやすいチェックボックスです。Microsoft Store版のPythonも避けてください。サンドボックス化されたファイル権限が原因で、原因を追いにくいStreamlitのエラーが起きることがあります。インストールできたら、次を使います。

py -m pip install streamlit pandas ollama pillow

通常の pip ではなく py -m pip を使うことで、アプリを実行するPythonと同じPythonへ確実にインストールできます。

Macでは代わりに error: externally-managed-environment が表示されることがあります。これはPythonの安全策であり、何かを壊したわけではありません。pip3 install --user streamlit pandas ollama pillow を使うか、プロジェクトフォルダーに仮想環境を作成し、python3 -m venv .venv && source .venv/bin/activate を実行して、その中にインストールしてください。


ステップ4: アプリを取得する

アプリは短いPythonファイル5個から成り、すでにGitHubにあります。ブログ記事から数百行をコピーすると、300行目のどこかで角括弧を一つ落としかねません。ダウンロードして使ってください。

cd ~/Desktop
git clone https://github.com/yong-asial/ReceiptExtractor.git
cd ReceiptExtractor

git がなければ、ブラウザでリポジトリを開き、緑色の Code ボタンをクリックして Download ZIP を選び、Desktopへ展開してください。GitHubは展開後のフォルダーを ReceiptExtractor-main と名付けるので、ReceiptExtractor に名前を変更すれば、次のステップのコマンドがそのまま使えます。

リポジトリの app/requirements.txt には、ステップ3と同じ四つのライブラリが固定されています。そのため、先へ進んだ場合や、システム全体ではなく仮想環境に入れたい場合は、フォルダー内で pip install -r app/requirements.txt を実行すれば、ステップ3全体を一行で済ませられます。

Windowsの場合: まず cd %USERPROFILE%\Desktop を実行します。OneDriveがDesktopを同期している場合、実際のパスは %USERPROFILE%\OneDrive\Desktop です。どちらを使っているか確認するには dir %USERPROFILE% を実行してください。ZIPを使う方法も同じです。ダウンロードを右クリックして Extract All を選びます。

使うだけならコードを読む必要はありません。読みたければ、関心のあるファイルだけを開き、ほかは無視できるように分けてあります。各ファイルは長くても数百行で、先頭に役割が記されています。

ファイル 内容
settings.py 列、プロンプト、サイズ制限。まずここから。
uploads.py ドロップされたファイルの確認と画像の準備
reader.py ローカルモデルへの問い合わせと応答の読み取り
tidy.py その応答を帳簿に記載できる数値へ変換する処理
app.py ページそのもの。何を、どの順番で見せるか

ファイル処理や表の生成とは別に、実際に領収書を読む処理は reader.py の7行だけです。

response = ollama.chat(
    model=model,
    messages=[{"role": "user", "content": PROMPT, "images": [image_bytes]}],
    # temperature 0 keeps repeated runs on the same receipt consistent.
    options={"temperature": 0},
)
return normalise(parse_json(response["message"]["content"]))

仕組みはこれだけです。自分のマシン上で動くモデルに画像とプロンプトを渡し、JSONを受け取ります。

もう一つの重要な構成要素が PROMPT です。中身は普通の英語で、settings.py ではフィールド一覧の隣に定義されています。モデルには、名前付きの八つのフィールドだけを返すよう求めています。

FIELDS = [
    "Vendor Name",
    "Date",
    "Invoice Number",
    "Subtotal",
    "Tax",
    "Total Amount",
    "Currency",
    "Payment Method",
]

# Which of the FIELDS above hold money, and which one holds a date. These are
# cleaned up differently from ordinary text, so if you add or rename a field,
# check that it is listed here when it should be. Everything else in FIELDS is
# copied across as plain text.
AMOUNT_FIELDS = ("Subtotal", "Tax", "Total Amount")
DATE_FIELD = "Date"

# What we write in a column when the value simply isn't on the document.
NOT_FOUND = "Not Found"

# What we say to the model. It is just English — edit it to suit your business:
# add a purchase order number, drop the payment method, ask for line items, or
# write the rules in your own language. Keep the JSON-only instruction, though;
# the rest of the app expects a JSON object back.
PROMPT = f"""You are a bookkeeping assistant reading a receipt or invoice.

Return ONLY a JSON object with exactly these keys:
{json.dumps(FIELDS, indent=2)}

Rules:
- "Date" is the date the receipt or invoice was ISSUED, not a due date or a
  service period. Return it as YYYY-MM-DD, converting from whatever format the
  document uses (including Japanese 2024年11月5日 style).
- Copy "Vendor Name" and "Payment Method" exactly as printed, in the document's
  own language and script. Do not translate or romanise them.
- Amounts must be plain numbers with no currency symbols and no thousands
  separators, e.g. 1234.56
- "Currency" is the 3-letter code, e.g. USD, JPY, EUR.
- Use the string "Not Found" for anything you cannot read on the receipt.
- Never guess or invent a value. Never do arithmetic to fill a blank.
- Output no markdown, no code fences, no commentary. JSON only.
"""

そのプロンプトは、自分の要件や業務に合わせて自由に変更してください。発注番号を追加する、支払い方法を外す、明細行を要求する、ルールを自分の言語で書く、といった変更ができます。FIELDS がCSVの列を決め、その直下の AMOUNT_FIELDS と DATE_FIELD は、どのフィールドが金額で、どれが日付かを示します。これらは通常のテキストとは異なる方法で整形されます。三つすべてを settings.py にまとめておけば、新しいフィールドの追加はたいてい一つのファイルだけで済みます。

自動で追随しない箇所が一つあります。tidy.py の Needs Review チェックです。これは小計と税額の合計が総額と等しいことを前提にしています。金額の列を作り直す場合は、その関数も読んでください。

tidy.py は、モデルの応答を鵜呑みにしないための処理をまとめたファイルです。通貨記号を取り除き、1.725,50 を 1725.50 に変換し、2024年11月5日 を 2024-11-05 にします。数値として扱えないものは捨て、存在しない日付は作りません。値が本当に曖昧な場合、たとえば 06/07/2024 が6月7日なのか7月6日なのか、1.234 が1234なのか1.234なのかは、推測せず、印字されたままの値を残して行にフラグを付けます。実際の帳簿に使うつもりなら、その詳細はファイル自体で読む価値があります。デモと、信頼できるものとの差はそこにあります。


ステップ5: アプリを起動する

ターミナルに戻り、次を実行します。

cd ~/Desktop/ReceiptExtractor
streamlit run app/app.py

Windowsの場合: バックスラッシュを使い、パスは明示しておくのが安全です。

cd %USERPROFILE%\Desktop\ReceiptExtractor
py -m streamlit run app\app.py

PowerShellでは、代わりに cd $HOME\Desktop\ReceiptExtractor を使います。注意点が二つあります。streamlit 単体が認識されない場合は、py -m streamlit なら常に動きます。同じプログラムをより確実に見つける方法です。また、OneDriveがDesktopを同期している場合、実際のパスは %USERPROFILE%\OneDrive\Desktop\... なので、どちらを使っているか確認するには dir %USERPROFILE% を実行してください。初回はファイアウォールの確認を求められることもありますが、ここでは外部からの接続を受け入れる必要がないため、安全にキャンセルできます。

ブラウザが自動的に開きます。領収書をいくつかドラッグし、Extract data をクリックすると、進捗バーが表示されます。

手元に領収書がありませんか。リポジトリには、私が使った六つのテスト文書が tests/receipts に含まれています。米国のカフェの領収書、ヨーロッパ形式の数値を含むドイツの請求書、意図的にぼかした燃料領収書の写真、そして三つの日本語文書です。まずそれらをドラッグして、出力がどのようになるか見てください。正しい値は同じフォルダー内の ground_truth.json にあるので、フィールドごとに表と照合できます。最初の一件はモデルをメモリに読み込むため10〜20秒かかりますが、その後は一件あたり約5秒で安定します。

終わると、緑色の Read 6 receipts. というメッセージ、表、ダウンロードボタンが表示されます。

表へ抽出された6枚の領収書。CSVとしてダウンロード可能

この実行では、3言語・3通貨の六つの文書を処理しています。米国のカフェの領収書、1.725,50 と書かれたヨーロッパ形式のドイツの請求書、斜めから撮影されたぼやけた燃料領収書の写真、そして三つの日本語文書です。すべての文書のすべてのフィールドが正しく抽出されています。日本語の販売元名はローマ字による推測ではなく株式会社サクラマートとして返され、日付は元の書式にかかわらずすべて YYYY-MM-DD に正規化されます。

Download as spreadsheet (CSV) を使うと、ExcelまたはSpreadsheetsでそのまま開けるファイルが得られます。一つ注意点があります。スプレッドシートアプリは、000517 のような請求書番号から先頭のゼロを取り除きがちです。重要であれば、インポート時にその列をテキストとして設定してください。

ターミナルで Ctrl+C を押すと停止します。翌日も同じ二つのコマンドです。(Macでも Cmd ではなく Ctrl です。)


では、使う価値はあるのか

これで、スキャン1回あたりの費用がかからず、飛行機内でも動き、顧客の銀行情報を正体の分からないサーバーへ一切送らない領収書リーダーができました。サブスクリプションも、ページごとの料金も、読むべきデータ処理契約もありません。

正直に述べると、限界もあります。領収書1枚あたりおよそ5秒なので、これは四半期末に箱いっぱいの領収書を処理するためのツールであり、一度に何千件も処理するためのものではありません。7Bモデルには約6 GBの空きメモリが必要ですが、3Bモデルも私のテストでは同じスコアで、8 GBのマシンでも余裕を持って収まります。画像を読むモデルである以上、いずれ何かを読み間違えます。そのためアプリは小計と税額の合計が総額と等しいかを確認し、そうでない行にフラグを付けます。数値を信用する前に、Needs Review 列を見てください。

最も驚いたのは、小さなローカルモデルがここまで良くなったことでした。私は当初、面白いけれど実用にはまだ難がある、週末に作ったハックについて記事を書くつもりでした。しかし実際には、qwen2.5vl は、意図的に質の悪いスマートフォン写真と日本語の請求書を含む、六つのテスト文書の48フィールドすべてを正しく読み取りました。2年前なら、これにはクラウドAPIとページごとの料金が必要でした。いまではノートPCで動きます。コードは5ファイルだけで、一度に読み切れる程度の規模です。

コードはGitHubにあります。フィールド一覧を変更し、自分の文書に向けるか、重要な7行を自分の用途へ取り込んでください。同じパターンは、名刺、身分証明書、手書きのメモ、あるいは入力したくない他の紙の山にも使えます。