GASで名刺画像をAIで読み取り
スプレッドシートに自動登録する方法
もらった名刺を1枚ずつ見ながら、会社名・氏名・電話・メールを表に打ち込む——この作業は自動化できます。DriveApp(GASからGoogleドライブを操作する標準機能)で名刺の写真を取り出し、Claude API(Anthropic社の生成AI)に 画像ごと渡して項目を読み取らせます。OCRライブラリも位置判定も使わない実装を、動くコードで解説します。
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.comとsales@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を組み合わせた自動化をご相談いただけます。既存のドライブ・スプレッドシート運用にもそのまま組み込めます。