Google Apps ScriptとGeminiで請求書データを全部抽出する【GAS】
以前、AppSheet + Google Apps Script + Gemini APIで請求データをOCRして請求書データを引き抜くものを作成しました。しかし、前回のエントリーでは請求明細までは取得出来ていませんでした。
また、2025年4月9日にAppSheet Enterprise Plusライセンス向けに、AI Taskと呼ばれる新機能が追加され、簡単に請求書のデータを抽出出来るようになったものの、やはり請求明細については簡単に取り出せず。そこで、今回は前回の課題である「請求明細」まで含めた全データを抽出してスプシに書き出すものを、Gemini APIを利用して構築してみました。
目次
今回利用するスプレッドシート等
- 請求明細取り込み - Google Spreadsheet
- Gemini API Reference
- サンプルの請求書PDFファイル
今回は、現在まだプレビューリリース段階のGemini 2.5 ProのAPIを使って、データを生成AI経由でOCRにかけて、請求書データおよびその中にある請求明細データを一度に取得して、請求書および明細のシートにそれぞれ一括で書き出すものを作成しています。
途中まではAppSheetでアップしたPDFをGeminiでOCRと同じですが、今回のコードはスプシで利用することを前提としています。加工し直せばAppSheetでも十分利用することが可能です。その為、前回とは少々コードを変えていますので注意。
AppSheetのAI Taskで8時間頭捻って明細が結局スマートに取得出来なかったのが、GASだと1時間で簡単に構築出来ました。ノーコードツールは楽な面は楽ですが、やはりちょっと踏み込むとすぐに限界に達するのが難点。
Gemini APIを利用する利点
AI Task機能は別途AppSheet Enterprise Plusライセンスが必要です。そのため追加で購入が必要な上に、1ライセンスあたり1000クレジット/月が上限となっているため、リクエスト回数が多いと上限に達してしまい使えなくなってしまいます。
一方、GASからGemini APIをリクエストする場合には、リクエストに応じた課金は必要なものの、そのリクエストに大きな制限はありません(プログラム的なQuotaの制約はありますが)。もちろん、CoreプランであってもGASからAPIを叩いてる仕組みなので、Enterprise Plusライセンスは不要です。
概ね現場で利用するGemini APIのリクエストのパターンは決まっているので、1つコードを構築できれば簡単に使い回せるだけでなく、パターンが決まり事ということはメンテナンス性でも優れているので、個人的には無理にAppSheetでロジックを構築するよりもGemini APIを叩くGASアプリを構築したほうが柔軟性も高いと考えています。
また、AI Taskは現在2種類の作業しか出来ませんが、Gemini APIの場合はプロンプト次第で様々なことが可能になり縛られないので、ぜひ頑張ってGASの習得をおすすめします。
事前準備
Gemini API Keyを取得する
GeminiのAPIキーが必要です。以下のエントリーに独立してまとめています。課金されますので利用のしすぎには要注意。また課金されていない場合、学習に利用されてしまう恐れがあるので、きちんとGoogle Cloud上で課金アカウントとの紐付けなどをしておきましょう。
スクリプトプロパティに格納する
今回取得したGeminiのAPI Keyはapikeyというスクリプトプロパティの中に格納しています。コード内に直接記述した場合、誤って流出するおそれがあるので、スクリプトプロパティを使うか?共通で利用するJSONファイルなどを用意して、参照して読み込むように、コードとは分離して管理するのが望ましいでしょう。
図:apikeyをスクリプトプロパティに格納する
エンドポイントURLの確認
今回利用してるGemini APIのエンドポイントURLですが、2.5 Proはまだプレビュー段階でLastestが存在しません。よって、変更されたり削除される可能性もあります。とりあえずは以下のURLで構築していますが、都度最新版のエンドポイントURLを確認して、安定版が出たら、そちらを利用するようにしましょう。
1 |
const endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent?key="; |
ソースコード
冒頭のseikyu変数に請求書PDFのファイルIDを入れておきます。また、スクリプトプロパティからGemini APIのAPIキーを取得してendpointのURLと結合してリクエストしています。
前回の記事との相違点ですが
- 請求書データと請求明細データを一括で取得し、JSON形式でまとめて出力させています。
- それぞれのデータにはタイトル行があるので出力時はこのタイトル行は除外しています。また型変換を行って一時配列に格納してから、一括書き出ししています。
- 請求データは1行なのでappendRowで請求書シートの行末に追記
- 明細データは複数行なのでgetLastRowにて最終行を取得して明細シートの行末に一括追記
さらに応用して、書き出しデータを会計ソフトにインポート出来るようなスタイルで出力(勘定科目や借方・貸方などの生成)をしてあげる会計ソフト連動や、また履歴書データの読み込みならばそのままSmartHRなどにAPI連携でインポートなども可能になるかと思います。
現時点でAppSheetのAI Taskがいまいちな出来なので、GASで普通に取り込みをやらせたほうが短時間でスマートに開発が可能です。
1 |
const endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent?key="; |
図:請求書PDFから明細部分を取り出せました
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
//請求書ファイル const seikyu = "請求書PDFのファイルのIDをここに入れる"; //Gemini 2.5 ProエンドポイントURL const endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent?key="; //ファイル名から特定しGeminiに読み取らせる function geminiReader() { //プロパティの値を取得する let prop = PropertiesService.getScriptProperties(); let apikey = prop.getProperty("apikey"); //リクエストURLを構築する let url = endpoint + apikey; // ファイルを特定しファイルIDを取得 let file = DriveApp.getFileById(seikyu); let fileblob = file.getBlob(); //base64エンコードする let pdf = Utilities.base64Encode(fileblob.getBytes()); //プロンプトを構築する let prompt = `あなたは経理の担当者です。アップロードされたPDFデータより数字や文字を読み取って文字起こしをしてください。 #重要次項 - 不明な項目がある場合は、正直に「N/A」と記入してください。 - 取得した値はダブルコーテーションで括ってください - 回答は配列に格納して返してください - 出力事例に従ったデータだけを返してください。余計な解説や説明は一切不要です。 #請求書の項目の説明 1. 請求元の会社名 2. 請求書発行日付 3. 請求金額合計 4. Tの文字からはじまる14桁の適格請求書発行事業者に発行される番号 5. 消費税8%部分の金額 6. 消費税10%部分の金額 #請求書の中の明細についての説明 請求書の中にある請求に対する明細については4つの項目を取得してください 1. 品目や品名などの請求に対する項目名を「品目」として取得 2. 数量はその数を取得 3. 単価はその品目に対する単価にあたる金額を取得 4. 金額は数量 * 単価の結果の金額を取得 5. 明細は複数行ある場合があるのでそれを考慮して配列で取得 #出力する内容 出力事例に従ったJSON形式でデータは返してください。請求書の項目についてはseikyuという中に出力し、明細についてはmeisaiという中に出力してください。 #出力事例 { "seikyu" : [ ["請求元会社名","請求書発行日付","請求金額合計","インボイス番号", "消費税8%", "消費税10%"], ["株式会社xxxx", "2024/5/3", "45000", "T1234123412341", "2800",""3500] ], meisai : [ ["品目","数量","単価","金額"], ["◯▲お品代","2","2500","5000"], ["付属費用","1","1000","1000"], ] } `; //payloadを構築する let payload = { 'contents': { 'parts': [ { 'inlineData': { 'mimeType': 'application/pdf', 'data': pdf } }, { 'text': prompt } ] }, 'generation_config': { 'temperature': 0, 'topP': 0, 'maxOutputTokens': 8192 } }; //リクエストオプション let options = { 'method': 'POST', 'contentType': 'application/json', 'payload': JSON.stringify(payload) }; //Geminiにリクエスト let response = UrlFetchApp.fetch(url, options); let json = JSON.parse(response.getContentText()); if (json && json.candidates && json.candidates.length > 0) { let result = json.candidates[0].content.parts[0].text; //余計な文字を除外する let ret = result.replace("```json",""); ret = ret.replace("```",""); ret = ret.replace("\n",""); //請求と明細データを別個に取り出す let jsondata = JSON.parse(ret) let seikyudata = jsondata.seikyu; let meisaidata = jsondata.meisai; //取得データをスプシに書き出し let ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getSheetByName("請求書"); let sheet2 = ss.getSheetByName("明細"); //uuidを生成する let uuid = Utilities.getUuid(); //請求データを整形する let target = seikyudata[1] let seikyuarr = [ uuid, target[3], target[1], target[0], Number(target[2]), Number(target[4]), Number(target[5]) ] //請求データを書き出す sheet.appendRow(seikyuarr); //明細データを書き出す let meisaiarr = []; for(let i = 1;i<meisaidata.length;i++){ //レコードを1個取り出す let rec = meisaidata[i]; //uuidを生成 let uuid2 = Utilities.getUuid() //一時配列を用意 let temparr = [ uuid2, uuid, rec[0], Number(rec[1]), Number(rec[2]), Number(rec[3]) ] //明細配列に追加 meisaiarr.push(temparr); } //明細側に追記する let endrow = Number(sheet2.getLastRow()) + 1; let lastColumn = meisaiarr[0].length; //カラムの数を取得する let lastRow = meisaiarr.length; //行の数を取得する sheet2.getRange(endrow,1,lastRow,lastColumn).setValues(meisaiarr); //処理完了 return true; } else { return false; } } |