0. はじめに
皆さん、こんにちは!面倒な家計簿入力、ついついサボってしまいがちではありませんか?
「このレシート、後で入力しよう…」と思っていたらいつの間にか山積みになっていたり、「飲み会の割り勘、いくらだっけ?」と忘れてしまったり。
もし、LINEにレシートの写真を送ったり、「飲み会 5000円」のようにメッセージを送るだけで、AIが賢く内容を読み取って自動でGoogleスプレッドシートに記録してくれるボットがあったら、最高だと思いませんか?
この記事では、そんな夢のような「AI家計簿ボット」を、Google Apps Script (GAS) と Gemini API を使って開発する方法を、頼れる先輩エンジニアのように、一から丁寧に解説していきます。
この記事を読み終える頃には、あなたもGASをハブにして、LINEやGoogle Drive、そしてGeminiのような強力な生成AIを連携させる具体的な手法をマスターし、自分だけの便利なツールを開発できるようになっているはずです。
さあ、一緒に開発を始めていきましょう!
1. 動作環境と準備
まずは、このAI家計簿ボットを動かすために必要なツールやサービスを揃えましょう。ほとんど無料で始められますよ。
- Googleアカウント: スプレッドシートやGASの利用に必須です。
- LINE Developersアカウント: LINEボットを作るために必要です。
- Google Cloud (Gemini API) アカウント: AIの頭脳となるGemini APIを使うために必要です。
1-1. GoogleスプレッドシートとGoogleドライブの準備
- Googleドライブに、このプロジェクト用の新しいフォルダを作成します。(例:
LINE家計簿BOT
) - そのフォルダの中に、レシート画像を保存するためのサブフォルダを作成します。(例:
ReceiptImages
)このサブフォルダのIDを後で使います。フォルダIDは、URLのfolders/
の後に続く文字列です。 - 同じくプロジェクト用フォルダの中に、新しいGoogleスプレッドシートを作成します。(例:
LINE家計簿
)このスプレッドシートのIDも後で使います。IDは、URLのd/
と/edit
の間にある長い文字列です。
1-2. GASプロジェクトの作成
- 先ほど作成したGoogleスプレッドシートを開き、メニューから「拡張機能」>「Apps Script」を選択します。
- 新しいGASプロジェクトが開きます。ここにコードを記述していきます。
1-3. LINE Messaging APIの準備
- LINE Developersコンソールにログインします。
- 初めての場合は「プロバイダー」を作成します。(例:
MyApps
) - プロバイダーの中に、「チャネルを作成する」から「Messaging API」を選択します。
- 必要な情報を入力してチャネルを作成します。
- 作成したチャネルの「チャネル基本設定」タブにあるチャネルシークレットと、「Messaging API設定」タブにある**チャネルアクセストークン(長期)**を発行して、控えておきます。
1-4. Gemini APIキーの取得
- Google AI Studio にアクセスし、Googleアカウントでログインします。
- 「Get API key」をクリックし、新しいAPIキーを作成します。
- 生成されたAPIキーをコピーして、大切に保管しておきます。
1-5. スクリプトプロパティの設定
さて、準備の最後にして、非常に重要なステップです。取得した各種IDやAPIキーをGASに設定します。
「なぜコードに直接書かないの?」
APIキーのような機密情報をコードに直接書き込むと、もしコードが第三者に見られた場合に、キーが悪用されてしまう危険性があります。PropertiesService
を使うことで、コードとは別の場所に安全に情報を保管できるため、セキュリティが格段に向上するのです。これは、実践的な開発における鉄則なので、必ず覚えておきましょう!
- GASエディタの左側メニューから歯車アイコンの「プロジェクトの設定」をクリックします。
- 「スクリプト プロパティ」のセクションで、「スクリプト プロパティを追加」をクリックします。
- 以下の4つのプロパティを追加します。
プロパティ名 | 値 |
---|---|
LINE_TOKEN |
取得したLINEチャネルアクセストークン |
FOLDER_ID |
画像保存用フォルダのID |
SPREADSHEET_ID |
記録用スプレッドシートのID |
GEMINI_API_KEY |
取得したGemini APIキー |
1-6. GASのデプロイ
最後に、作成したGASをLINEからの通知を受け取れるように「ウェブアプリ」として公開します。
- GASエディタ右上の「デプロイ」>「新しいデプロイ」を選択します。
- 種類の選択で歯車アイコンの「ウェブアプリ」を選択します。
- 「次のユーザーとして実行」を「自分」、「アクセスできるユーザー」を「全員」に設定します。
- 「デプロイ」ボタンをクリックし、表示される権限を承認します。
- 表示されたウェブアプリURLをコピーします。
- LINE Developersコンソールの「Messaging API設定」タブにある「Webhook設定」の「Webhook URL」に、コピーしたURLを貼り付けて「更新」します。「Webhookの利用」もオンにしてください。
これで、すべての準備が整いました!
2. コード全体の流れ
これから解説するコードが、全体としてどのように動くのか、先にイメージを掴んでおきましょう。
- ユーザーがLINEで画像またはテキストを送信します。
- LINEプラットフォームが、私たちのGASプロジェクトのウェブアプリURL(Webhook)に通知を送ってきます。(
doPost
関数が起動) - GASは、まず「この通知は処理済みじゃないか?」をチェックします。(二重実行防止)
- 送られてきたのが画像かテキストかを判断します。
-
【画像の場合】
- LINEのサーバーから画像データを取得します。(
getLineImage
) - 取得した画像を、Googleドライブの月別フォルダに保存します。(
saveImageToDriveWithRetry
) - 保存した画像をGemini APIに送り、「このレシートから店舗名や合計金額、品目を読み取ってJSONで教えて」とお願いします。(
analyzeReceiptWithGemini
)
- LINEのサーバーから画像データを取得します。(
-
【テキストの場合】
- 送られてきたテキストをGemini APIに送り、「この文章から内容、金額、カテゴリを抽出してJSONで教えて」とお願いします。(
analyzeTextForRegistration
)
- 送られてきたテキストをGemini APIに送り、「この文章から内容、金額、カテゴリを抽出してJSONで教えて」とお願いします。(
- Geminiから返ってきたJSONデータを解析し、スプレッドシートの正しい月別シートに書き込みます。(
writeToSpreadsheet
) - 最後に、スプレッドシートから今月の支出合計などを計算し(
calculateMonthlySummary
)、「記録しました!」という結果をLINEに返信します。(replyToLine
)
このように、GASが中心となって各サービスと連携し、一連の処理を自動で行ってくれるわけです。
3. コードの詳細解説
ここからは、コードを機能のかたまりごとに分けて、それぞれの役割やポイントを詳しく見ていきましょう。
doPost(e)
: すべての処理の入り口
この関数が、LINEからの通知(Webhook)を受け取るメインの関数です。
/**
* LINEからのPOSTリクエストを処理する関数
*/
function doPost(e) {
// ... (エラーチェックやイベント解析)
// --- メッセージが重複して処理されるのを防ぐおまじない ---
const messageIdFromEvent = (event.type === 'message' && event.message) ? event.message.id : null;
if (messageIdFromEvent) {
const scriptCache = CacheService.getScriptCache();
const cacheKey = `processed_line_message_id_${messageIdFromEvent}`;
if (scriptCache.get(cacheKey)) {
// 既に処理済みなら、ここで処理を終了する
console.log(`メッセージID '${messageIdFromEvent}' は既に処理済みのため、今回のリクエストをスキップします。`);
return ContentService.createTextOutput(JSON.stringify({'status': 'ok'})).setMimeType(ContentService.MimeType.JSON);
}
// 処理済みIDを600秒間(10分)キャッシュに保存
scriptCache.put(cacheKey, 'true', 600);
}
// ... (イベントのreplyTokenを取得)
// --- 画像かテキストかで処理を振り分ける ---
if (event.type === 'message' && event.message.type === 'image') {
// 画像処理のロジックへ
} else if (event.type === 'message' && event.message.type === 'text') {
// テキスト処理のロジックへ
} else {
// それ以外の場合の応答
}
// ... (エラーハンドリング)
}
-
doPost(e)
: この名前はGASで決まっていて、外部からPOSTリクエストがあったときに自動的に実行されます。e
という引数の中に、LINEから送られてきた情報(ユーザーID、メッセージ内容など)がすべて詰まっています。 -
CacheService
による二重実行防止: LINEのWebhookは、ネットワークの状況などによって同じ通知を複数回送ってくることがあります。そのままでは同じレシートが二重に記録されてしまいますよね。そこでCacheService
を使います。一度処理したメッセージのIDを一時的にキャッシュ(短期記憶)に保存し、「このID、さっき処理したな」と判断したら、処理をスキップするようにしています。これは安定したボット運用に欠かせない、実践的なテクニックです。 -
処理の振り分け:
event.message.type
を見ることで、送られてきたのが'image'
なのか'text'
なのかを判断し、その後の処理を分けています。これがボットの基本的な制御構造になります。 -
try-catch
によるエラーハンドリング: コード全体がtry{...}
で囲まれており、最後にcatch(error){...}
が待ち構えています。もし処理のどこかで予期せぬエラーが発生しても、プログラムが停止せず、catch
ブロックでエラー内容をログに記録し、ユーザーには「システムエラーが発生しました」と通知することができます。これがないと、エラーが起きたときにボットが完全に沈黙してしまい、ユーザーは何が起きたのか分からなくなってしまいます。
analyzeReceiptWithGemini(fileId)
: 画像解析の心臓部
この関数は、Googleドライブ上の画像をGemini APIに渡して、レシート情報を抽出させる、このボットの最も賢い部分です。
/**
* Gemini APIで【画像から】レシート情報を抽出する
*/
function analyzeReceiptWithGemini(fileId) {
// ... (APIキーのチェック)
const file = DriveApp.getFileById(fileId);
const imageBlob = file.getBlob();
const imageBytes = Utilities.base64Encode(imageBlob.getBytes());
const mimeType = imageBlob.getContentType() || 'image/jpeg';
// --- ここが最重要! Geminiへの「指示書(プロンプト)」 ---
const prompt = `あなたはレシートの画像から情報を抽出するエキスパートです。
以下の項目を抽出し、JSON形式で出力してください。
- "shopName": 店舗名 (文字列。病院名、クリニック名、薬局名など)。...
- "totalAmount": レシートに記載されている、実際に支払った最終的な「税込み合計金額」 (数値)。...
- "receiptType": このレシートの種別を判断してください (... "医療", "薬局", "スーパーマーケット", ...)。
- "items": 各品目を含む配列。
- ...
例1 (スーパーマーケット):
{ "shopName": "〇〇スーパー", "totalAmount": 370, ... }
例2 (クリニック - 品目省略):
{ "shopName": "ふたばクリニック", "totalAmount": 3500, "receiptType": "クリニック", "items": [] }
...
提供するレシート画像の内容を厳密に解析し、正確なJSON形式で出力してください。特に "totalAmount" と "receiptType" の抽出精度を最大限に高めてください。`;
// --- Gemini APIにリクエストを送信 ---
const payload = { 'contents': [ { 'parts': [ { 'text': prompt }, { 'inlineData': { 'mimeType': mimeType, 'data': imageBytes } } ] } ], 'generationConfig': { 'responseMimeType': 'application/json' } };
const options = { 'method': 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload), 'muteHttpExceptions': true };
const response = UrlFetchApp.fetch(GEMINI_API_BASE_URL, options);
// ... (レスポンスの解析とエラー処理)
}
-
プロンプトエンジニアリング: Geminiのような生成AIをうまく使う秘訣は、いかに的確な指示(プロンプト)を出せるかにかかっています。
-
役割設定:
あなたはレシートの画像から情報を抽出するエキスパートです。
と最初に役割を与えることで、AIの回答の精度が上がります。 -
厳密な出力形式の指定:
JSON形式で出力してください。
と明確に指示し、さらにresponseMimeType': 'application/json'
という設定も加えることで、AIは機械的に処理しやすいデータを返してくれます。 -
項目ごとの詳細な指示: 各項目(
shopName
,totalAmount
など)について、どんな情報を、どんな点に注意して抽出してほしいかを細かく定義します。例えばshopName
では「自然な日本語表記に変換して」と指示したり、totalAmount
では「最終的な税込み合計金額」であることを強調しています。 -
具体例の提示 (Few-shot Prompting):
例1 (スーパーマーケット): ...
のように、具体的な出力例をいくつか示すことで、AIは「なるほど、こういう形式で返せばいいんだな」とより深く理解してくれます。特に、品目詳細が不要な医療系のレシートの例も示すことで、柔軟な対応を促しています。
-
役割設定:
-
画像データの送信:
Utilities.base64Encode()
を使って画像をテキスト(Base64文字列)に変換し、プロンプトと一緒にpayload
に詰めてAPIに送信しています。
analyzeTextForRegistration(userInputText)
: テキスト解析の心臓部
こちらはユーザーが送ってきたテキストメッセージを解析する関数です。画像解析と同様、プロンプトが鍵となります。
/**
* Gemini APIで【テキストから】家計簿情報を抽出する
*/
function analyzeTextForRegistration(userInputText) {
// ...
const today = new Date();
const todayStr = Utilities.formatDate(today, Session.getScriptTimeZone(), "yyyy年MM月dd日");
// --- テキスト解析用のプロンプト ---
const prompt = `現在の日付は「${todayStr}」です。
ユーザーが入力した以下のテキストから、家計簿に記録するための情報を抽出してください。
ユーザー入力テキスト: "${userInputText}"
以下の項目をJSON形式で出力してください。
...
- "transactionDate": 取引の日付 ("yyyy-MM-dd" 形式)。テキストに "昨日"、"今日"、"MM月DD日" のような日付情報が含まれていれば、それを解釈して日付を特定してください。もし日付情報が含まれていない場合は null としてください。この際、現在の日付 (${todayStr}) を基準に解釈してください。
例1:
ユーザー入力テキスト: "昨日の飲み会 3000円でした"
出力JSON:
{ ... "transactionDate": "${/*昨日の日付*/}" }
...`;
// ... (APIリクエスト)
}
-
文脈の提供:
現在の日付は「${todayStr}」です。
とプロンプトに含めることで、「昨日」や「今日」といった相対的な日付表現をAIが正しく解釈できるようになります。 - 柔軟な情報抽出: テキストはレシートと違って形式が自由です。このプロンプトでは、「品目」「金額」「カテゴリ」「店舗名」「日付」を抽出するように指示し、AIの自然言語処理能力を最大限に活用しています。
saveImageToDriveWithRetry(...)
: 安定性のためのリトライ処理
この関数は、取得した画像をGoogleドライブに保存するだけですが、一工夫されています。
/**
* Google Driveに画像を保存する (リトライ処理付き)
*/
function saveImageToDriveWithRetry(imageBlob, maxRetries = 3, initialDelayMillis = 1000) {
// ...
while (attempts < maxRetries) {
try {
// ... (ファイル保存処理)
return file.getId();
} catch (e) {
if (e.message && e.message.includes("サーバー エラーが発生しました") && attempts < maxRetries) {
console.warn(`Drive保存サーバーエラー ... リトライ`);
Utilities.sleep(delay); // 少し待つ
delay *= 2; // 次の待ち時間を長くする
} else {
throw e; // 致命的なエラーなら再スロー
}
}
}
// ...
}
- なぜリトライが必要?: Google Driveのようなクラウドサービスは、ごく稀に一時的なサーバーエラーでリクエストに失敗することがあります。そんな時に一度の失敗で処理全体を止めてしまうのはもったいないですよね。
-
リトライの実装:
try-catch
をwhile
ループで囲み、もし「サーバーエラー」のような一時的なエラーだったら、Utilities.sleep()
で少し時間をおいてから処理を再試行(リトライ)します。何度か試してもダメな場合や、回復不能なエラーの場合は、そこで初めて処理を中断します。これにより、ボットの安定性(堅牢性)が大きく向上します。
writeToSpreadsheet(...)
: データを賢く書き込む
Geminiが抽出したデータを、最終的にスプレッドシートに書き込む関数です。
/**
* スプレッドシートにレシート情報を書き込む
*/
function writeToSpreadsheet(receiptData) {
// ...
const dateToRecord = receiptData.registrationDateObj ? new Date(receiptData.registrationDateObj) : new Date();
const sheetName = Utilities.formatDate(dateToRecord, Session.getScriptTimeZone(), "yyyy年MM月");
let sheet = ss.getSheetByName(sheetName);
if (!sheet) {
// シートがなければ新規作成し、ヘッダーを書き込む
sheet = ss.insertSheet(sheetName);
sheet.appendRow(header);
}
// --- レシートの種別によって書き込み方を変える ---
const isMedicalType = ...;
const isTextEntry = receiptType === "テキスト入力";
if (isMedicalType || isTextEntry) {
// 医療系やテキスト入力は、品目をまとめず合計金額を1行で記録
sheet.appendRow([ ... ]);
} else {
// 通常のレシートは、品目ごとに1行ずつ記録
receiptData.items.forEach(item => {
sheet.appendRow([ ... ]);
});
}
}
-
月別シートの自動生成:
Utilities.formatDate(...)
で記録日の年月からシート名を生成し(例:2025年06月
)、ss.getSheetByName(sheetName)
でそのシートを探します。もしシートが存在しなければ、新しく作成してヘッダーを書き込みます。これで、月が変わると自動的に新しいシートに記録が始まります。 -
条件に応じた書き分け: Geminiの解析結果から
receiptType
(レシート種別)を読み取り、「医療系」や「テキスト入力」の場合は合計金額を1行でシンプルに記録し、スーパーなどの通常レシートの場合は品目ごとに複数行で詳細に記録するように処理を分けています。これにより、後でデータを見返したときに分かりやすくなります。
4. 実行方法と結果
すべての設定とコードの記述が終わったら、実際に使ってみましょう!
- LINEアプリで、作成したボットを友だち追加します。
- トーク画面で、財布の中にあるレシートを撮影して送信します。
- または、「昨日のランチ 1200円」のようにテキストメッセージを送信します。
成功すると…
数秒〜数十秒後、ボットから下図のような返信メッセージが届きます。
レシート情報を記録しました。
店舗名: 〇〇スーパー
種別: スーパーマーケット
合計金額: 370円
【主な品目】
- 牛乳: 250円
- お茶: 120円
--- 今月の状況 ---
総合計: 15,820円
カテゴリ別支出:
- 食費: 8,500円
- 交際費: 5,000円
- 日用品: 2,320円
※詳細はスプレッドシートをご確認ください。
そして、Googleスプレッドシートを確認すると…
日付 | 店舗名 | 品名 | ジャンル | 金額 |
---|---|---|---|---|
2025/06/07 11:50:10 | 〇〇スーパー | 牛乳 | 食料品 | 250 |
2025/06/07 11:50:10 | 〇〇スーパー | お茶 | 飲料 | 120 |
2025/06/06 12:30:00 | 不明 | ランチ代 | 食費 | 1200 |
このように、送った情報が自動で記録されていることが確認できます!
5. アレンジ・カスタマイズのヒント
このボットは、あくまで基本形です。ここからあなたのアイデア次第で、さらに便利にカスタマイズできます。
-
Geminiのプロンプト変更
-
カテゴリの種類を増やす:
analyzeReceiptWithGemini
やanalyzeTextForRegistration
内のプロンプトで、category
の説明部分に(例: 食費, 交際費, 交通費, ... , **光熱費, 家賃**)
のように、あなたが必要なカテゴリを追加します。 -
抽出項目を追加する: レシートから「電話番号」も抽出したくなったら、プロンプトに
- "phoneNumber": 店舗の電話番号 (文字列)
のような項目定義を追加し、writeToSpreadsheet
関数とスプレッドシートのヘッダーにも「電話番号」列を追加します。
-
カテゴリの種類を増やす:
-
スプレッドシートの項目追加
- スプレッドシートのヘッダーに新しい列(例: 「メモ」)を追加します。
-
writeToSpreadsheet
関数内のsheet.appendRow([...])
の部分に、新しい列に対応するデータを追加します。固定の文字列でも良いですし、Geminiに抽出させた新しい項目でも良いでしょう。
-
返信メッセージの変更
-
doPost
関数内でLINEに返信するメッセージを組み立てている部分 (replyMessageText += ...
) を自由に変更してみましょう。例えば、今月の予算を設定しておき、残りの予算を表示する、といったアレンジも面白いかもしれませんね。
-
6. まとめ
今回は、GASをハブ(中心)として、LINE (UI)、Google Drive (ストレージ)、スプレッドシート (DB)、そして Gemini API (頭脳) という強力なサービス群を連携させる方法を解説しました。
ポイントの振り返り:
-
PropertiesService
でAPIキーを安全に管理する。 -
CacheService
で二重実行を防ぎ、安定性を高める。 - プロンプトエンジニアリングで、AIから欲しい情報を的確に引き出す。
-
try-catch
とリトライ処理で、エラーに強い堅牢なプログラムを作る。
この仕組みを応用すれば、家計簿ボット以外にも、様々なツールが作れるはずです。
- 名刺の写真を撮って送ると、顧客リストに自動で追加される 「名刺管理ボット」
- 薬の袋を撮って送ると、飲む時間をリマインドしてくれる 「お薬リマインダー」
- 会議のホワイトボードを撮って送ると、議事録のテキストを生成してくれる 「議事録作成アシスタント」
ぜひ、この記事をきっかけに、あなただけの便利なボット開発に挑戦してみてください!