見出し画像

GASからAI(ChatGPT / Claude)APIを叩いて、スプレッドシート業務を自動化する

この記事で得られること

「スプレッドシートに入っているテキストを、AIに処理させたい」
最近こう思う場面が増えていませんか。

たとえば、こんなケースです。

  • 顧客からの問い合わせ一覧を自動で分類したい

  • 商品レビューのポジティブ/ネガティブを判定したい

  • 英語の商品説明を日本語に一括翻訳したい

  • 大量のテキストから要約を自動生成したい

これらはChatGPTやClaudeの得意分野ですが毎回ブラウザにコピペして結果を貼り直すというのは本末転倒です…

この記事ではGASからAI APIを直接呼び出してスプレッドシート上で完結させる方法を解説します。
OpenAI(ChatGPT)とAnthropic(Claude)の両方のAPIで動くコードを載せているので、好みに合わせて使ってください。



前提知識

この記事は以下の知識がある前提で書いています。

  • GASでスプレッドシートの読み書きができる

  • GASから外部APIを叩く基本がわかる

特にUrlFetchAppの使い方とPropertiesServiceでのAPIキー管理は必須です。不安な方は先にそちらを読んでから戻ってくると理解しやすいです。


APIキーの取得

OpenAI(ChatGPT)の場合

  1. OpenAI Platform にアクセス

  2. アカウント作成(またはログイン)

  3. 左メニュー「API keys」→「Create new secret key」

  4. 生成されたキーをコピー(この画面を閉じると二度と表示されないので注意)

料金はAPI呼び出しごとの従量制です。
GPT-4o-miniなら入力100万トークンあたり$0.15と非常に安く個人利用なら月数十円〜数百円で収まることが多いです。

Anthropic(Claude)の場合

  1. Anthropic Console にアクセス

  2. アカウント作成(またはログイン)

  3. 「API Keys」→「Create Key」

  4. 生成されたキーをコピー

Claude Sonnet 4であれば入力100万トークンあたり$3です。用途に応じてモデルを選んでください。

GASへのキー設定

どちらのAPIキーもコードにベタ書きは厳禁です。
スクリプトプロパティに保存します。

  1. スクリプトエディタ →「プロジェクトの設定」(歯車アイコン)

  2. 「スクリプト プロパティ」で以下を追加:

    • プロパティ: OPENAI_API_KEY / 値: sk-xxxxxxx

    • プロパティ: ANTHROPIC_API_KEY / 値: sk-ant-xxxxxxx

使うAPIに合わせてどちらか一方だけでもOKです。


基本:GASからAI APIを呼び出す

OpenAI(ChatGPT)版

: このコードは Chat Completions API を使っています。
まずこれで動かすのが一番手軽です。
OpenAIは新しくResponses API も公開しており新規実装ではそちらも選択肢になります。

/**
 * OpenAI Chat Completions APIを呼び出す
 * @param {string} prompt - AIへの指示
 * @param {string} userMessage - 処理対象のテキスト
 * @return {string} AIの応答テキスト
 */
