AppSheetでアップしたPDFをGeminiでOCR【GAS】

以前、Googleドライブで電子帳簿保存法対応のアップローダとOCRを装備したことがあります。GASとGoogle DriveのOCR機能で実装したものでした。まだ生成AIもメジャーではなくVision APIなどを使ってのOCRが主流の時代。しかし扱いにくいものでした。

この時PDFをアップすると中身をOCRしてスプシに書き出していましたが、これをAppSheet + Gemini APIで出来ないか?ということで、請求書をアップするだけで電帳法対応し中身をデータとして取り出す一歩としてチャレンジしてみました。

今回利用するサンプル等

以前のGASでのアップロードとOCRに関しては以下のエントリーにまとめています。昨今はOCRは専用のAPIよりも生成AIのほうが断然読み取り精度や面倒なフォーマットなどをよしなにやってくれるので、プログラム的に取り出しやすいということで、レシート読み取りなどをやらせてみたり、カメラで取った内容から書き出しをしたりなどが盛んに行われています。

今回は業務で利用する請求書のPDFを読み取らせて、データを取り出せるかをGoogle Apps Script + Gemini APIを含めて実装しています。GeminiはGemini 2.0 Flashを利用しています。

過去にバクラクのAI-OCR使ったことがありますが非常に精度が高く便利でしたが、Gemini APIで作った今回のアプリも負けていないのではないかと思います。

Google Driveで始める電子帳簿保存 & インボイス制度対応

事前準備

Gemini APIキーを取得する

GeminiのAPIキーが必要です。以下のエントリーに独立してまとめています。課金されますので利用のしすぎには要注意。また課金されていない場合、学習に利用されてしまう恐れがあるので、きちんとGoogle Cloud上で課金アカウントとの紐付けなどをしておきましょう。

GeminiのAPIキーの取得と学習の可否

スクリプトプロパティに値を格納

前述までに取得しておいたGeminiのAPIキーおよび画像ファイルの出力先フォルダとして、GASのスクリプトプロパティに値をセットします。ただし、folderidについては後述のAppSheet側での実装の後でなければフォルダのIDが不明であるため、入れ物だけ用意しておき後で記入することになります。

  • folderid:出力先フォルダのID
  • geminikey : Gemini APIのAPIキー

図:2つ値をセットしておく

スタンドアローンのGASの準備

AppSheetテンプレートをコピーした後はGASへの参照が切れています。故に自身でスタンドアローンコンテナのGASファイルを作成して繋げてあげる必要があります。ここではそのGASのファイルの作成手順です。

  1. Google Driveを開いて、左上の新規をクリック
  2. その他 => Google Apps Scriptをクリック
  3. スクリプトエディタが直接開いて警告文が出るのでそのまま作成を続行する
  4. ドライブにはスタンドアローンファイルが生成される
  5. スタンドアローンスクリプトをダブルクリックしてスクリプトエディタを開く
  6. 左上のプロジェクト名がファイル名になるので、無題のプロジェクトではなくきちんと名前を付けましょう。(今回は緯度経度変換スクリプトという名前にしています)。
  7. 後述のGoogle Apps Scriptをコピー&ペーストして保存する
  8. AppSheetのAutomation内のCall a scriptで参照してるGASを今回作ったものに差し替えます

見つからない場合には、Chrome Webstoreから拡張機能インストールしましょう(Google Apps Scriptで検索すると出てくる)。

図:新規から作成することが出来る

技術的ポイント

AppSheet側での実装

テーブルの設定

請求書シートを取り込み後にファイル列にはファイルのパスが入ることを想定するので、データの型を「File」に変更しておきます。これだけでAppSheet上ではクリックするだけでローカルからのアップロードや、ドラッグアンドドロップでファイルを簡単にアップロードできるようになります。

ただしファイルのIDはAppSheetでは取得できずファイル名が入ることになるため、IDを取得するためのロジックをGAS側で実装する必要があります。

