見出し画像

名刺画像をアップするだけでシートに自動転記する仕組みづくり!

みなさんは名刺管理はどうしていますか?専用アプリで撮影したり、ファイルに保管したりと様々な方法がありますが、手間や検索性に課題を感じることも多いはずです。
「急ぎで名刺の情報をシートに記載したい💦」と依頼を受けたので、
名刺のデータや画像をGoogle Driveにアップするだけで自動的にシートに情報が入る仕組みをつくりました。

専用アプリを選ぶのすら面倒な方
エコに・簡単に管理していきい方

完成イメージ

システム概要

・GoogleDriveに名刺画像をアップロード
・Google Vision APIを使用しOCR処理により名刺上の文字を認識
・ChatGPT APIを活用し、抽出したテキストを「氏名、会社名、役職」など、必要な項目を自動で分類・整理

GASやプログラミング初学者にも比較的分かりやすいように記録しています。

1.Google Drive に名刺画像をアップロード

①Google Drive名刺フォルダを作成

Google Drive内に、名刺画像を保存するためのフォルダを作成します。

次に【名刺】フォルダ内に【登録済み】フォルダを作成してください。
(シートへ転記が終わった画像の移動先)

②【名刺】フォルダに画像をアップロード

(推奨:なるべく鮮明な画像を使用)

2.スプレッドシートの準備

①名刺データ保存用のスプシを作成

シートのヘッダー行には「会社名」「部署名」「役職」「氏名」「電話番号」など、名刺から抽出する情報に対応する項目を設定します。
シート名を「名刺データ」に設定

②一つの目のスクリプト作成

メニューの「拡張機能」から「Apps Script」をクリック

③Google Cloud Vision API の設定

GASエディターが開けたら、左メニューバーにある【プロジェクトの設定】をクリック

スクリプトプロパティを追加】を選択

【スクリプトプロパティ】から以下のプロパティを追加してください。

VISION_API_KEY: 値=取得したGoogle Cloud APIキーを入力

Google Cloud API Keyを発行する手順
【参考記事】https://zenn.dev/tmitsuoka0423/articles/get-gcp-api-key


④コードを貼り付け

※以下をご自身の情報に書き換えて、以下のコードをスクリプトへ貼り付け。

  const SOURCE_FOLDER_ID = "Google Driveに作成した名刺フォルダのID";

画像を格納するGoogle Driveの名刺フォルダのリンク/folders/この部分

以下のコードをスクリプトに貼り付け

// 名刺画像処理スクリプト
// Google Vision APIを使用して名刺画像からテキストを抽出し、スプレッドシートに保存します

/**
 * メイン関数:指定フォルダ内の名刺画像を処理します
 * OCR処理を実行し、処理済みファイルを完了フォルダに移動します
 */
function scanBusinessCards() {
  // 設定値
  const SOURCE_FOLDER_ID = "Google Driveに作成した名刺フォルダのID";
  const COMPLETED_FOLDER_NAME = "登録済み";
  const TARGET_SHEET_NAME = "名刺データ";
  
  // リソースの初期化
  const sourceFolder = DriveApp.getFolderById(SOURCE_FOLDER_ID);
  const completedFolder = sourceFolder.getFoldersByName(COMPLETED_FOLDER_NAME).next();
  const files = sourceFolder.getFiles();
  const visionApiKey = PropertiesService.getScriptProperties().getProperty("VISION_API_KEY");
  const targetSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TARGET_SHEET_NAME);

  Logger.log(`処理開始: フォルダID ${SOURCE_FOLDER_ID}`);

  // 各画像ファイルの処理
  while (files.hasNext()) {
    const currentFile = files.next();
    const imageData = currentFile.getBlob();
    Logger.log(`処理中の名刺: ${currentFile.getName()}`);

    // OCR分析の実行
    const extractedText = performOcrAnalysis(visionApiKey, imageData);

    if (extractedText) {
      Logger.log(`テキスト抽出成功: ${currentFile.getName()}`);
      saveToSpreadsheet(targetSheet, extractedText);

      // 完了フォルダへ移動
      moveToCompletedFolder(currentFile, completedFolder, sourceFolder);
    } else {
      Logger.log(`警告: テキストが検出されませんでした: ${currentFile.getName()}`);
    }
  }

  Logger.log("名刺処理が正常に完了しました");
}

/**
 * Google Vision APIを使用してOCRを実行
 * @param {string} apiKey - Google Vision APIキー
 * @param {Blob} imageData - 処理対象の画像データ
 * @returns {string|null} 抽出されたテキストまたはnull
 */