function callOpenAI(prompt, userMessage) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');

  const payload = {
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: prompt },
      { role: 'user', content: userMessage }
    ],
    temperature: 0.3 // 出力の安定性を重視(0に近いほど一貫した結果)
  };

  const options = {
    method: 'post',
    headers: {
      'Authorization': 'Bearer ' + apiKey,
      'Content-Type': 'application/json'
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', options);
  const statusCode = response.getResponseCode();

  if (statusCode !== 200) {
    throw new Error(`OpenAI API エラー (${statusCode}): ${response.getContentText()}`);
  }

  const result = JSON.parse(response.getContentText());
  return result.choices[0].message.content.trim();
}

Anthropic(Claude)版

/**
 * Anthropic Messages APIを呼び出す
 * @param {string} prompt - AIへの指示(system prompt)
 * @param {string} userMessage - 処理対象のテキスト
 * @return {string} AIの応答テキスト
 */
function callClaude(prompt, userMessage) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY');

  const payload = {
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system: prompt,
    messages: [
      { role: 'user', content: userMessage }
    ]
  };

  const options = {
    method: 'post',
    headers: {
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01',
      'Content-Type': 'application/json'
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', options);
  const statusCode = response.getResponseCode();

  if (statusCode !== 200) {
    throw new Error(`Claude API エラー (${statusCode}): ${response.getContentText()}`);
  }

  const result = JSON.parse(response.getContentText());
  return result.content[0].text.trim();
}

両APIの違い

2つのコードを見比べると、構造はほぼ同じです。違いは主に3点です。

  • エンドポイントURL: OpenAIは /v1/chat/completions、Claudeは /v1/messages

  • 認証ヘッダー: OpenAIは Authorization: Bearer、Claudeは x-api-key + anthropic-version

  • レスポンス構造: OpenAIは choices[0].message.content、Claudeは content[0].text

どちらを使うかはお好みで。
以降のコード例ではOpenAI版で書きますがcallOpenAI()をcallClaude()に差し替えるだけで動きます。


実践①:テキスト分類の自動化

スプレッドシートのA列にテキストデータ、B列にAIによる分類結果を自動で入れるという基本パターンです。

ここでは「顧客フィードバックをカテゴリ分類する」例でやります。

シートの想定

コード

/**
 * A列のテキストをAIで分類してB列に結果を書き込む
 */
function classifyFeedback() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRow = sheet.getLastRow();

  if (lastRow < 2) return;

  const prompt = `あなたはカスタマーフィードバックの分類担当です。
与えられたフィードバックを以下のカテゴリのいずれか1つに分類してください。
カテゴリ名だけを返してください。

カテゴリ:
- 配送
- 商品品質
- UI/UX
- 価格
- カスタマーサポート
- その他`;

  const feedbacks = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
  const results = [];

  feedbacks.forEach((row, index) => {
    const text = row[0];
    if (!text) {
      results.push(['']);
      return;
    }

    try {
      const category = callOpenAI(prompt, text);
      results.push([category]);
      console.log(`[${index + 1}/${feedbacks.length}] "${text.substring(0, 20)}..." → ${category}`);
    } catch (e) {
      results.push(['エラー: ' + e.message]);
      console.error(`[${index + 1}] 分類失敗: ${e.message}`);
    }

    // API レート制限対策(1リクエストごとに1秒待機)
    Utilities.sleep(1000);
  });

  // 一括書き込み
  sheet.getRange(2, 2, results.length, 1).setValues(results);
  console.log(`${results.length}件の分類が完了しました`);
}

ポイント解説

プロンプト設計が結果の9割を決める
「カテゴリ名だけを返してください」と明示しているのが重要です。

これがないと「このフィードバックは配送に関する内容ですので、カテゴリは『配送』に分類されます」のような冗長な応答が返ってきてセルに入れたときに使いにくくなります。

temperature: 0.3にしている理由
分類タスクのように正解が決まっている処理ではtemperatureを低くして出力を安定させます。

創造的な出力が欲しい場合(文章生成など)は0.7〜1.0に上げてください。

Utilities.sleep(1000)は必須
API呼び出しの間隔を空けないとレート制限に引っかかります。

大量データを処理する場合はこの待機時間をもう少し長くするか後述のバッチ処理パターンを使ってください。


実践②:商品説明の翻訳

eBayや越境ECで使える英語→日本語(またはその逆)の一括翻訳です。

/**
 * A列の英語テキストをB列に日本語訳で書き込む
 */
function translateDescriptions() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRow = sheet.getLastRow();

  if (lastRow < 2) return;

  const prompt = `あなたは商品説明の翻訳者です。
与えられた英語の商品説明を自然な日本語に翻訳してください。

ルール:
- ECサイトに掲載する商品説明として自然な日本語にする
- 専門用語はカタカナ表記で残す
- 箇条書きの構造は維持する
- 翻訳結果のみを返す(余計な説明は不要)`;

  const texts = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
  const translations = [];

  texts.forEach((row, index) => {
    const text = row[0];
    if (!text) {
      translations.push(['']);
      return;
    }

    try {
      const translated = callOpenAI(prompt, text);
      translations.push([translated]);
      console.log(`[${index + 1}/${texts.length}] 翻訳完了`);
    } catch (e) {
      translations.push(['翻訳エラー: ' + e.message]);
    }

    Utilities.sleep(1000);
  });

  sheet.getRange(2, 2, translations.length, 1).setValues(translations);
  console.log(`${translations.length}件の翻訳が完了しました`);
}

