試してみたブログ

AI関連・iPhone/Pixelなどのガジェット・音声入力・サーマルプリンタなど興味をある事をどんどん試してみた際の記録

geminiを使ってGmailからcoopのメールをスプシに転記させるGASを作った

  • 前回の記事で、「Coopの定期宅配の使い勝手が悪いので注文メールを任意の曜日に自動的に送信させる物を作った」まで行きました。
  • 今回は、メール送信された物をGASを使ってGmailを読み込みgeminiを使ってスプシに記載していきます。
  • 最終的にスプシで管理していきます。

tameshitemita.hatenablog.jp

試してみた

  • gemini APIキーの取得

https://aistudio.google.com/api-keys

  • GASで動かしているソース
// ▼▼▼【設定項目】ここから ▼▼▼

// 1. あなたのGemini APIキーを貼り付けてください
const GEMINI_API_KEY = "API_KEY";

// 2. 検索したいGmailの条件を指定してください
// 例: 'from:supermarket@example.com subject:"発送のお知らせ"'
// 特定のネットスーパーの送信元アドレスや件名で絞り込むと精度が上がります
const GMAIL_SEARCH_QUERY = 'subject:("【GCweb】" "カタログでご注文を受け付けている商品です。")';

// 3. 結果を書き込むシート名を指定してください
const SHEET_NAME = "シート1"; 

// ▲▲▲【設定項目】ここまで ▲▲▲


/**
 * メインの処理を実行する関数
 */
function main() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  if (!sheet) {
    SpreadsheetApp.getUi().alert(`シート「${SHEET_NAME}」が見つかりません。`);
    return;
  }

  // 検索条件に合致し、まだ処理されていないメールのスレッドを取得
  const threads = GmailApp.search(`${GMAIL_SEARCH_QUERY} -label:処理済み`);

  if (threads.length === 0) {
    console.log("新しいメールはありませんでした。");
    return;
  }

  // 処理済みラベルがなければ作成
  const labelName = "処理済み";
  let label = GmailApp.getUserLabelByName(labelName);
  if (!label) {
    label = GmailApp.createLabel(labelName);
  }

  for (const thread of threads) {
    const message = thread.getMessages()[0]; // スレッドの最初のメッセージを取得
    const body = message.getPlainBody(); // メールの本文をテキストで取得
    const date = message.getDate(); // メールの受信日を取得

    console.log(`処理中のメール: ${message.getSubject()}`);

    // Gemini APIに本文を渡して商品リストを抽出
    const extractedItems = callGeminiAPI(body);

    if (extractedItems && extractedItems.length > 0) {
      // 抽出した商品リストをスプレッドシートに書き込む
      for (const item of extractedItems) {
        sheet.appendRow([date, item.name, item.price]);
      }
      console.log(`${extractedItems.length}件の商品をシートに書き込みました。`);
      // 処理が終わったスレッドにラベルを付けて、次回から重複処理しないようにする
      thread.addLabel(label);
    } else {
      console.log("メールから商品を抽出できませんでした。");
    }
  }
}
/**
 * ★★★ 応答解析を強化した最終版 ★★★
 * Gemini APIを呼び出して、メール本文から商品名と金額を抽出する関数
 * @param {string} mailBody - 解析したいメールの本文
 * @return {Array<{name: string, price: number}> | null} - 抽出した商品の配列
 */
function callGeminiAPI(mailBody) {
  // このURLは、これまでの経緯から最も安定しているものです
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;

  const prompt = `
以下のメール本文はネットスーパーの購入履歴です。
この中から「商品名」と「金額」を抽出し、以下のJSON形式の配列で出力してください。
金額は数値のみを抽出してください。商品ではない項目(送料、手数料、ポイントなど)は含めないでください。

出力形式:
[
  {"name": "商品名1", "price": 金額1},
  {"name": "商品名2", "price": 金_額2}
]

---
${mailBody}
---
`;

  const payload = {
    contents: [{
      parts: [{ text: prompt }]
    }]
  };

  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true // エラー時も応答内容を確認するためtrueにする
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const responseText = response.getContentText();
    
    // Geminiからの応答がJSONを含むかチェック
    const candidate = JSON.parse(responseText).candidates && JSON.parse(responseText).candidates[0];
    const content = candidate && candidate.content;
    const part = content && content.parts && content.parts[0];
    const geminiText = part && part.text;

    if (geminiText) {
      // 応答テキストから```json ... ```で囲まれた部分を正規表現で抽出
      const jsonMatch = geminiText.match(/```json\s*([\s\S]*?)\s*```/);
      if (jsonMatch && jsonMatch[1]) {
        // 抽出したJSON文字列だけをパースする
        return JSON.parse(jsonMatch[1]);
      } else {
        // ```がない場合、直接パースを試みる
        return JSON.parse(geminiText);
      }
    }
    console.error("Geminiの応答から有効なテキスト部分を抽出できませんでした。応答内容:", responseText);
    return null;

  } catch (e) {
    console.error("Gemini APIの呼び出し中、またはJSONの解析中にエラーが発生しました:", e);
    // エラー発生時の生の応答内容をログに出力すると、デバッグに役立ちます
    const errorResponse = UrlFetchApp.getLastResponse();
    if (errorResponse) {
       console.error("エラー発生時の生の応答:", errorResponse.getContentText());
    }
    return null;
  }
}

実行結果

つまづきポイントとしては2点

  • GAS実行時に下記の様にエラーっぽく見える
  • 解決策としては、「詳細」を押して進める

  • gemini実行時に404が返ってくる
    • EndpointURLが異なる or APIが有効化されてない or プロジェクトの請求登録されてない
    • EndpointURLは、AI Studio側から「cURLクイックスタートをコピー」からURLを確認する
    • APIが有効化されてないは、APIキー取得時に紐付けたor作成したプロジェクトにGC側から入り直して有効化する
    • 請求登録は https://console.cloud.google.com/billing から確認する