GASで名刺画像をAIで読み取り
スプレッドシートに自動登録する方法

もらった名刺を1枚ずつ見ながら、会社名・氏名・電話・メールを表に打ち込む——この作業は自動化できます。DriveApp(GASからGoogleドライブを操作する標準機能)で名刺の写真を取り出し、Claude API(Anthropic社の生成AI)に 画像ごと渡して項目を読み取らせます。OCRライブラリも位置判定も使わない実装を、動くコードで解説します。

|対象: GAS / Googleドライブ / Claude API / 名刺管理

Table of Contents

名刺のデータ化が手作業になりがちだった理由

結論から言うと、名刺のデータ化はGAS(Google Apps Script。Googleサービスを操作する無料のプログラム実行環境)と AIで自動化できます。これまで手打ちが残っていた理由は、はっきりしています。名刺のレイアウトが1枚ごとに違うからです。

会社名が上とは限りません。ロゴだけの会社もあります。電話とFAXと携帯が縦に並び、ラベルも「TEL」「Tel」「電話」とばらばら。 画像から文字を抜き出して位置やキーワードで項目を当てる従来のOCRでは、名刺のデザインが変わるたびに判定が崩れました。 今回の方法は、その判断をすべてAIに任せます。人間が名刺を見て「これが会社名、これが役職」と分かるなら、AIも同じように読めます。

取り出す

Driveの未処理フォルダから名刺画像を取得。1回の実行で扱う件数に上限をかける

読ませる

画像をBase64に変換してClaude APIへ送信。項目をJSONで返させる

残す

重複を除いてシートに登録し、画像を処理済みフォルダへ移動する

名刺は個人情報です。だからこそ、確信度が低い項目は人が見直す前提で組みます。この記事の実装は「入力の手間をゼロにする」ものであって、「確認をゼロにする」ものではありません。 そのために、AIが読み取った確信度も一緒にシートへ残します。

準備:フォルダを2つ用意し、APIキーを保管する

Googleドライブに「名刺_未処理」「名刺_処理済み」の2つのフォルダを作ります。 撮った名刺の写真を未処理フォルダに置くだけで取り込まれ、成功したものは処理済みへ移動する——という流れです。フォルダの位置が、そのまま処理状態を表します。管理用のフラグ列を作るより、目で見て分かるぶん壊れにくい設計です。

登録先のスプレッドシートを開き、拡張機能 → Apps Script からスクリプトを作ります。 シート名は「名刺」とし、1行目に見出し(取込日時・会社名・氏名・ふりがな・部署・役職・電話・携帯・メール・FAX・URL・住所・確信度・リンク)を入れておきます。 APIキーとフォルダIDは、コードに直接書かずスクリプト プロパティへ保存します。

// スクリプトプロパティに以下を保存しておく
//   ANTHROPIC_API_KEY : AnthropicのAPIキー
//   INBOX_FOLDER_ID   : 未処理の名刺画像を置くDriveフォルダのID
//   DONE_FOLDER_ID    : 処理済み画像の移動先フォルダのID
function getProp(name) {
  const value = PropertiesService.getScriptProperties().getProperty(name);
  if (!value) throw new Error(name + " が未設定です。");
  return value;
}

// フォルダIDは、Driveでフォルダを開いたときのURL末尾の文字列
// https://drive.google.com/drive/folders/【この部分】

フォルダIDは、Driveでフォルダを開いたときのURLの末尾です。 APIキーをコードに直書きすると、スクリプトを共有した相手に鍵ごと渡ることになります。PropertiesService(スクリプトごとの設定値を保存するGASの仕組み)を使ってください。

Driveから名刺画像を取り出す(件数の上限を決める)

フォルダ内の画像を列挙します。ここで大事なのは、1回の実行で処理する件数に上限をかけることです。GASの1回の実行は最大6分。名刺1枚の読み取りに数秒かかるため、 展示会後に100枚まとめて置かれると途中で強制終了します。

// 未処理フォルダから画像ファイルだけを取り出す
// 1回の実行で扱う件数に上限を設け、6分の実行時間制限に収める
const MAX_FILES_PER_RUN = 15;

// Claude APIが受け取れる画像形式だけを対象にする
const SUPPORTED_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];