Google翻訳APIでも翻訳はできますがAI APIの強みは「ECサイト向けの自然な日本語にする」「専門用語はカタカナで残す」のような細かい指示が効くことです。
プロンプトでトーンや形式を制御できるのはルールベースの翻訳にはない柔軟性です。


実践③:テキスト要約の自動生成

長文レビューや記事本文を指定文字数に要約するパターンです。

/**
 * A列の長文をB列に要約して書き込む
 */
function summarizeTexts() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRow = sheet.getLastRow();

  if (lastRow < 2) return;

  const prompt = `与えられたテキストを100文字以内で要約してください。
要約文のみを返してください。`;

  const texts = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
  const summaries = [];

  texts.forEach((row, index) => {
    const text = row[0];
    if (!text) {
      summaries.push(['']);
      return;
    }

    try {
      const summary = callOpenAI(prompt, text);
      summaries.push([summary]);
    } catch (e) {
      summaries.push(['要約エラー']);
    }

    Utilities.sleep(1000);
  });

  sheet.getRange(2, 2, summaries.length, 1).setValues(summaries);
}

大量データを処理する:バッチ処理とコスト管理

GASの実行時間制限への対処

GASは1回の実行が最大6分で強制終了されます。
AI APIは1リクエストに数秒かかるため100件を超えるデータを一度に処理しようとするとタイムアウトします。

対策として処理を分割して続きから再開する仕組みを入れます。

/**
 * バッチ処理:処理済み行数をPropertiesServiceに保存し、続きから再開できる
 */
function batchClassify() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRow = sheet.getLastRow();
  const props = PropertiesService.getScriptProperties();

  // 前回の続きから開始
  const startRow = Number(props.getProperty('BATCH_CURRENT_ROW') || '2');
  const maxExecutionMs = 5 * 60 * 1000; // 5分(6分制限に余裕を持たせる)
  const startTime = Date.now();

  const prompt = `フィードバックを「配送」「商品品質」「UI/UX」「価格」「サポート」「その他」のいずれかに分類してください。カテゴリ名のみ返してください。`;

  console.log(`バッチ処理開始: ${startRow}行目から`);

  for (let row = startRow; row <= lastRow; row++) {
    // 実行時間チェック
    if (Date.now() - startTime > maxExecutionMs) {
      props.setProperty('BATCH_CURRENT_ROW', String(row));
      console.log(`時間切れ: ${row}行目で中断。次回ここから再開します`);
      return;
    }

    const text = sheet.getRange(row, 1).getValue();
    if (!text) continue;

    // 処理済みチェック(B列に値があればスキップ)
    const existing = sheet.getRange(row, 2).getValue();
    if (existing) continue;

    try {
      const result = callOpenAI(prompt, text);
      sheet.getRange(row, 2).setValue(result);
    } catch (e) {
      sheet.getRange(row, 2).setValue('エラー');
      console.error(`${row}行目でエラー: ${e.message}`);
    }

    Utilities.sleep(1500);
  }

  // 全件完了
  props.deleteProperty('BATCH_CURRENT_ROW');
  console.log('バッチ処理が全件完了しました');
}

このコードのポイントは3つです。

1つ目は処理位置の保存
PropertiesServiceに「何行目まで処理したか」を保存します。

次回実行時に続きから始められるのでGASの6分制限を超えるデータ量にも対応できます。

2つ目は5分での自主中断
6分制限ギリギリまで回すと最後のAPIレスポンスが返ってくる前にタイムアウトするリスクがあります。

余裕を持って5分で止めます。

3つ目は処理済みスキップ
B列にすでに結果が入っている行はスキップします。

途中で止まっても同じ行を二重処理しません。
この関数をGASのトリガーで5分間隔に設定すれば数百件のデータも放置しておくだけで全件処理されます。

APIコストの見積もり方

AI APIは従量課金なので大量処理する前にコストを見積もっておくのが大事です。

/**
 * 処理予定のテキストからAPI呼び出しコストを概算する
 */
