AppSheetで名刺読み取り管理ツールを作る【GAS】

AppSheetやGoogle Apps Scriptを活用できれば、社内から余計なSaaSを排除し、管理コストや運用コストを減らすことができます。その高機能なサービス本当に必要ですか?と問うた時に、殆どの中小では不要というものは今後生成AIによって淘汰されていくことでしょう。何でもサブスクな重課金地獄とはおさらばしましょう。

そういったものの1つに名刺管理アプリがあります。AppSheet + Gemini APIを使って2時間くらいで作ってみました。

今回利用するツール等

請求書読み取りのGoogle Apps Scriptのコードや、PDFをアップロードしてOCRするAppSheetの作品を過去作って公開しています。これらは難易度も高めながら、Gemini APIはしっかり仕事をしてくれて、高精度で中身を読み取りデータとして整形して返してくれています。

今回はそれよりもずっと簡単ながら、日常で使う機会が多いと思われる「名刺読み取り」→データ化についてAppSheet, Google Apps Script, Gemini APIで実現しました。スマホで写真を取ってその場で読み取り実行しデータ化できます。

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

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

事前準備

Gemini APIキーを取得する

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

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

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

前述までに取得しておいたGeminiのAPIキーを格納する先として、GASのスクリプトプロパティに値をセットします。AppSheetでテストで画像をアップするとスプシと同じ場所に名刺管理_Imagesというフォルダができるので、そのフォルダのIDを取得しておきましょう。後述のGASの中で記述します。

  • apikey : 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側での実装

テーブルの設定

名刺管理シートを取り込み後に名刺画像列には画像へのパスが入ることを想定するので、データの型を「Image」に変更しておきます。これだけでAppSheet上ではクリックするだけでローカルからの写真の選択だけじゃなく、カメラで直接撮影して登録が可能になります。

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

図:Image型の列の指定

アップロードフォルダ

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

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

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

図:アップロードフォルダが自動で作成される

Google Apps Script側の実装

ソースコード

請求書よりも読み取り項目が少ないのと、わりとわかりやすい配置になってはいるものの、名刺の場合そのレイアウトが独特なケースも多いです。また、人によっては役職があったり無かったりも普通にありえることで、無い場合にはN/Aで返すように指示を出しています。

返り値はJSON形式で具体的にどんな値をそこにいれるのか?読み取るべき項目と事例をつけて、指示を出します。最後にAppSheet側からの引数であるidの値をもって、行を特定しOCRで読み取った値を格納していくという流れになっています。

ポイントとしては

  • 指定のフォルダ内だけをターゲットにファイル名で特定しアップロードしたファイルIDを取得する
  • PDFの時と同様にBASE64でファイルはエンコードする
  • プロンプトにて詳細な名刺の画像データの読み方と事例を記述して配列データで返してもらう
  • 返り値内に余計な文字が付くのでこれを除外する
  • スプレッドシートに読み取ったデータを格納して完了

また名刺の場合は斜めになっていたり、横で撮影したり、暗かったりなど様々な状態がありますが、少なくとも適当にすこし暗い場所で、横撮影した程度ではバッチリ読み取ってくれました。これは正直すごいなと思う点。

//モデル
const model = "gemini-2.5-pro";

//エンドポイントURL
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=`;

//画像格納フォルダのID
const folderid = "ここに画像が格納されてるフォルダのIDを入れる";

//読み書き先スプシのID
const ssid = "読み書き先のスプレッドシートのIDを入れる";

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

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

  //リクエスト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 meisi = Utilities.base64Encode(fileblob.getBytes()); 

  //プロンプトを構築する
  let prompt = `あなたは外回りの営業マンです。アップロードされた画像データは名刺であり、そこから項目を読み取りデータを返してください。

    #重要次項
    - 不明な項目がある場合は、正直に「N/A」と記入してください。
    - 取得した値はダブルコーテーションで括ってください
    - 回答は配列に格納して返してください
    - 出力事例に従ったデータだけを返してください。余計な解説や説明は一切不要です。
    - 名刺から読み取る項目を取得し、出力事例に従ったJSON形式でデータを返してください。

    #名刺から読み取る項目
    1. 会社名
    2. 名刺の氏名
    3. 所属部署名
    4. 名刺本人の役職名(部長や課長など)
    5. 電話番号
    6. メールアドレス

    #出力する内容
    出力事例に従った配列形式でデータは返してください。読み取り項目の格納先各要素名は以下の通り
    - 会社名は、company
    - 名刺の氏名は、name
    - 所属部署名は、busyo
    - 名刺本人の役職名は、jobtitle
    - 電話番号は、phonenumber
    - メールアドレスは、mail

    #出力事例
    {
      "company" : "谷山ベース整備店",
      "name" : "相良左之助",
      "busyo" : "メンテナンス部",
      "jobtitle" : "チーフマネージャー",
      "phonenumber" : "090-1234-5678",
      "mail" : "sagara@taniyamabase.com"
    }
  `;

  //payloadを構築する
  let payload = {
    'contents': {
      'parts': [
        {
          'inlineData': {
            'mimeType': 'image/jpeg',
            'data': meisi
          }
        },
        {
          '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 temparr = JSON.parse(ret);

    //レコードを特定して追記する
    let ss = SpreadsheetApp.openById(ssid);
    let sheet = ss.getSheetByName("名刺管理");
    let targetsheet = sheet.getRange("A2:I").getValues();

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

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

        //値を書き込む
        sheet.getRange("B" + point).setValue(temparr.company);
        sheet.getRange("C" + point).setValue(temparr.name);
        sheet.getRange("D" + point).setValue(temparr.busyo);
        sheet.getRange("E" + point).setValue(temparr.jobtitle);
        sheet.getRange("F" + point).setValue(temparr.phonenumber);
        sheet.getRange("G" + point).setValue(temparr.mail);
        break;
      }
    }

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

現在の課題点

個人的に請求書と違って、名刺の場合においてのGemini API、AppSheetの不満点。それは

  • Gemini 2.5 Proには、2.0 flash preview Image Generationのような画像の再編集機能が備わっていない(別のAPIのエンドポイントを叩く必要がある)
  • 横撮影すると、AppSheet上では横倒しで画像が表示される
  • 名刺以外の切り抜きも同様で、2.5 Proに再編集が備わっていないので、現在はできない。
  • よって、画像の再編集とテキスト読み取り指示は別々に出す必要性があるので、2回叩く必要がある。

なぜ、未だにGeminiアプリに対しても画像の再編集機能が降りてこないのか?が不明。このあたりはChatGPTでは既に実装し公開済みの機能となってるので、Geminiが明確に遅れを取ってる領域です。

使ってみる

使い方自体は非常に簡単。以下の手順でさっくり登録することが可能です。

  1. AppSheetアプリの右下の+ボタンをクリック
  2. 1つだけ名刺画像というフィールドがあるので、カメラアイコンをタップする。
  3. PCの場合はアップロードのみ。スマホの場合はアップロードか?カメラ起動で撮影する
  4. SAVEをクリックするとAutomationが作動して、GASでGemini APIリクエストが実行される。
  5. 返ってきた値を取得できると、他の項目が埋まって、表示される

もちろん文字起こしされてるわけなので、検索もできますし画像の置き換えも可能。試しにサンプル画像をアップして読み取れるか?やってみました。間違ってpng画像をあげてしまった・・・mimetypeでjpeg指定なのに。

しかし、問題なく読み込めて、OCRも無事に完了しました。

図:画像のアップロード画面

図:無事に名刺を読み取り成功しました

関連リンク

コメントを残す

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

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