📖

【GAS/Discord/Claude】スプレッドシートが更新されたら表っぽい文字列を生成してDiscordに通知する

2025/02/09に公開

サマリ

大学時代の友人と恒例行事として年に1~2回旅行をします。
その際に現地では誰か手が回る人が会計をして、後日割り勘精算するという流れが暗黙の了解になっています。ですが手元のお金が減らないので実際に思ってたよりお金を使っていたり、そもそも計算も精算も忘れて会ったときに「やべ、忘れてた」と皆で言うという残念展開も多々あります・・・。

といった課題を解決したくで以下をやってみます。

  • Googleスプレッドシートにて支払金額を管理、割り勘計算する(ここはただの表計算なので記事上は割愛) ※1
  • Googleスプレッドシートが更新されたら表のデータをGoogle Apps Script(GAS)でDiscordサーバーに通知する
  • 金額投稿時にテーブルの情報を生成AI(Claude)で表っぽい文字列にして報告する・・・DiscordではMarkdownの表形式が使えないので無理やり表っぽくします

※1 サンプルとして今回は以下の画像のスプレッドシートを使うこととします

手順

Anthropic API(Claude)の有効化

以下の記事に従いAnthropic API(Claudeのモデルが使用できる)のAPIキーを入手します。
単純な表の理解なら一番安いモデルのHaikuを使えば充分なはずなので上限金額は5ドルにしておきます。記事中で言及されているBussiness Tax IDは空欄でも登録可能になっていました。
https://note.com/noa813/n/n307d62b5820b

Discord Webhook URLの取得

次に以下の記事に従いDiscordのWebhook URLを取得します。(当たり前ですが)Discordの管理者権限が必要です。
https://zenn.dev/lambta/articles/5edbda4ccb1ec6

GASを作成しAPIキーをプロパティに格納する

続いてGASのプロジェクトを作成します。
スプレッドシートが更新されたらスクリプトをトリガしたいので、通常はGASをスプレッドシートファイルに紐付けした形で作成します。しかしスプレッドシートは共同編集で友人にも記載してもらうが、スクリプトは触れないようにしたいのでスタンドアロンのスクリプトファイルを作成することにしました。
Google Driveから「新規」->「その他」->「Google Apps Script」で作成できます。
ついでにプロジェクト名や.gsのファイル名も任意のものに変えておきます。



手順の最初に取得したAnthropic APIのAPIキーですが、スクリプトに直接埋め込むのはセキュリティ上よろしくありません。GASにはAPIキー等のシークレット情報を管理できるプロパティサービスが標準で備わっているので、そこに保存します。

左のメニューの歯車マークにカーソルを当て、プロジェクトの設定を押下して遷移します。
ページ一番下の「スクリプトプロパティを追加」を押下して「プロパティ」にスクリプトから参照する時の名前と「値」にAPIキーを設定します。DiscordのWebhook URLと連携するスプレッドシートのIDもプロパティとして設定しました(ここはプロパティ化要否の意見が分かれる気がします)。


ベースのスクリプト作成

本丸のスクリプト作成に入ります。
まずはベースとなるものをClaude3.5 Sonnetに作成してもらいました。

このようなプロンプトを投入して作成してもらいます。

Googleスプレッドシートが更新されたらDiscordに通知するGoogle Apps Scriptを作成してください。
その際にスプレッドシートのA2セルからE5セルまでを取得してDiscordに投稿する文章に値を含めることとします。

Claudeはスクリプト作成を依頼すると画面左側に解説、右側にスクリプトが出てきてチャットのやり取りに応じてスクリプトがインタラクティブに変わる機能があります(もう1からコード書くことは無いのかなという気分)。

※画像は最初の出力。この後少し調整しました。



実際に出力されたコードは以下。
ベースとしてはおおよそ問題ないと思います。編集されたシート取得になっているので固定に後で変えます。文字コードにループカウンタを足してセル文字を算出するスタイルもちょっとクセがありますが受け入れることにします。このあたりはプロンプトの工夫ポイントになりそうです。

// Discordのウェブフックを設定する変数
const DISCORD_WEBHOOK_URL = "YOUR_DISCORD_WEBHOOK_URL";

// スプレッドシートが編集された時に実行される関数
function onEdit(e) {
  // 編集されたシートを取得
  const sheet = e.source.getActiveSheet();
  
  // A2:E5の範囲の値を取得
  const range = sheet.getRange("A2:E5");
  const values = range.getValues();
  
  // Discord用のメッセージを作成
  let message = "スプレッドシートが更新されました!\n\n";
  message += "更新された内容:\n";
  
  // 各セルの値を追加
  for (let i = 0; i < values.length; i++) {
    for (let j = 0; j < values[i].length; j++) {
      const cellAddress = `${String.fromCharCode(65 + j)}${i + 1}`; // A1, B1, etc.
      message += `${cellAddress}: ${values[i][j]}\n`;
    }
  }
  
  // Discordに通知を送信
  sendToDiscord(message);
}