function performOcrAnalysis(apiKey, imageData) {
  const endpoint = `https://vision.googleapis.com/v1/images:annotate?key=${apiKey}`;
  
  const requestPayload = {
    "requests": [{
      "image": {
        "content": Utilities.base64Encode(imageData.getBytes())
      },
      "features": [{
        "type": "TEXT_DETECTION"
      }]
    }]
  };

  const requestOptions = {
    "method": "post",
    "contentType": "application/json",
    "payload": JSON.stringify(requestPayload)
  };

  try {
    const apiResponse = UrlFetchApp.fetch(endpoint, requestOptions);
    const responseData = JSON.parse(apiResponse.getContentText());
    Logger.log("Vision API処理完了");
    
    const textResults = responseData.responses[0].textAnnotations;
    return textResults?.length > 0 ? textResults[0].description : null;
  } catch (error) {
    Logger.log(`Vision APIエラー: ${error.message}`);
    return null;
  }
}

/**
 * 抽出されたテキストをスプレッドシートに保存
 * @param {Sheet} sheet - 対象シート
 * @param {string} extractedText - 保存するテキスト
 */
function saveToSpreadsheet(sheet, extractedText) {
  const textLines = extractedText.split('\n');
  const formattedRow = new Array(12).fill("");  // 空の行を初期化

  // OCR結果全体を参照用に保存
  sheet.appendRow([extractedText]);

  // 特定のフィールドを抽出
  textLines.forEach(line => {
    // フィールド抽出機能の改善
    const processField = (fieldName, index) => {
      if (line.includes(fieldName)) {
        const [_, value] = line.split(":");
        formattedRow[index] = value?.trim() || "";
      }
    };

    processField("氏名", 0);
    processField("会社名", 1);
    // 必要に応じて他のフィールドを追加
  });

  // 空でない行のみ追加
  if (formattedRow.some(cell => cell !== "")) {
    sheet.appendRow(formattedRow);
  }
}

/**
 * 処理済みファイルを完了フォルダに移動
 * @param {File} file - 移動するファイル
 * @param {Folder} targetFolder - 移動先フォルダ
 * @param {Folder} sourceFolder - 移動元フォルダ
 */
function moveToCompletedFolder(file, targetFolder, sourceFolder) {
  targetFolder.addFile(file);
  sourceFolder.removeFile(file);
  Logger.log(`ファイルを完了フォルダに移動: ${file.getName()}`);
}

【Ctl + S】で保存。

⑤スクリプト実行

上部バーにある「実行」をクリック

⑥アクセスを承認

初めてそのスクリプトを実行する場合は権限の確認が必要です。
そのため、『権限を確認』を押します。

【高度】または【詳細】(英語の場合は【(Advanced)】)をクリック

「無題のプロジェクト(安全ではないページ)に移動」をクリック

「許可」をクリック

スプシに戻り以下のように情報が転記されていたら成功です!✨✨

次にChatGPT OpenAIの APIを活用し、抽出したテキストを「氏名、会社名、役職」など、必要な項目を自動で分類・整理していきます。

3.ChatGPTで名刺データを自動分類

①OpenAIのAPIを設定

GASエディターを開き、左メニューバーにある【プロジェクトの設定】をクリック

スクリプトプロパティを追加】を選択

【スクリプトプロパティ】から以下のプロパティを追加してください。

OPENAI_API_KEY: 値=取得したOpenAIのAPIキーを入力

Google Cloud API Keyを発行する手順 【参考記事】https://qiita.com/kurata04/items/a10bdc44cc0d1e62dad3


スクリプトプロパティは以下二つ存在している状態ですね。
VISION_API_KEY: Google Cloud APIキーを入力
OPENAI_API_KEY: OpenAIのAPIキーを入力


①二つ目のスクリプトを作成

二つ目のスクリプトを作成するので、
【+】マークをクリックして【スクリプト】を選択。ぺージが新規追加されるので、適宜ファイル名を書いてください。

今回2つ目のファイル名は例:「OpenAIAPI_Data_Column」

以下のコードをスクリプトへ貼り付け。

// 名刺情報分析スクリプト
// ChatGPTを使用して名刺情報を分析・分類します

