バイブコーディング×ノーコードで名刺管理アプリを構築してみた【第3編】GAS×GeminiでOCR自動化

2025年5月21日水曜日

Appsheet ChatGPT GAS Gemini API Google Apps Script No Code VibeCoding

バイブコーディング×ノーコードで名刺管理アプリを構築してみた【第3編】GAS×GeminiでOCR自動化

投稿日:


巷にあふれる名刺管理アプリを参考に、AppSheetとGAS×Geminiを組み合わせてOCR自動化を実現。画像から自動で名刺情報を抽出・登録する仕組みを徹底解説。

目次

OCR自動化に挑戦した理由

名刺管理の現場では、名刺の情報を手入力するのが非常に面倒です。そこで、画像から文字を読み取って自動で必要な項目を抽出・記録する仕組みを作れないかと考えました。

どうやって構築したか?

AppSheet上に保存した名刺画像(表・裏)のURLを、Google Apps Script(GAS)で処理し、Gemini APIを使ってOCRと項目抽出を行う構成です。得られた情報はスプレッドシートのK列〜Q列に書き込み、完了フラグを「済」として記録します。

全体の処理フロー

  1. 名刺画像がスプレッドシートにアップロードされる
  2. GASでURLから画像を取得
  3. Gemini APIでOCR処理を実行
  4. 取得したテキストから「氏名」「メール」「会社名」などをJSON形式で抽出
  5. 各カラムに自動で書き込み、完了フラグを設定

使用したGASコード

以下が、実際に使用したコードです(長文のため折りたたみ)。
※シートIDやAPIキー、フォルダIDは各自の環境に合わせて差し替えてください。

クリックしてコードを見る

/**
 * 名刺交換ログ1行を処理:OCR→項目抽出→K〜Q列書き込み
 * (gemini-2.0-flash-lite モデル使用版)
 */
function processBusinessCardRowWithGemini() {
  const sheetId   = 'XXXX'; 
  const folderId  = 'YYYY'; 
  const sheetName = '名刺交換ログ';
  const apiKey    = 'ZZZZ'; 

  const ss      = SpreadsheetApp.openById(sheetId);
  const sheet   = ss.getSheetByName(sheetName);
  const lastRow = sheet.getLastRow();
  const headers = sheet.getRange(1,1,1,sheet.getLastColumn())
                       .getValues()[0].map(h=>h.toString().trim());

  const idx    = name => headers.indexOf(name);
  const frontImgCol = idx("名刺画像(表)");
  const backImgCol  = idx("名刺画像(裏)");
  const ocrFrontCol = idx("OCRテキスト_表");
  const ocrBackCol  = idx("OCRテキスト_裏");
  const statusCol   = idx("OCR済");

  const colK = 11, colL = 12, colM = 13,
        colN = 14, colO = 15, colP = 16, colQ = 17;

  const folder = DriveApp.getFolderById(folderId);

  for (let r = 2; r <= lastRow; r++) {
    const row = sheet.getRange(r,1,1,sheet.getLastColumn()).getValues()[0];
    if (row[statusCol] === "済") continue;
    const frontUrl = row[frontImgCol], backUrl = row[backImgCol];
    if (!frontUrl && !backUrl) continue;

    let ocrF="", ocrB="";
    if (frontUrl) {
      const blob = getImageBlobFromUrl(frontUrl, folder);
      if (blob) ocrF = ocrWithGemini(blob, apiKey);
    }
    if (backUrl) {
      const blob = getImageBlobFromUrl(backUrl, folder);
      if (blob) ocrB = ocrWithGemini(blob, apiKey);
    }
    if (ocrF) sheet.getRange(r, ocrFrontCol+1).setValue(ocrF);
    if (ocrB) sheet.getRange(r, ocrBackCol+1).setValue(ocrB);

    const extracted = extractBusinessCardFieldsFromText(ocrF, apiKey);
    if (extracted["氏名"])           sheet.getRange(r, colK).setValue(extracted["氏名"]);
    if (extracted["メールアドレス"]) sheet.getRange(r, colL).setValue(extracted["メールアドレス"]);
    if (extracted["会社名"])         sheet.getRange(r, colM).setValue(extracted["会社名"]);
    if (extracted["部署"])           sheet.getRange(r, colN).setValue(extracted["部署"]);
    if (extracted["役職"])           sheet.getRange(r, colO).setValue(extracted["役職"]);
    if (extracted["電話番号(代表)"]) sheet.getRange(r, colP).setValue(extracted["電話番号(代表)"]);
    if (extracted["電話番号(個人)"]) sheet.getRange(r, colQ).setValue(extracted["電話番号(個人)"]);

    sheet.getRange(r, statusCol+1).setValue("済");
    SpreadsheetApp.flush();
  }
}

function getImageBlobFromUrl(url, folder) {
  const fileName = url.split('/').pop();
  const files = folder.getFilesByName(fileName);
  if (files.hasNext()) return files.next().getBlob();
  Logger.log("ファイル未検出: " + fileName);
  return null;
}

function ocrWithGemini(blob, apiKey) {
  const base64Image = Utilities.base64Encode(blob.getBytes());
  const payload = {
    contents:[{ parts:[{ inlineData:{ mimeType:blob.getContentType(), data:base64Image }}]}],
    generationConfig:{ temperature:0.2, maxOutputTokens:2048 }
  };
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=${apiKey}`;
  const res = UrlFetchApp.fetch(url, {
    method:"post", contentType:"application/json", payload:JSON.stringify(payload)
  });
  return JSON.parse(res.getContentText())?.candidates?.[0]?.content?.parts?.[0]?.text || "";
}

function extractBusinessCardFieldsFromText(text, apiKey) {
  const prompt = `
次のOCRテキストから、以下の項目を抽出してください:
氏名、メールアドレス、会社名、部署、役職、電話番号(代表)、電話番号(個人)

【重要】
- 必ずJSON形式でのみ返してください。
- 空の値は "" としてください。
- JSON以外は出力しないでください。

{
  "氏名": "",
  "メールアドレス": "",
  "会社名": "",
  "部署": "",
  "役職": "",
  "電話番号(代表)": "",
  "電話番号(個人)": ""
}

OCRテキスト:
${text}
`;
  const payload = {
    contents:[{ parts:[{ text:prompt }]}],
    generationConfig:{ temperature:0.2, maxOutputTokens:1024 }
  };
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=${apiKey}`;
  const res = UrlFetchApp.fetch(url, {
    method:"post", contentType:"application/json", payload:JSON.stringify(payload)
  });
  let responseText = JSON.parse(res.getContentText())?.candidates?.[0]?.content?.parts?.[0]?.text || "";

  responseText = responseText.trim()
    .replace(/^```[a-zA-Z]*\r?\n/, '') 
    .replace(/\r?\n```$/, '')
    .trim();

  try {
    return JSON.parse(responseText);
  } catch (e) {
    Logger.log("JSON parse error: " + e);
    Logger.log("レスポンス(クリーニング後): " + responseText);
    return {};
  }
}
  

まとめと次回予告

今回は、名刺画像から情報を自動抽出し、スプレッドシートに反映させるOCR自動化の手順をご紹介しました。ノーコード×GAS×LLMの組み合わせで、名刺管理の手間は大きく減らせます。

次回は、何のアプリを作ろうかな~