// Discordにメッセージを送信する関数
function sendToDiscord(message) {
  const payload = {
    content: message
  };
  
  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload)
  };
  
  try {
    UrlFetchApp.fetch(DISCORD_WEBHOOK_URL, options);
  } catch (error) {
    Logger.log("Discord通知の送信に失敗しました: " + error);
  }
}

// トリガーを手動で設定する関数
function createEditTrigger() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  ScriptApp.newTrigger("onEdit")
    .forSpreadsheet(ss)
    .onEdit()
    .create();
}

Claudeで表データを解析するスクリプトを作成する

Claudeに表データを理解して表っぽい文字列を生成してもらうスクリプトを作成します。
ここで作成したスクリプトは最終的にはベースのスクリプトに取り込みますが、一旦別々で作成(生成)します。同じ固有名詞が並ぶのでややこしいですが、Anthropic API(Claude)を叩くスクリプトをClaudeに生成してもらっています。

このようなプロンプトを使用しました。

以下のプロンプトをAnthropic APIを使用して結果を取得するGoogle Apps Script上で実行するjavascriptコードを作成してください。
生成AIのモデルは一番安いモデルを指定してください。
表計算ソフトのセルと値の情報を渡します。
この表を人間が表形式として認識できる文字列にしてください。
#表形式 セルアドレス: 値

生成されたコードは以下。
スプレッドシートに書き込みしたり余計な部分があるので、APIを叩くところだけ拝借します。

生成されたコード
function analyzePaymentsWithClaude() {
  // スクリプトプロパティからAPIキーを取得
  const ANTHROPIC_API_KEY = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY');
  
  // アクティブなスプレッドシートを取得
  const sheet = SpreadsheetApp.getActiveSheet();
  
  // プロンプトの作成
  const prompt = `表計算ソフトのセルと値の情報を渡します。
この表を人間が表形式として認識できる文字列にしてください。

# 表形式
セルアドレス: 値`;

  // APIリクエストのオプション
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': ANTHROPIC_API_KEY,
      'anthropic-version': '2023-06-01'
    },
    payload: JSON.stringify({
      model: 'claude-3-haiku-20240307',
      max_tokens: 1000,
      messages: [{
        role: 'user',
        content: prompt
      }]
    })
  };

  try {
    // APIリクエストを送信
    const response = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', options);
    const responseData = JSON.parse(response.getContentText());
    
    // 結果を出力するシートを準備
    const outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('分析結果') || 
                       SpreadsheetApp.getActiveSpreadsheet().insertSheet('分析結果');
    outputSheet.clear();
    
    // タイトルを設定
    outputSheet.getRange('A1').setValue('支払分析結果');
    outputSheet.getRange('A1').setFontWeight('bold');
    
    // APIレスポンスから結果を取得
    const analysisResult = responseData.content[0].text;
    
    // 結果をシートに書き込む
    outputSheet.getRange('A3').setValue(analysisResult);
    
    // セルの幅を自動調整
    outputSheet.autoResizeColumn(1);
    
    // 実行完了のメッセージを表示
    SpreadsheetApp.getActive().toast('分析が完了しました', '完了');
    
  } catch (error) {
    // エラー発生時
    console.error('Error:', error);
    SpreadsheetApp.getActive().toast('エラーが発生しました: ' + error.toString(), 'エラー');
  }
}

// メニューに追加する関数
function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('支払分析')
    .addItem('支払額を分析', 'analyzePaymentsWithClaude')
    .addToUi();
}

完成版スクリプト

最終的なスクリプトを以下に示します。

// スプレッドシートが編集された時に実行される関数
function onEdit(e) {
  // プロパティを取得
  const DISCORD_WEBHOOK_URL = PropertiesService.getScriptProperties().getProperty('DISCORD_WEBHOOK_URL');
  const ANTHROPIC_API_KEY = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY');
  const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
  const SHEET_NAME = "シート1";

  // シートを取得
  const spreadSheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = spreadSheet.getSheetByName(SHEET_NAME)
  
  // A2:E5の範囲の値を取得
  const range = sheet.getRange("A2:E5");
  const values = range.getValues();
  
  // Discord用のメッセージを作成
  let message = "スプレッドシートが更新されました!\n\n";
  let promptSheetData = "";
  
  // 各セルの値をプロンプト用表データに追加
  for (let i = 0; i < values.length; i++) {
    for (let j = 0; j < values[i].length; j++) {
      const cellAddress = `${String.fromCharCode(65 + j)}${i + 1}`; // A1, B1, etc.
      promptSheetData += `${cellAddress}: ${values[i][j]}\n`;
    }
  }

  // 表解析
  tableAnalysisMessage = analyzePaymentsWithClaude(ANTHROPIC_API_KEY, promptSheetData);
  message += tableAnalysisMessage;

  // Discordに通知を送信
  sendToDiscord(DISCORD_WEBHOOK_URL, message);
}

