GASでアンケートの自由記述を
AIで感情分析する方法
アンケートの自由記述欄に集まった声を、UrlFetchApp(GASから外部APIを呼ぶ標準機能)とClaude API(Anthropic社の生成AI)で感情分析し、 スコア・判定・話題をスプレッドシートに自動で書き込む方法を、そのまま動くコードで解説します。
Table of Contents
自由記述は「読む」より「数える」に変える
結論から言うと、アンケートの自由記述はGAS(Google Apps Script。Googleサービスを操作する無料のプログラム実行環境)と AIで数値化できます。1件ずつ読んで印象を語る代わりに、0.0〜1.0のスコアと判定ラベルを全件に付けて集計する——これが感情分析(センチメント分析)です。
数値になると、選択式の設問と同じように扱えます。満足度の平均を出す、月ごとの推移を折れ線にする、 ネガティブが集中している話題を特定する。読む対象を絞り込めるので、100件の回答があっても 最初に読むべき10件がすぐ分かります。この記事で作る仕組みは次の4つを自動で埋めます。
score(スコア)
0.0(非常に否定的)〜1.0(非常に肯定的)の数値。集計やグラフ化に使う
判定ラベル
ポジティブ / 中立 / ネガティブ。閾値はGAS側の固定ルールで決める
topic(話題)
価格・対応・使いやすさなど、回答が触れている論点を1語で
reason(理由)
なぜその判定になったかの短い説明。目視チェックの手がかりになる
ポイントは、1件ずつAPIを呼ばず10件ずつまとめて送ることです。指示文は毎回ほぼ同じなので、まとめて送ればAPI呼び出し回数も待ち時間も大きく減らせます。
準備:APIキーの保管とシート設計
シートは「アンケート」という名前で用意し、A列に自由記述、B〜E列を書き込み先にします。 1行目はヘッダー(自由記述 / スコア / 判定 / 話題 / 理由)です。Googleフォームの回答シートをそのまま使う場合は、自由記述の列がA列に来るようシートを分けるか、 コード内の列番号を実際の位置に合わせてください。
次にClaude APIのキーです。このキーは料金が発生する権限そのものなので、コードに直書きしてはいけません。GASの「スクリプトプロパティ」(PropertiesServiceで読み書きする、スクリプト単位の設定保存領域)に保管し、コードからはキー名で読み出します。
// 一度だけ実行してAPIキーを安全に保存する
function saveApiKey() {
// 実行後、この関数のキー文字列は削除しておく
PropertiesService.getScriptProperties()
.setProperty("ANTHROPIC_API_KEY", "sk-ant-xxxxxxxx");
}
// コードからはキー名で読み出す(直書きしない)
function getApiKey() {
const key = PropertiesService.getScriptProperties()
.getProperty("ANTHROPIC_API_KEY");
if (!key) throw new Error("APIキーが未設定です。saveApiKeyを実行してください。");
return key;
}なお、アンケート回答には氏名やメールアドレスが含まれることがあります。分析に不要な情報は APIに送らないのが原則です。今回のコードはA列の自由記述しか送らないため、他の列は外部に出ません。
10件まとめて分析し、JSONで受け取る
複数の自由記述を一度に渡し、結果をJSON配列(プログラムがそのまま読めるデータ形式)で受け取る関数を作ります。 安定させるコツは2つ。回答に通し番号を振って順番を固定すること、そして出力形式を実例つきで指定することです。
// 複数件の自由記述をまとめて感情分析し、JSON配列で受け取る
// texts: ["対応が早くて助かりました", "画面が分かりにくい", ...]
function analyzeSentiments(texts) {
const url = "https://api.anthropic.com/v1/messages";
// 1件ずつ番号を振って渡す(順番を対応づけるため)
const numbered = texts
.map(function (t, i) { return (i + 1) + ". " + t; })
.join("\n");
const prompt =
"あなたはアンケート回答の感情分析を行います。\n" +
"各回答について、次の3項目を判定してください。\n" +
"・score: 0.0(非常に否定的)〜1.0(非常に肯定的)の数値\n" +
"・topic: 回答が触れている話題を1語で(例: 価格, 対応, 使いやすさ, 納期)\n" +
"・reason: 判定理由を20文字以内で\n\n" +
"出力ルール:\n" +
"・JSON配列だけを出力する。説明文やコードブロック記号は書かない。\n" +
"・入力と同じ件数・同じ順番で返す。\n" +
'・形式: [{"score":0.9,"topic":"対応","reason":"迅速さを評価"}]\n\n' +
"回答一覧:\n" + numbered;
const payload = {
model: "claude-haiku-4-5",
max_tokens: 2000,
messages: [{ role: "user", content: prompt }]
};
const options = {
method: "post",
contentType: "application/json",
headers: {
"x-api-key": getApiKey(),
"anthropic-version": "2023-06-01"
},
payload: JSON.stringify(payload),
muteHttpExceptions: true // エラー時も例外にせずレスポンスを受け取る
};
const res = UrlFetchApp.fetch(url, options);
if (res.getResponseCode() !== 200) {
throw new Error("API エラー (" + res.getResponseCode() + "): " + res.getContentText());
}
const raw = JSON.parse(res.getContentText()).content[0].text;
return parseJsonArray(raw, texts.length);
}プロンプト(AIへの指示文)で「JSON配列だけを出力する」「入力と同じ件数・同じ順番で返す」と 明示している点が重要です。順番が入れ替わると、別の回答の結果を別の行に書き込んでしまいます。max_tokensは10件分の応答が収まるよう、1件あたりの見積もりに余裕を持たせて設定してください。 モデルは軽量で安価なclaude-haiku-4-5から始め、判定精度を上げたいときに上位モデルへ差し替えます。
AIの返答を安全にJSONへ変換する
「JSONだけを出力して」と頼んでも、前後に説明文やコードブロックの記号が混ざることがあります。 そのままJSON.parse()に渡すと例外で止まるため、最初の[から最後の]までを切り出してから変換します。あわせて件数のチェックも入れます。
// AIの返答からJSON配列だけを取り出す(前後の余計な文字への保険)
function parseJsonArray(raw, expectedLength) {
const start = raw.indexOf("[");
const end = raw.lastIndexOf("]");
if (start === -1 || end === -1) {
throw new Error("JSON配列が見つかりません: " + raw);
}
const parsed = JSON.parse(raw.slice(start, end + 1));
// 件数がずれた応答は信用しない(行のズレを防ぐ)
if (parsed.length !== expectedLength) {
throw new Error("件数が一致しません: " + parsed.length + " / " + expectedLength);
}
return parsed;
}件数が合わない応答をそのまま書き込むと、行が1つずれて全件の対応関係が壊れます。ずれるくらいなら書き込まない方が安全なので、ここでエラーにして呼び出し元に判断を任せます。
もう一点、ポジティブ/ネガティブの境界はAIに判定させず、GAS側の固定ルールにします。 AIは同じ文でも出力が揺れることがあるため、閾値の比較はコードで行った方が結果が一貫します。
// 閾値の判定はAIに任せず、GAS側で固定ルールとして行う
function toLabel(score) {
if (typeof score !== "number") return "判定不能";
if (score >= 0.6) return "ポジティブ";
if (score < 0.3) return "ネガティブ";
return "中立";
}シート全体をバッチ処理で分析する
本体です。A列に記述があり、B列(スコア)が空の行だけを集め、10件ずつAPIに送ります。 判定済みの行はスキップされるので、途中で止まっても再実行すれば続きから埋まります。
// A列の自由記述を10件ずつ分析し、B〜E列に書き込む
// シート構成: A=自由記述, B=スコア, C=判定, D=話題, E=理由
const BATCH_SIZE = 10; // 1回のAPI呼び出しで送る件数
const MAX_ROWS = 100; // 1回の実行で処理する上限(実行時間制限対策)
function analyzeSurveySheet() {
const sheet = SpreadsheetApp.getActive().getSheetByName("アンケート");
const lastRow = sheet.getLastRow();
if (lastRow < 2) return; // データなし(1行目はヘッダー想定)
const rows = sheet.getRange(2, 1, lastRow - 1, 2).getValues();
// 「自由記述があり、B列が空」の行だけを対象にする
const targets = [];
for (let i = 0; i < rows.length && targets.length < MAX_ROWS; i++) {
const text = String(rows[i][0]).trim();
const done = String(rows[i][1]).trim();
if (text && !done) {
targets.push({ row: i + 2, text: text }); // row は実際のシート行番号
}
}
if (targets.length === 0) return;
// BATCH_SIZE件ずつに分けて処理する
for (let s = 0; s < targets.length; s += BATCH_SIZE) {
const chunk = targets.slice(s, s + BATCH_SIZE);
let results;
try {
results = analyzeSentiments(chunk.map(function (t) { return t.text; }));
} catch (e) {
// このかたまりだけ諦め、次のかたまりに進む(全体を止めない)
console.error("分析に失敗: 行" + chunk[0].row + " / " + e.message);
continue;
}
// 連続する行なのでまとめて書き込む(1件ずつのsetValueより速い)
const values = results.map(function (r) {
return [r.score, toLabel(r.score), r.topic, r.reason];
});
sheet.getRange(chunk[0].row, 2, values.length, 4).setValues(values);
SpreadsheetApp.flush(); // 途中経過をすぐ画面に反映する
Utilities.sleep(500); // 連続呼び出しの間隔をあける
}
}書き込みはsetValues()で10行×4列をまとめて行います。1セルずつsetValue()を呼ぶより大幅に速く、実行時間制限(無料枠で6分)にも余裕が生まれます。
なお、setValues()でまとめ書きできるのは連続した行が対象のときです。上のコードは未処理行を上から順に集めているため、 シートの途中に判定済みの行が混ざると、かたまりの中で行が飛ぶ可能性があります。 判定済みが飛び石状に混在する運用では、1行ずつchunk[j].rowを指定して書き込む形に変えてください。
失敗したかたまりはcontinueで飛ばし、全体を止めません。空のままの行は次の実行で再び対象になるので、 一時的なAPIエラーは再実行だけで回復します。
ネガティブ回答だけをSlackに通知する
分析結果を活かすなら、通知までつなげます。判定が「ネガティブ」の行だけをSlackに流せば、 不満の声を見逃しません。Slack側の設定(Incoming Webhook。指定URLにPOSTするとメッセージが投稿される仕組み)は 関連記事で解説しています。
// ネガティブ判定の行をSlackに通知する(Webhook URLはプロパティに保管)
function notifyNegative() {
const sheet = SpreadsheetApp.getActive().getSheetByName("アンケート");
const lastRow = sheet.getLastRow();
if (lastRow < 2) return;
// A=記述, B=スコア, C=判定, D=話題, E=理由, F=通知済みフラグ
const rows = sheet.getRange(2, 1, lastRow - 1, 6).getValues();
const webhook = PropertiesService.getScriptProperties()
.getProperty("SLACK_WEBHOOK_URL");
if (!webhook) return;
for (let i = 0; i < rows.length; i++) {
const label = String(rows[i][2]).trim();
const notified = String(rows[i][5]).trim();
if (label !== "ネガティブ" || notified) continue; // 二重通知を防ぐ
const message =
"【ネガティブ回答】話題: " + rows[i][3] + "\n" +
"> " + String(rows[i][0]).slice(0, 200);
UrlFetchApp.fetch(webhook, {
method: "post",
contentType: "application/json",
payload: JSON.stringify({ text: message })
});
sheet.getRange(i + 2, 6).setValue("通知済み"); // F列にフラグを立てる
}
}F列に「通知済み」フラグを立てるのがポイントです。これがないと、実行のたびに同じ回答を 何度も通知してしまいます。時間主導トリガー(決まった間隔で関数を自動実行するGASの仕組み)でanalyzeSurveySheetとnotifyNegativeを1時間おきに回せば、回答が入るたびに分析と通知が自動で進みます。
まとめ
自由記述の感情分析は、GASとClaude APIの組み合わせで自動化できます。押さえるべきは5つです。 APIキーはスクリプトプロパティで保管する。出力形式をJSONで実例つきに指定する。 返答は切り出しと件数チェックを通してから使う。ポジ・ネガの閾値はGAS側の固定ルールで判定する。 10件ずつのバッチと未処理行スキップで、API制限と実行時間制限をかわす。
この型は感情分析に限りません。「複数件をまとめてAIに渡し、構造化されたJSONで受け取り、シートに戻す」 という流れは、要約・タグ付け・優先度判定にもそのまま応用できます。
よくある質問
感情分析(センチメント分析)とは、文章が肯定的か否定的かを判定する処理です。アンケートの自由記述に使うと、「満足している回答が何割か」「不満はどの話題に集中しているか」を数字で把握できます。全部を読まなくても、ネガティブな回答だけを先に読む、といった優先順位づけができるようになります。
件数が多いならまとめて送る(バッチ処理)方が有利です。プロンプト(AIへの指示文)は毎回ほぼ同じ内容なので、1件ずつ送ると同じ指示を何度も課金対象として送ることになります。10件ずつまとめれば、API呼び出し回数も待ち時間も約10分の1になります。ただし1回の応答が長くなるので、max_tokensを十分に確保してください。
「JSONだけを出力し、説明文やコードブロックの記号は付けない」とプロンプトで明示するのが基本です。それでも前後に文字が混ざることがあるため、本文のコードでは最初の [ から最後の ] までを切り出してからJSON.parse()する保険を入れています。加えて、パースに失敗した場合はその回だけスキップして処理を止めない作りにすると安定します。
完全に同じとは限りません。生成AIは同じ入力でも出力が揺れることがあります。基準を安定させるには、プロンプトに「0.0〜1.0の数値」「0.3未満はネガティブ」のように定義と境界値を書き、判定例を1〜2件添えるのが効果的です。厳密な一貫性が必要な用途では、閾値の判定はAIに任せずGAS側の比較演算で行ってください。
社内規程と利用規約の確認が先です。氏名やメールアドレスなど、分析に不要な情報は送らないのが原則です。GAS側で該当列を除外する、もしくは送信前に置換して伏せることで、送るデータを最小限にできます。判断に迷う場合は、まず社内の情報管理担当に確認してください。
できます。GAS自体は無料で使えます。ただし1回の実行あたり6分の実行時間制限(Google Workspaceは30分)があります。本文のコードは1回の実行で処理する件数を区切り、判定済みの行をスキップする作りなので、件数が多い場合は再実行するだけで続きから処理できます。別途Claude APIの利用料金は発生します。
関連するサービス・記事
AI×GASの業務自動化を
相談する。
アンケート分析のほかにも、問い合わせ分類・要約・返信下書きの自動生成など、 AI APIとGASを組み合わせた自動化をご相談いただけます。既存のスプレッドシート運用にもそのまま組み込めます。