function analyzeBusinessCards() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const dataRange = sheet.getRange(`A2:A${sheet.getLastRow()}`);
  const cardData = dataRange.getValues();

  Logger.log("名刺分析開始");

  cardData.forEach((row, index) => {
    const cardInfo = row[0];
    const currentRow = index + 2;
    
    // 行の処理が必要かチェック
    const isProcessed = sheet.getRange(`B${currentRow}:L${currentRow}`)
                            .getValues()[0]
                            .some(cell => cell !== "");

    if (cardInfo && !isProcessed) {
      const analyzedData = analyzeWithChatGPT(sheet, cardInfo);
      if (analyzedData) {
        updateSpreadsheetRow(sheet, currentRow, analyzedData);
      }
    }
  });

  Logger.log("名刺分析完了");
}

function analyzeWithChatGPT(sheet, cardInfo) {
  const apiKey = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY");
  const endpoint = "https://api.openai.com/v1/chat/completions";
  
  // ヘッダーからテンプレートを作成
  const headers = sheet.getRange("A1:L1").getValues()[0];
  const template = headers.reduce((acc, header) => {
    acc[header] = "";
    return acc;
  }, {});

  // プロンプトを修正して、必ずJSONで返すように指示
  const promptMessage = {
    "role": "system",
    "content": "You must respond with valid JSON only, no explanatory text. Format should exactly match the template provided."
  };
  
  const userMessage = {
    "role": "user",
    "content": `Template: ${JSON.stringify(template)}\n\nExtract information from this business card and respond in the exact JSON format of the template: ${cardInfo}`
  };

  const requestOptions = {
    "method": "post",
    "headers": {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json"
    },
    "payload": JSON.stringify({
      "model": "gpt-4",
      "messages": [promptMessage, userMessage],
      "max_tokens": 300,  // (適宜調整)名刺データの場合、通常これで十分
      "temperature": 0.3  // (適宜調整)低め、少ないトークンで、予測可能で安定した出力
    })
  };

  try {
    const response = UrlFetchApp.fetch(endpoint, requestOptions);
    const json = JSON.parse(response.getContentText());
    const content = json.choices[0].message.content.trim();
    
    // 応答が有効なJSONかどうかを確認
    try {
      return JSON.parse(content);
    } catch (parseError) {
      Logger.log(`JSON解析エラー: ${content}`);
      Logger.log(`エラー詳細: ${parseError.message}`);
      return null;
    }
  } catch (error) {
    Logger.log(`ChatGPT APIエラー: ${error.message}`);
    return null;
  }
}

function updateSpreadsheetRow(sheet, rowIndex, analyzedData) {
  if (!analyzedData) return;

  const headers = sheet.getRange("A1:L1").getValues()[0];
  headers.forEach((header, index) => {
    const cell = sheet.getRange(rowIndex, index + 1);
    cell.setValue(analyzedData[header] || "");
  });
}

function setApiKey() {
  const apiKey = "OPENAI_API_KEY";
  PropertiesService.getScriptProperties().setProperty("OPENAI_API_KEY", apiKey);
}

【Ctl + S】で保存。

②アクセスを承認

初めてそのスクリプトを実行する場合は権限の確認が必要です。
そのため、『権限を確認』を押します。

【高度】または【詳細】(英語の場合は【(Advanced)】)をクリック

「無題のプロジェクト(安全ではないページ)に移動」をクリック

「許可」をクリック

スプシに戻り以下のように情報が自動分類されていたら成功です!✨✨

※必ず自動分類に問題が無いか人の目でチェックを行ってください

まとめ

実装時の注意点:APIキーは必ずスクリプトプロパティで管理

システムの制限事項:
・OCR精度は画像品質に依存
・API使用量が増えると課金が必要になります
(例)
→Google Cloud Vision API=無料枠: 月1000件まで
→ChatGPT API(GPT-4): 入力1,000トークンあたり約$0.03(約4.5円)
例: 1枚の名刺処理で約200トークンを使用する場合
月100枚の名刺: 約$0.60(約90円)※あくまでも目安です

個人や小規模での利用なら、月数百円程度で運用可能
大量の名刺を処理する場合は、事前に料金シミュレーションがおすすめ!APIの料金は変動するので、最新情報は公式サイトか、お気軽にご相談ください◎
使用量に応じて料金が発生しますが、効率的に使えば、手作業での管理時間を考えるとコスパの良いシステムと言えます! 👍

急なトラブルが発生した場合や、Web開発・AI連携についてのご相談があれば、ぜひお気軽にお声掛けください。意見交換も大歓迎です。

最後まで読んでくださり、ありがとうございました🌱少しでも参考になる箇所があればそーっと♡で教えてください✨大変励みになります!

いいなと思ったら応援しよう!