// Discordにメッセージを送信する関数
function sendToDiscord(discord_url, message) {
  const payload = {
    content: message
  };
  
  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload)
  };
  
  try {
    UrlFetchApp.fetch(discord_url, options);
  } catch (error) {
    Logger.log("Discord通知の送信に失敗しました: " + error);
  }
}

// Claudeで表データを解析する
function analyzePaymentsWithClaude(api_key, promptSheetData) {
  const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
  const API_VERSION = "2023-06-01";
  const MODEL_VERSION = "claude-3-haiku-20240307";
  const MAX_TOKENS = 1000;
  
  // プロンプトの作成
  const prompt = `表計算ソフトのセルと値の情報を渡します。
この表を人間が表形式として認識できる文字列にしてください。

# 表形式
セルアドレス: 値

# 表データ
${promptSheetData}`;

  // APIリクエストのオプション
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': api_key,
      'anthropic-version': API_VERSION
    },
    payload: JSON.stringify({
      model: MODEL_VERSION,
      max_tokens: MAX_TOKENS,
      messages: [{
        role: 'user',
        content: prompt
      }]
    })
  };

  try {
    // APIリクエストを送信
    const response = UrlFetchApp.fetch(ANTHROPIC_URL, options);
    const responseData = JSON.parse(response.getContentText());
    
    // APIレスポンスから結果を取得
    const analysisResult = responseData.content[0].text;
    
    // 結果を返却する
    return analysisResult;

  } catch (error) {
    // エラー発生時
    Logger.log("Anthropic APIの呼び出しに失敗しました: " + error);
  }
}

// トリガーを手動で設定する関数
function createEditTrigger() {
  const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  ScriptApp.newTrigger("onEdit")
    .forSpreadsheet(ss)
    .onEdit()
    .create();
}

主な変更ポイントは以下です。

  • DISCORD_WEBHOOK_URLをプロパティから取得方式に変更
  • 読み込み対象シートを固定化しonEditのイベントオブジェクトは使用しない(onEditはトリガーとしてのみ利用)
  • スプレッドシートから取得した「セル名: 値」の文字列格納先を変数promptSheetDataに変更し関数analyzePaymentsWithClaudeの引数に指定
  • Claude用プロンプトをテンプレートリテラルにしてスプレッドシートから取得したデータを追加
  • エラーログ出力方法をLogger.logに統一
  • ベタ書きの値を定数化

実行してみる

作成したスクリプトを実行してみます。
実行にあたっての事前準備としてトリガを設定する必要がありますが、UIからだとスプレッドシートと紐付いたスクリプトのみ更新トリガが設定でき、スタンドアロンのスクリプトでは設定できません。
そのためスクリプトを使用して設定する必要があります。

ご丁寧にClaudeが生成したコード内にその記述があったため少々改変して使います。
具体的には以下です。

// トリガーを手動で設定する関数
function createEditTrigger() {
  const SPREADSHEET_ID =   PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  ScriptApp.newTrigger("onEdit")
    .forSpreadsheet(ss)
    .onEdit()
    .create();
}

Apps Scriptのエディタ上でcreateEditTrigger関数を指定し「実行」ボタンを押します。
実行ボタンを押すと「承認が必要です」のポップアップが出ますが承認します。
実行ログで「実行完了」と出たら成功です。

Apps ScriptのUI上でも設定が確認できます。



トリガが設定できたらスプレッドシートを更新するとスクリプトが起動されるようになります。
試しにサンプルファイルのD3セルの金額を更新してみます。

するとDiscordにメッセージが投稿されました!
Markdown風味の文字列になっています。

感想

  • 今回GASを使うのは初めてだったが、記事を書きながら開発して半日かからなかった。実質3時間ちょっとで内訳としてスクリプトを書いたのは30分無い位、後の2時間半は設定の正しさやツールの組み合わせ事例について調べていた。効率上がりますね。
  • セル文字を作るクセのあるスクリプトを受け入れたが本当に良かったのかが気にかかっている。今後生成AIがネットをクロールして学習する際に重みがついてしまわないだろうか?もしソースコードレビューで自分がレビュアーだとしたら「このスクリプト・プログラムで今後入って来るメンバが理解しやすいか?バグを埋め込み易くならないか?」と立ち止まると思う。妥当性の検討は誰がするのだろうか、それともAIが全てコードを書くようになるのだとしたら人間が中身に口出しする必要は無いのだろうか
  • マークダウンを生成するプロンプトにすると表形式の理解は無視された。あれこれプロンプトを変えてみたが上手くいかず「人間が表形式として認識できる文字列にしてください」という表現に落ち着きました

参考記事

https://note.com/noa813/n/n307d62b5820b
https://zenn.dev/lambta/articles/5edbda4ccb1ec6
https://zenn.dev/buenotheebiten/articles/04abfe45d61ea2
https://note.com/manabibashi/n/n679edc719696
https://auto-worker.com/blog/?p=6397
https://docs.anthropic.com/ja/api/messages

Discussion