図:File型にしておくのがポイント

アップロードフォルダ

前述のアップロードしたファイルは、スプレッドシートがある場所にシート名にてフォルダが自動作成されてそこに格納されるので、パスとしては「請求書_Files_/ファイル名」といった形で自動で格納されます。一個試しにアップしてみてフォルダができるかどうか?確認しましょう。

このフォルダのIDを前述のfolderidに格納しておきます。GASでこのフォルダ内でファイル名を手がかりにファイルを特定してピックアップしますので、重要な値になります。

※この時アップされるファイル名は重複を避ける為にAppSheet側でランダムなファイル名としてアップされています。

図:フォルダが自動作成されます

Google Apps Script側の実装

今回のアプリの最も肝になる部分のGeminiにファイルを投げてOCRしてもらい、登録したレコードに取得したデータを追記するコードになります。アップロード後のAutomationで作動し、レコードのIDとファイル名を引数として渡して完了したらTrue/Falseで返すようにしています。事前に読み書き先のスプシのIDをコード内に記述しておいてください。

ポイントとしては

  • 指定のフォルダ内だけをターゲットにファイル名で特定しアップロードしたファイルIDを取得する
  • 画像の時と同様にBASE64でファイルはエンコードする
  • プロンプトにて詳細な請求書データの読み方と事例を記述して配列データで返してもらう
  • 返り値内に余計な文字が付くのでこれを除外する
  • 1行目がタイトル行なので2行目のみを対象にデータを拾って、対象のレコードに追記する

といった流れになります。自分のサンプルの場合かなり綺麗に取得することが出来ました。こういった配列でよしなにしてくれるというのがCloud Vision APIでは出来ないので、Gemini APIでOCRはその辺りが実に優秀です。

正直過去のOCRアプリケーション時代を知ってると、生成AIのOCR精度はものすごく高くてびっくりするレベルです。

図:OCRの精度が半端ない

//Gemini 2.0エンドポイントURL
var endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=";

//読み書き先スプシのID
var ssid = "ここに読み書き先のスプシのIDを入れる";

//ファイル名から特定しGeminiに読み取らせる
function geminiReader(target,id) {
  //targetからファイル名だけ取り出す
  let fileman = target.split("/");

  //プロパティの値を取得する
  let prop = PropertiesService.getScriptProperties()
  let folderid = prop.getProperty("folderid");
  let apikey = prop.getProperty("geminikey");

  //リクエストURLを構築する
  let url = endpoint + apikey;

  // ファイルを特定しファイルIDを取得
  let fileId,fileblob;
  const folder = DriveApp.getFolderById(folderid);
  const files = folder.getFiles();
  while (files.hasNext()) {
    const file = files.next();
    const fileName = file.getName();

    //targetのファイル名と一致するか?
    if(fileName == fileman[1]){
      //ファイルIDとBlobを取得する
      fileId = file.getId();
      fileblob = file.getBlob();
      break
    }
  }

  //base64エンコードする
  let pdf = Utilities.base64Encode(fileblob.getBytes());  

  //プロンプトを構築する
  let prompt = `あなたは経理の担当者です。アップロードされたPDFデータより数字や文字を読み取って文字起こしをしてください。
    #重要次項
    - 不明な項目がある場合は、正直に「N/A」と記入してください。
    - 取得した値はダブルコーテーションで括ってください
    - 回答は配列に格納して返してください
    - 出力事例に従ったデータだけを返してください。余計な解説や説明は一切不要です。

    #請求書の項目の説明
    1. 請求元の会社名
    2. 請求書発行日付
    3. 請求金額合計
    4. Tの文字からはじまる14桁の適格請求書発行事業者に発行される番号
    5. 消費税8%部分の金額
    6. 消費税10%部分の金額

    #出力する内容
    出力事例に従った配列形式でデータは返してください。

    #出力事例
    [
      ["請求元会社名","請求書発行日付","請求金額合計","インボイス番号", "消費税8%", "消費税10%"],
      ["株式会社xxxx", "2024/5/3", "45000", "T1234123412341", "2800",""3500]
    ]
  `;

  //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 array = JSON.parse(ret);
    let setman = array[1];
    
    //レコードを特定して追記する
    let ss = SpreadsheetApp.openById(ssid);
    let sheet = ss.getSheetByName("請求書");
    let targetsheet = sheet.getRange("A2:K").getValues();

    for(let i = 0;i<targetsheet.length;i++){
      //レコードを一個取り出す
      let rec = targetsheet[i];

      //IDと一致するか?
      if(id == rec[0]){
        //書き込みポイントを特定する
        let point = i + 2;

        //値を書き込む
        sheet.getRange("D" + point).setValue(setman[3]);    //インボイス番号
        sheet.getRange("E" + point).setValue(setman[1]);    //取引日
        sheet.getRange("F" + point).setValue(setman[0]);    //取引先
        sheet.getRange("G" + point).setValue(setman[2]);    //請求金額
        sheet.getRange("H" + point).setValue(setman[4]);    //消費税8%
        sheet.getRange("I" + point).setValue(setman[5]);    //消費税10%
        sheet.getRange("J" + point).setValue(fileId);       //ファイルID
        sheet.getRange("K" + point).setValue("完了");        //ステータス
        break;
      }
    }

    //処理完了
    return true;
  } else {
    return false;
  }
}