function estimateCost() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRow = sheet.getLastRow();
  const texts = sheet.getRange(2, 1, lastRow - 1, 1).getValues().flat().filter(t => t);

  // テキストの合計文字数(日本語1文字 ≒ 2〜3トークンとして概算)
  const totalChars = texts.reduce((sum, t) => sum + String(t).length, 0);
  const estimatedTokens = totalChars * 2.5; // 日本語の概算

  // システムプロンプト分を加算(1リクエストあたり約100トークンと仮定)
  const promptTokens = texts.length * 100;
  const totalInputTokens = estimatedTokens + promptTokens;

  // 出力トークン(1リクエストあたり平均50トークンと仮定)
  const totalOutputTokens = texts.length * 50;

  // GPT-4o-mini の料金
  const inputCostUsd = (totalInputTokens / 1_000_000) * 0.15;
  const outputCostUsd = (totalOutputTokens / 1_000_000) * 0.60;
  const totalCostUsd = inputCostUsd + outputCostUsd;
  const totalCostJpy = totalCostUsd * 150; // 概算レート

  const summary = `
=== API コスト概算 ===
処理件数: ${texts.length}件
合計文字数: ${totalChars.toLocaleString()}文字
推定入力トークン: ${Math.round(totalInputTokens).toLocaleString()}
推定出力トークン: ${Math.round(totalOutputTokens).toLocaleString()}

GPT-4o-mini の場合:
  概算コスト: $${totalCostUsd.toFixed(4)} (約${Math.ceil(totalCostJpy)}円)
  `.trim();

  console.log(summary);
  SpreadsheetApp.getUi().alert(summary);
}

実行すると処理前にだいたいのコストがわかります。
数百件の分類処理であればGPT-4o-miniなら数円〜数十円で済むことが多いです。


カスタム関数として使う(上級)

ここまでのコードはすべてスクリプトを「実行」して処理するパターンでした。
もう一歩進んで、スプレッドシートの関数として使う方法もあります。

/**
 * セル内で =AI_CLASSIFY(A2) のように使えるカスタム関数
 * ※制約あり(後述)
 */
function AI_CLASSIFY(text) {
  if (!text) return '';

  const prompt = 'フィードバックを「配送」「商品品質」「UI/UX」「価格」「サポート」「その他」に分類。カテゴリ名のみ返す。';
  return callOpenAI(prompt, text);
}

B2セルに =AI_CLASSIFY(A2)と入れればA2のテキストを自動分類した結果が返ります。

ただし、カスタム関数には制約があります

  • PropertiesServiceに制約がある
    カスタム関数内からスクリプトプロパティにアクセスする方法は制限されています。
    APIキーをシートのセルや引数で渡す回避策もありますがコードやシートが他者に見える状況では漏えいリスクがあり推奨しません

  • 実行時間が30秒制限
    通常の関数(6分)より短い制限があります

  • セルの再計算で毎回API呼び出しが走る
    シートを開くたびに課金される可能性があります

これらの制約からカスタム関数方式は少量データのアドホック処理には便利ですが大量データの定期処理には向きません。
前述のバッチ処理パターンのほうが安定します。


まとめ

この記事で紹介した内容を整理します。

  • GASからAI API(OpenAI / Claude)を呼び出す基本コードは、UrlFetchAppの応用で書ける

  • 分類・翻訳・要約など、プロンプトを変えるだけで多様なタスクに対応できる

  • プロンプト設計(出力形式を明示、temperatureの調整)が結果の質を大きく左右する

  • 大量データはバッチ処理+処理位置保存でGASの6分制限に対処する

  • コスト見積もりを処理前に行う習慣をつける

GASとAI APIの組み合わせはスプレッドシートでの手作業を大幅に減らせる強力なパターンです。
今回は単純な「入力→出力」の処理を紹介しましたが複数セルの情報をまとめてプロンプトに渡したり、AIの出力を構造化して複数列に書き戻したりと応用の幅はかなり広いです。

外部API連携の実践としてeBay APIとGASを組み合わせた例も公開しています。OAuth認証やレート制限対策など、より複雑なAPI連携のハマりどころが気になる方はこちらもどうぞ。

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