何を自動化したか
家計簿の入力で一番面倒なのは、レシートを見ながら日付・店名・金額・品目ごとの費目を手で打ち込む作業だと思う。うちで運用している「ウキウキ家計簿」(GASで作った個人用Webアプリ)では、レシートやスクリーンショットをスマホで撮るだけでこの入力を済ませ、さらに楽天カード・楽天ペイの利用明細メールを自動で取り込んで、何も撮らなくても記録が埋まっていくようにした。
撮影1回あたりの入力時間を計測してはいないため実測値は無いが、体感では「レシートを見ながら日付・店名・金額・費目を1つずつ選ぶ」作業がほぼ無くなり、撮って内容を確認して保存するだけになった。メール取り込みの分は完全に無操作で、キャッシュレス払いの記録は何もしなくても翌日には家計簿に載っている。
仕組み(データの流れ)
「塾 GAS 自動化」というより家計簿寄りの話だが、しくみ自体は「Google Apps Script スプレッドシート 同期」の文脈とも重なる部分が多い。レシート撮影とメール取り込みの2経路があり、最終的には同じ1つのデータファイルに合流する。
| 経路 | 読み取り元 | 実行場所 |
|---|---|---|
| レシート撮影 | スマホのカメラ・アルバム画像 | ブラウザ内(クライアントJS)からGemini APIへ直接送信 |
| 楽天カード明細 | Gmail「カード利用のお知らせ」(確定版・速報版) | GAS(budgetFetchRakutenCard 等)が定期的にGmailを検索 |
| 楽天ペイ明細 | Gmail「楽天ペイお支払い完了」 | GAS(budgetFetchRakutenPay)が定期的にGmailを検索 |
レシート撮影は、画像を長辺1600pxまで縮小してからGemini(gemini-2.5-flash)へ直接投げ、「日付・店名・合計・品目ごとの費目」をJSONで返してもらう。サーバー(GAS)を経由させず、ブラウザから直接APIを呼ぶ構成にしているため、画像をいったんどこかにアップロードする手間が無い。
メール取り込みのほうは、楽天カードには「速報版」(利用直後に届くが店名が無い)と「確定版」(数日遅れで店名つき)の2種類のメールがあり、速報版を仮レコードとして先に記録し、確定版が届いたら店名つきの記録に置き換える2段構えにしている。これにより、家計簿への反映が数日遅れる不満と、店名が分からない不便さの両方を避けている。楽天ペイも別ルートで届くため、同じ買い物が「ペイの完了メール」と「カードの利用明細」の両方に載る問題があり、同じ利用日・同額のペイ記録が既にある場合はカード側の取り込みを飛ばす、という重複排除を挟んでいる。
データの保存先はサーバー側の1つのJSONファイル(Google ドライブ)で、iPhone・Mac のどちらから開いても同じファイルを読み書きする。ここに「①レシート撮影」「②メール自動取り込み(定期実行)」「③手入力」が同時に書き込みに来るため、保存のたびに単純上書きはせず、現状のファイルとマージしてから書く方式にしている(詳細は次章)。
コード(要点)
レシート撮影は完全にクライアント側(ブラウザのJavaScript)で完結する。画像を縮小し、プロンプトと一緒にGemini APIへ投げるだけの構成。
function callOcr(b64, mime) {
var exCats = DB.categories.filter(function(c){ return c.kind==='expense'; })
.map(function(c){ return c.name; });
var prompt = 'あなたは日本のレシートや支払い画面のスクリーンショットを読み取る家計簿アシスタントです。'
+ '画像から支払い1件を読み取り、JSONだけを返してください。\n'
+ '形式: {"date":"YYYY-MM-DD","store":"店名","total":合計金額の数値,'
+ '"items":[{"name":"品名","price":数値,"category":"費目"}]}\n'
+ '費目は次の中から各品目に最も近いものを1つ選ぶ: ' + exCats.join('、') + '。';
var url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'
+ '?key=' + encodeURIComponent(DB.settings.geminiKey);
return fetch(url, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }, { inline_data: { mime_type: mime, data: b64 } }] }],
generationConfig: { temperature: 0.1, maxOutputTokens: 4096 }
})
}).then(function(r){ return r.json(); }).then(function(j){
var txt = j.candidates[0].content.parts.map(function(p){ return p.text || ''; }).join('');
var m = txt.match(/\{[\s\S]*\}/);
return JSON.parse(m[0]);
});
}
APIキーは本人のGoogleアカウントに保存され、Gemini公式サイトの無料枠で取得したものを使う想定なので、コード自体にキーは書かない。GAS側のメール取り込みは、Gmail検索でヒットしたメール本文を正規表現でパースするだけのシンプルな構成になっている(本文の細かいパース処理はここでは省く)。
function budgetFetchRakutenPay(afterYmd) {
var q = 'from:(楽天ペイの通知アドレス) subject:(楽天ペイお支払い完了)';
q += afterYmd ? (' after:' + afterYmd) : ' newer_than:1y';
var threads = GmailApp.search(q, 0, 200);
var out = [];
threads.forEach(function(t){
t.getMessages().forEach(function(m){
var rec = parseRakutenPayBody_(m.getPlainBody() || '');
if (rec) out.push(rec);
});
});
return out;
}
ハマった点と回避策
- 保存の単純上書きは危険。 このデータファイルには「10分おきの自動取り込みトリガー」「毎晩の定期実行」「LINEからのレシート送信」「アプリ画面からの保存」の複数系統が書き込みに来る。アプリ画面は開いた時点の古いスナップショットを送ってくるため、単純に上書きすると、その間にトリガーやLINE経由で追加された記録が消えてしまう事故が起きた。対策として、保存のたびに「ファイルの現状」と「送られてきたデータ」をマージしてから書き込む方式に変更した。ルールは、設定類は送信側をそのまま採用し、記録は id で突き合わせてどちらか片方にしか無いものを残す、削除の印(墓標)は両側の和を取る、というもの。
- 同じ買い物が別経路で二重に記録される。 楽天ペイでの支払いは「ペイの完了メール」と「カードの利用明細メール」の両方に載るため、何も対策しないと同じ買い物が2件記録される。同一日・同額のペイ記録が既にあればカード側の取り込みをスキップする形で防いでいる。また、店名の表記ゆれ(同じ店でも「楽天SP ○○」と実店舗名の両方で届く等)による二重計上も発生したため、定期的に走る掃除処理で明らかな二重を後から消す仕組みも足した。
- 同じレシートを2回撮ってしまう。 手ぶれ等で撮り直した際に気づかず両方保存すると重複するため、保存前に同じ日付・同額・同じ店の記録が既にあるかを確認し、あれば確認ダイアログを出す簡単な仕組みを入れている。
- 画像はそのまま送らない。 スマホのカメラ画像をそのまま送ると通信が遅く不安定になりやすいため、送信前にブラウザ側で長辺1600px・JPEGに縮小してから送るようにした。読み取り速度と安定性が上がり、通信量もかなり減った。
導入手順
- Google AI StudioでGemini APIキーを無料枠で取得する。
- 家計簿アプリの設定画面でAPIキーを入力する(本人のGoogleアカウントに保存され、端末間で共有される)。
- 楽天カード・楽天ペイのメール取り込みを使う場合は、GAS側のスクリプトプロパティは特に必要無く、Web アプリを公開した本人のGmailをそのまま検索する構成にする(複数人分をまとめたい場合は、家族のメールアドレスを設定項目として保持しておく)。
- データ保存用にGoogle ドライブへJSON形式のファイルを1つ用意し、読み込み・保存の2つの関数(
DriveApp.getFilesByNameで検索して読む/マージしてから書く)をGAS側に用意する。 - 保存関数は必ず「現状とのマージ」を経由させる。複数の書き込み元がある場合、単純な
setContentによる上書きは避ける。 - 日次バックアップとして、保存ファイルの日付つきコピーを一定世代数だけ残しておくと、事故が起きたときに貼り戻して復旧できる。
よくある質問
Q. Gemini APIの利用は有料ですか?
無料枠の範囲で個人の家計簿用途には十分足りている。詳しい料金体系はGoogle公式のページを確認してほしい。
Q. レシートの品目を1つずつ費目分けしてくれますか?
する。スーパーのレシートのように食品と日用品が混ざっている場合は、品目ごとにAIが費目を割り当て、金額の大きい費目を代表として全体のカテゴリにも反映する。違っていればその場で選び直せる。
Q. メールの書式が変わったら壊れませんか?
正規表現でのパースなので書式変更には弱い。実際にコロン記号が全角・半角どちらでも来ることがあったため、両方を受け付けるようにするなど、頑丈さは実際に届いたメールを見ながら少しずつ足している。完全に無音で止まらないよう、取り込み失敗はエラーとして拾い、件数と合わせて確認できるようにしておくとよい。
この仕組みを、もっと詳しく
- note『妻の代わりに僕が作った、全自動の家計簿。』(1,500円)レシートを撮るだけで家計簿に記録する仕組みを、GAS×Geminiのコードつきでまとめた有料記事(完成品アプリの案内つき)
- Kindle『個人塾長、AIを雇う。』(500円・Kindle Unlimited対応)2年間の実録を1冊に。何をどの順で自動化したか、失敗も含めて
- note『個人塾をGASで自動化した実録(実コード付き)』(1,800円)教室で動かしているGAS×AIの仕組みを、手順とコードつきでまとめた有料記事
- English edition: How I Automated My One-Man Tutoring School with AI ($6.99)