請求明細含めて全部取得する

前述までのGASのコードは請求書の外側のデータの取得に限られています。しかし実際の請求書は明細データもあるわけで、会計ソフトなどと連動させたい場合には欠かせないデータです(当然項目により勘定科目も異なるわけで)。

そこで、同じくGemini 2.5 ProのAPIを利用して明細部分まで含めて全部引き抜くものを以下のエントリーに独立させて作りました。AppSheet向けにコードを書いていませんが、改造すればAppSheet連動用に使えます。

Google Apps ScriptとGeminiで請求書データを全部抽出する【GAS】

使ってみる

使う側の手順は簡単。今回のサンプルを開いて以下の手順でファイルをアップするだけです。

  1. 請求書アップの右下+ボタンをクリックする
  2. ファイルのところにドラッグアンドドロップするか?ファイルを選択します。(ローカルからアップロードします)
  3. 保存をクリックするとアップロードされます。
  4. 同時に後ろでGemini問い合わせが実行されて、OCRされた内容が対象レコードに追記されます。
  5. 対象のレコードを開いてみると見事に取り出された値が格納されています。

これで受け取った請求書についての電子帳簿保存法に従った格納先やタイムスタンプ、ラベルなどを付けるスクリプトなどを整備してあげれば、請求書の読み取りだけでなくその後の会計ソフトへの流し込みなども非常に楽になるでしょう。

※今回の事例では「請求明細」については読み取り指示をしていないので、追加で請求明細も取得するようなプロンプトや出力指示を加えると尚良いでしょう。

図:アップロードはとても簡単

図:追記された各種データ

AI Task機能で実現する

2025年4月9日より、AppSheet Enterprise Plusで利用可能になったGemini連動のオートメーションの新機能である「AI Task」。現在はまだプレビュー版扱いという点と、これによりできる内容は限られていますが、主に

  • PDFなどを読み取って1行のレコードに起こして取り込み(OCR的な機能)
  • 画像などを読み取ってカテゴライズ情報を取り込み

といった事がGoogle Apps Script無しで実現出来ています。今回の取組のような請求書の外側の情報を取り込みに関して言えば、かなり実現出来ていますが、明細情報の取得はAppSheet自身の機能不足や制約により簡単には実現出来ません。このあたりの取り組みに関しては別エントリーを用意しましたので、以下のエントリーを参照してください。

AppSheetのAI Taskで請求書からデータ抽出

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)