皆さんこんにちは、横浜で清掃業をしているヤスです。
前回は【Dify中級】事業アイデアを入力するだけ!Jina AIで競合自動調査+市場規模分析+投資家向けピッチ要約を全自動生成
のアプリを作りましたね。今回はこちらです。
🎁 この記事で作るアプリの「完成品」を配布しています
「読んだけど組む時間がない」という方のために、インポートするだけで動くDSLファイル+GASコード+エラー対処集をnoteで提供中です(980円)→ [こちらからどうぞ]
1. 領収書の手入力をゼロにします
経費精算の領収書入力は地味に時間がかかります。1枚4〜5分 × 月20枚 = 月80〜100分。このアプリを使えば1枚40秒になります。写真を撮ってアップするだけで日付・店名・金額・カテゴリが自動でGoogleスプレッドシートに記録されます。
今回紹介する「経費精算くん」は以下を全自動で実行します。
- 領収書・レシートの写真をビジョン機能で自動読み取り(日付・店名・金額・カテゴリ)
- 経費カテゴリに自動分類(交通費・会議費・消耗品費など8種類)
- GAS経由でGoogleスプレッドシートの経費管理シートに自動追記
- 読み取り精度を高・中・低で自己評価して不鮮明な場合は警告を表示
実際にコンビニのレシート(まいばすけっと・229円・消耗品費)でテストしたところスプレッドシートに正しく記録されました。ただしここに至るまでに5つの失敗がありました。全て公開します。
💡 領収書の画像を読み取っているのは、Difyの「ビジョン機能」です。基本設定がまだの方は[【第11話】ビジョン機能の解説記事]を先にどう
2. 全体のブロック構成(最終版)
| # | ブロック名 | 種類 | 役割 |
| 1 | ユーザー入力 | 開始 | 領収書写真・申請者名・用途ヒントを入力 |
| 2 | 領収書情報抽出 | LLM①(ビジョンON) | 写真から日付・店名・金額・カテゴリを抽出 |
| 3 | JSON整形 | コード実行 | 正規表現で各項目を個別に抽出して変数化 |
| 4 | スプレッドシート記録 | HTTPリクエスト | GAS経由でGoogleスプレッドシートに追記 |
| 5 | 確認メッセージ | LLM② | 登録完了メッセージと警告を生成 |
| 6 | 最終整形 | テンプレート変換 | 完了レポートをまとめる |
| 7 | 出力 | 終了 | 最終レポートを出力 |
3. 【正直レポート】発生した失敗5つの全記録
このアプリを完成させるまでに5つの大きな壁がありました。同じ問題で詰まる読者のために全て詳しく公開します。
失敗①:LLM①がJSONの先頭を欠いて出力する
エラー:Failed to parse JSON: 2021-04-06″, “store”: “まいばすけっと… 原因:Output Formatにコードブロック記法を使ったため LLMがJSON形式を正しく出力できなかった 正しい出力:{“date”: “2021-04-06″, …} 実際の出力:2021-04-06”, …(先頭が欠けている)
解決方法:Output Formatからコードブロック記法を全て削除して「1行JSONのみを出力すること。他の文字・説明は一切不要」と明記しました。
失敗②:コード実行ブロックでJSONのパースが失敗し続ける
症状:コード実行ブロックの出力が{}(空) 原因:json.loads()でJSONを丸ごとパースしようとしたが LLMの出力が完全なJSON形式でないことがあり失敗 解決方法:json.loads()をやめて 正規表現で各項目を個別に抽出する方式に変更
最終的な解決コード(正規表現で個別抽出): import re def main(extracted_data): date_match = re.search(r'(\d{4}-\d{2}-\d{2})’, extracted_data) store_match = re.search(r'”store”\s*:\s*”([^”]+)”‘, extracted_data) amount_match = re.search(r'”amount”\s*:\s*[“\’]?(\d+)[“\’]?’, extracted_data) category_match = re.search(r'”category”\s*:\s*”([^”]+)”‘, extracted_data) purpose_match = re.search(r'”purpose”\s*:\s*”([^”]+)”‘, extracted_data) applicant_match = re.search(r'”applicant”\s*:\s*”([^”]+)”‘, extracted_data) confidence_match = re.search(r'”confidence”\s*:\s*”([^”]+)”‘, extracted_data) return {“date”: date_match.group(1) if date_match else “不明”, “store”: store_match.group(1) if store_match else “不明”, “amount”: amount_match.group(1) if amount_match else “0”, “category”: category_match.group(1) if category_match else “その他”, “purpose”: purpose_match.group(1) if purpose_match else “不明”, “applicant”: applicant_match.group(1) if applicant_match else “不明”, “confidence”: confidence_match.group(1) if confidence_match else “低”}
💡 json.loads()はJSONが完全でないと即失敗します。LLMの出力は完全なJSONを保証できないため正規表現で個別抽出する方式が安定します。
失敗③:GASのdoPost関数を直接実行してエラーになる
エラー:TypeError: Cannot read properties of undefined (reading ‘postData’) 原因:GASの実行ボタンで「doPost」を直接実行した doPost(e)はPOSTリクエストが来たときに動く関数のため 直接実行するとeがundefinedになる 解決方法:testDoPost関数を別途作成してダミーデータを渡す
テスト用関数(GASに追加する): function testDoPost() { var testData = { postData: { contents: JSON.stringify({ “date”: “2026-06-04”, “store”: “まいばすけっと 黄金町駅南店”, “amount”: “229”, “category”: “消耗品費”, “purpose”: “不明”, “applicant”: “田中” }) } }; doPost(testData); }
失敗④:GASのデプロイURLが変わってDifyと接続できなくなる
症状:GASを再デプロイするたびに新しいURLが発行される DifyのHTTPリクエストには古いURLが残る 解決方法: 「新しいデプロイ」ではなく 「デプロイを管理」→編集(鉛筆マーク) →「デプロイを更新」を使う → URLが変わらず同じURLを維持できる
⚠️ コードを修正するたびに「新しいデプロイ」を使うとURLが変わります。必ず「デプロイを更新」を使ってください。
失敗⑤:HTTPリクエストのボディで先頭の{“date”:”が欠ける
エラー:SyntaxError: Unexpected non-whitespace character after JSON at position 4 DifyからGASに送信されたデータ: 2021-04-06″, “store”: “まいばすけっと… ↑ {“date”: “が先頭から欠けている 原因:HTTPリクエストのボディに 改行やスペースが含まれていたことで 変数展開が正しく行われなかった 解決方法:ボディを完全1行・スペースなしで書き直す
正しいボディの書き方(完全1行・スペースなし): {“date”:”{{コード実行.date}}”,”store”:”{{コード実行.store}}”,”amount”:”{{コード実行.amount}}”,”category”:”{{コード実行.category}}”,”purpose”:”{{コード実行.purpose}}”,”applicant”:”{{コード実行.applicant}}”}
💡 HTTPリクエストのボディは改行やスペースが原因で変数展開がおかしくなることがあります。完全1行で書くことで解決しました。

4. 失敗から学んだ3つの教訓
教訓①:LLMへの出力指示は「見本を見せる」より「禁止事項を明記する」
「以下の形式で出力してください」と見本を示すだけでは不十分です。「バッククォートを使わない・改行しない・説明文を書かない」という禁止事項を明記することでLLMの出力が安定します。
教訓②:外部データのパースは「完璧なデータが来ない前提」で設計する
json.loads()は完璧なJSONが来ることを前提とします。LLMや外部APIのデータは予期しない形式で来ることがあります。正規表現で個別抽出する方式は多少壊れたデータでも動作するため実用的です。
教訓③:GASのテストは専用のテスト関数を用意する
doPost()のような「外部からのリクエストで動く関数」は直接実行できません。testDoPost()のようなテスト専用関数を最初から用意しておくことでデバッグ時間を大幅に短縮できます。
5. 作り方まとめ
- スプレッドシートを作成してIDをメモする
- GASにdoPost関数とtestDoPost関数を書いてデプロイする(次のユーザーとして実行:自分・アクセス:全員)
- DifyでLLM①(ビジョンON)→コード実行(正規表現)→HTTPリクエスト(raw・1行)の順で設定する
- LLM①のOutput FormatはコードブロックなしのJSON1行のみを指定する
- HTTPリクエストのボディは完全1行・スペースなしで書く
- 💡 DifyのHTTPリクエストブロックの基本設定はこちらの記事で詳しく解説しています
- 【Dify】スプレッドシート連携・完結編!HTTPリクエストでデータを飛ばそう
領収書の手入力に月80〜100分かかっていた作業が約13分に短縮されます。失敗談が多いアプリでしたが全て解決方法を公開しています。同じ問題で詰まった方はこの記事を参考にしてください。
今回はかなりはまりましたが、何度もエラーチェックして見つけたのが
かなりの凡ミスでした。凡ミスほど、見つけるのが難しいですね。
なにしろ、自分はそこは間違ってないと思い込んでるので。
でもやっと完成して良かったです!
🎁 この記事で作るアプリの「完成品」を配布しています
「読んだけど組む時間がない」という方のために、インポートするだけで動くDSLファイル+GASコード+エラー対処集をnoteで提供中です(980円)→ [こちらからどうぞ]
次におすすめの記事
【Dify実践】コピペ5分!「文字数チェッカー」を実装する完全図解マニュアル
xもやってるので良かったら見に来てください
次回も是非お楽しみに!
※以下は広告(PR)を含みます
このブログの動作環境について
当ブログはレンタルサーバー「ConoHa WING」で運営しています。IT未経験の私でもWordPressの自動インストール機能で30分でブログを開設できました。GASやDifyと連携するブログ運営を考えている方には、表示速度が速くて管理画面がシンプルなのでおすすめです。


コメント