function listCardImages() {
  const folder = DriveApp.getFolderById(getProp("INBOX_FOLDER_ID"));
  const it = folder.getFiles();

  const files = [];
  while (it.hasNext() && files.length < MAX_FILES_PER_RUN) {
    const file = it.next();
    // 画像以外(メモやPDF)はスキップする
    if (SUPPORTED_TYPES.indexOf(file.getBlob().getContentType()) !== -1) {
      files.push(file);
    }
  }
  return files;
}

PDF請求書の記事ではgetFilesByType(MimeType.PDF)で一発に絞れましたが、画像はJPEG・PNGなど複数の形式があります。そこでgetContentType()でファイルの種類を調べ、Claude APIが受け取れる形式(JPEG / PNG / GIF / WebP)だけを対象にしています。 上限に達しなかった画像は未処理フォルダに残り、後述のトリガーで次の回に片付きます。

画像をそのままAIに渡して項目を抽出する

この記事の核心です。Claude APIは、メッセージの中身としてimage型のブロックを受け取れます。名刺のバイナリをBase64(バイナリを文字列に変換する方式)にして渡せば、 文字もレイアウトも含めて解釈してくれます。 専用のOCRエンジンを別途契約しなくても、AIひとつで文字認識と項目の振り分けが済む理由がこれです。

// 名刺画像そのものをClaude APIに渡し、項目をJSONで受け取る
function extractCard(file) {
  const url = "https://api.anthropic.com/v1/messages";

  const blob = file.getBlob();
  const mediaType = blob.getContentType();          // 例: "image/jpeg"
  const base64 = Utilities.base64Encode(blob.getBytes()); // 画像を文字列に変換

  const instruction =
    "添付の画像は名刺です。以下のJSONだけを出力してください。\n" +
    "前置きも説明文もコードブロックの記号も付けないでください。\n\n" +
    "{\n" +
    '  "company": "会社名",\n' +
    '  "name": "氏名",\n' +
    '  "kana": "氏名のふりがな。記載がなければ空文字",\n' +
    '  "department": "部署名",\n' +
    '  "title": "役職",\n' +
    '  "phone": "電話番号(TEL)",\n' +
    '  "mobile": "携帯番号",\n' +
    '  "fax": "FAX番号",\n' +
    '  "email": "メールアドレス",\n' +
    '  "website": "URL",\n' +
    '  "address": "住所",\n' +
    '  "confidence": "high | medium | low"\n' +
    "}\n\n" +
    "ルール:\n" +
    "・名刺に書かれていない項目は空文字 \"\" にする。推測で埋めない。\n" +
    "・TELと明記された番号を phone、FAXと明記された番号を fax に入れる。\n" +
    "・ラベルのない番号は phone に入れる。携帯番号は mobile に入れる。\n" +
    "・文字がかすれて読み取りに自信がない場合は confidence を low にする。";

  const payload = {
    model: "claude-haiku-4-5",
    max_tokens: 1000,
    messages: [{
      role: "user",
      content: [
        {
          type: "image",
          source: {
            type: "base64",
            media_type: mediaType,
            data: base64
          }
        },
        { type: "text", text: instruction }
      ]
    }]
  };

  const res = UrlFetchApp.fetch(url, {
    method: "post",
    contentType: "application/json",
    headers: {
      "x-api-key": getProp("ANTHROPIC_API_KEY"),
      "anthropic-version": "2023-06-01"
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true // エラー時も例外にせずレスポンスを受け取る
  });

  if (res.getResponseCode() !== 200) {
    throw new Error("API エラー (" + res.getResponseCode() + "): " + res.getContentText());
  }

  const text = JSON.parse(res.getContentText()).content[0].text;
  return parseJson(text);
}

画像形式はgetContentType()で取得した実際の種類をmedia_typeにそのまま渡しています。JPEGなのに「png」と申告するとエラーになるため、決め打ちにせずファイルから読むのが安全です。

プロンプト(AIへの指示文)で決めているのは3点です。出力をJSONだけに固定すること。書かれていない項目は推測で埋めず空文字にすること。そして電話・FAX・携帯を明記のラベルで振り分けること。名刺で最も混ざりやすいのが番号の種類なので、 「ラベルのない番号は phone に入れる」と迷ったときの入れ先まで指定しておくと、結果が安定します。

AIの返答を安全にJSONへ変換する

「JSONだけを出力して」と指示しても、まれに前後に説明文やコードブロックの記号が付きます。 返答をそのままJSON.parse()に渡すと、そこで処理が落ちます。最初の{から最後の}までを切り出してから変換します。

// AIの返答からJSONを取り出す
// 「JSONだけ」と指示しても前後に文が付くことがあるため、最初の { から最後の } までを拾う
function parseJson(text) {
  const start = text.indexOf("{");
  const end = text.lastIndexOf("}");
  if (start === -1 || end === -1) {
    throw new Error("JSONが見つかりません: " + text.slice(0, 200));
  }
  return JSON.parse(text.slice(start, end + 1));
}

// メールアドレスを照合用に正規化する(前後の空白を除き、小文字にそろえる)
function normalizeEmail(value) {
  return String(value || "").trim().toLowerCase();
}

あわせて、メールアドレスを照合用に整える関数も用意します。 名刺のメールは大文字・小文字が混ざったり、前後に空白が入ったりします。 次のセクションの重複チェックでSales@example.comsales@example.comを別人と誤判定しないよう、小文字にそろえて比べます。

メールアドレスで重複登録を防ぐ

名刺は同じ人から何度ももらいます。異動のたび、再訪のたびに1枚ずつ増えるので、重複対策をしないと同じ人が何行にもなります。ここではメールアドレスをキーにして、すでに登録済みの相手をスキップします。

// すでに登録済みのメールアドレスの集合を作る
// 名刺は同じ人から何度ももらうため、メールで重複を防ぐ
function loadKnownEmails(sheet) {
  const last = sheet.getLastRow();
  const known = {};
  if (last < 2) return known; // 見出し行だけなら空

  // メールアドレスは9列目(見出しの並びに合わせて調整する)
  const values = sheet.getRange(2, 9, last - 1, 1).getValues();
  values.forEach(function (row) {
    const email = normalizeEmail(row[0]);
    if (email) known[email] = true;
  });
  return known;
}

登録の前に一度だけシートのメール列を読み込み、{ email: true }の形の集合を作っておきます。行ごとにシートを検索し直すと遅くなるため、 まとめて読んでメモリ上で照合するのがコツです。 メールが空の名刺もあるので、その場合は重複チェックを飛ばして登録します。 メールなしの相手まで確実に一意にしたいときは、会社名と氏名を組み合わせたキーに変えてください。

シートに登録し、トリガーで自動実行する

すべてをつなぐ本体です。1件ずつtryで囲むのがポイントです。1枚の名刺が読み取れなくても、残りの14枚は処理されます。

// 本体:名刺画像を1件ずつ読み取り、シートに登録して、処理済みへ移す
function importNameCards() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("名刺");
  const doneFolder = DriveApp.getFolderById(getProp("DONE_FOLDER_ID"));
  const files = listCardImages();
  const known = loadKnownEmails(sheet);

  files.forEach(function (file) {
    try {
      const data = extractCard(file);
      const email = normalizeEmail(data.email);

      // 同じメールがすでにあれば、登録せず処理済みへ移すだけ
      if (email && known[email]) {
        file.moveTo(doneFolder);
        return;
      }

      sheet.appendRow([
        new Date(),               // 取込日時
        data.company || "",       // 会社名
        data.name || "",          // 氏名
        data.kana || "",          // ふりがな
        data.department || "",    // 部署
        data.title || "",         // 役職
        data.phone || "",         // 電話
        data.mobile || "",        // 携帯
        data.email || "",         // メール(9列目:重複チェックのキー)
        data.fax || "",           // FAX
        data.website || "",       // URL
        data.address || "",       // 住所
        data.confidence || "",    // 確信度
        file.getUrl()             // 元画像へのリンク
      ]);

      if (email) known[email] = true; // 同じ実行内での二重登録も防ぐ
      file.moveTo(doneFolder);        // 登録に成功した画像だけ移動する

    } catch (e) {
      // 1件の失敗で全体を止めない。画像は未処理フォルダに残す
      console.error(file.getName() + ": " + e.message);
    }
  });
}

file.moveTo(doneFolder)appendRow()の後に置いているのには理由があります。順序が逆だと、登録に失敗した画像が処理済みフォルダへ消えていきます。記録してから移動する。この順序を守れば、失敗した名刺は未処理フォルダに残り、原因を直したあと再実行するだけで復旧します。 重複でスキップした画像も処理済みへ移し、未処理フォルダに残り続けないようにしています。

最後に、時間主導トリガー(決まった間隔で関数を自動実行するGASの仕組み)を仕掛けます。 トリガー登録の関数は一度だけ手動で実行してください。

// 30分おきに importNameCards を自動実行する
// この関数は一度だけ手動で実行する(実行のたびに重複登録されるため)
function createCardTrigger() {
  ScriptApp.getProjectTriggers().forEach(function (t) {
    if (t.getHandlerFunction() === "importNameCards") {
      ScriptApp.deleteTrigger(t);
    }
  });

  ScriptApp.newTrigger("importNameCards")
    .timeBased()
    .everyMinutes(30)
    .create();
}

// 確信度が low の行だけ、後から目視で確認すればよい
// スプレッドシートの条件付き書式で confidence 列が "low" の行に色を付けておくと早い

既存の同名トリガーを消してから作り直しているのは、実行のたびにトリガーが増殖して同じ名刺が複数回処理されるのを防ぐためです。 運用では確信度が low の行だけを後から見直せば十分です。 条件付き書式で色を付けておけば、確認は数十秒で終わります。

まとめ

名刺のデータ化は、GASとClaude APIで自動化できます。押さえるべきは5つです。 画像はOCRを挟まずimageブロックでそのまま渡す。media_typeはファイルから読んだ実際の形式を使う。書かれていない項目は空文字にさせ、推測させない。 メールアドレスで重複を防ぐ。そして登録してから画像を移動する。

この仕組みで消えるのは、名刺を1枚ずつ入力する時間だけではありません。「輪ゴムでまとめた名刺の束を、いつかやろうと先送りする気重さ」が消えます。フォルダに写真を放り込めば表になっている状態から始められる。 同じ「ファイルをAIに渡して項目を抽出する」型は、請求書PDFや領収書、アンケート用紙のデータ化にもそのまま応用できます。 読み取りをAIに、確認を人に——分担を守るかぎり、この自動化は安全に効きます。

よくある質問

読み取れます。Claude APIは画像をそのまま解析するため、専用スキャナーがなくてもスマホで撮った写真で十分です。ただし精度は画質に依存します。斜めから撮った・影が濃い・手ブレしている写真は読み取りが落ちるので、明るい場所で名刺を平置きにして真上から撮るのが確実です。読み取り結果に自信が持てない項目は、後述の confidence(確信度)を出力させ、低いものだけ人が確認する運用が現実的です。

要りません。名刺の画像ファイルそのものをAIに渡しているためです。従来はOCRで文字を抜き出し、「どれが会社名でどれが役職か」を位置やキーワードで判定する処理が必要でした。この記事の方法では、画像をBase64(バイナリを文字列に変換する方式)に変換してClaude APIへ送り、「会社名・氏名・電話を項目ごとに返して」と指示するだけです。レイアウトが名刺ごとに違っても、同じコードで動きます。

読めます。プロンプトで「氏名のふりがなが記載されていれば kana に入れる」「英語表記があれば nameEn に入れる」と項目を足せば、AIがその欄を埋めます。表と裏を別々に撮った2枚の画像を1回のリクエストにまとめて渡すこともできます。ただし裏面が英語の対訳なだけの場合は、日本語の表だけで足りることが多いです。必要な項目に絞るとトークン消費と料金を抑えられます。

メールアドレスをキーにした重複チェックを入れておけば防げます。本文のコードでは、登録前にシートのメール列を読み、すでに同じアドレスがあればスキップします。名刺は同じ人からもらう機会が多いので、この一手間が効きます。メールが空の名刺もあるため、その場合は「会社名+氏名」で照合するなど、業務に合わせてキーを調整してください。

プロンプトで区別させれば分けられます。本文では「TEL と明記された番号を phone、FAX と明記された番号を fax に入れる」「携帯番号は mobile に入れる」と指示しています。ラベルのない番号は phone に入れるなど、迷ったときの入れ先も指定しておくと安定します。読み取った番号はハイフンの有無が揺れるので、GAS側で数字だけに整えてから保存すると、あとで検索しやすくなります。

名刺は取引先の個人情報なので、社内規程とプライバシーポリシーの確認が先です。AnthropicのAPI経由で送信したデータは、標準の設定では学習に使われません(詳細は公式の利用規約・プライバシーポリシーを確認してください)。取得した個人情報の利用目的の明示や保管ルールなど、個人情報保護法の観点も社内で整理してから運用してください。判断を省略しないでください。

AI×GASの業務自動化を
相談する。

名刺のデータ化のほかにも、請求書の読み取り・議事録の要約・問い合わせ分類・翻訳など、 AI APIとGASを組み合わせた自動化をご相談いただけます。既存のドライブ・スプレッドシート運用にもそのまま組み込めます。

AI×GAS自動化の対応範囲を見る