取り組みの背景:Forms × Gemini APIが変えるアンケート運用
Google Formsは手軽に使えるアンケートツールとして、マーケティング調査・採用面接フィードバック・顧客満足度調査・社内研修評価など、あらゆる現場で活用されています。しかし、回答が集まった後の「分析」が課題になっているケースは非常に多いです。
選択式の回答は関数で集計できても、自由記述欄の回答は目で読んで手動でカテゴリ分けする作業が発生しがちです。回答数が50件を超えると分析だけで半日かかることも珍しくありません。
ここに Gemini API を組み合わせることで、状況が一変します。Gemini の高い自然言語理解能力を活用すれば、自由記述回答をリアルタイムで感情分析・テーマ抽出・要約し、構造化されたインサイトレポートとして自動生成できます。
私自身、個人開発で運営している複数のメディアやアプリの改善判断に、読者アンケートの自由記述を使ってきました。Dolice Labs の運用でも、寄せられた声を一件ずつ読み込む時間が積み重なり、肝心の分析がいつも後回しになりがちでした。だからこそ、回答が集まったその瞬間に下処理まで終わっているという状態の心強さは、実感を伴って分かります。その「読む前に整っている」状態を Forms と Gemini API でどう作るのか、ここからは実際に動くコードに沿って一歩ずつ組み立てていきます。
システム全体構成と技術選定
アーキテクチャ概要
構築するシステムの全体フローは以下の通りです。
Google Forms(回答収集)
↓ トリガー(フォーム送信 or 定期実行)
Google Sheets(回答データ蓄積)
↓ Apps Script / Python
Gemini API(自然言語分析)
↓ 分析結果
Google Sheets 分析シート / Gmail レポートメール
技術スタックの選択肢
Apps Script アプローチ(推奨・初中級者向け)
- メリット: Google Workspaceとシームレスに統合、サーバー不要、OAuthが自動処理
- デメリット: 実行時間に6分制限、複雑なデータ処理には向かない
- 向いているケース: 回答数が500件以下、チーム共有が主目的
Python アプローチ(上級者・大規模向け)
- メリット: 実行時間制限なし、pandasなどデータ分析ライブラリと連携可能、CI/CDに組み込める
- デメリット: Google Sheets APIの認証設定が必要、実行環境の準備が必要
- 向いているケース: 回答数が多い、既存のデータ基盤と連携、定期バッチ処理
Step 1: Gemini API キーの取得と設定
Gemini API を利用するには Google AI Studio でAPIキーを取得する必要があります。
Google AI Studio での設定
Google AI Studio にアクセスし、Googleアカウントでサインイン後、「Get API key」からAPIキーを発行します。
Apps Script で使用する場合は、スクリプトプロパティに安全に保存します。
// Google Apps Scriptエディタのコンソールで1度だけ実行
function setApiKey() {
PropertiesService.getScriptProperties().setProperty(
'GEMINI_API_KEY',
'YOUR_GEMINI_API_KEY' // 実際のキーに置き換える
);
Logger.log('APIキーを設定しました');
}使用するモデルの選択
本システムでは Gemini 2.5 Flash の使用を推奨します。
- 理由: 分析精度とコストのバランスが最も優れている
- コスト目安: 入力100万トークンあたり約$0.075(2026年4月時点)
- 1回の分析コスト: 回答50件・各200文字の場合、約$0.002
Step 2: Apps Script 基本実装
フォームとスプレッドシートの連携設定
Google Formsの「回答」タブから「スプレッドシートで表示」を選択し、回答を自動収集するシートを作成します。このシートを分析の起点とします。
メイン分析スクリプト
以下のスクリプトを Apps Script エディタ(スプレッドシートの「拡張機能」→「Apps Script」)に貼り付けます。
// ============================================================
// Google Forms × Gemini API 回答分析システム
// ============================================================
const GEMINI_API_KEY = PropertiesService.getScriptProperties()
.getProperty('GEMINI_API_KEY');
const GEMINI_API_URL =
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
// 分析対象のシート名と列番号を設定
const CONFIG = {
sourceSheetName: 'フォームの回答 1', // Formsが作成するシート名
analysisSheetName: '分析結果',
targetColumns: [3, 4, 5], // 分析する列(1始まり)- 自由記述欄のみ
batchSize: 10, // 一度にGeminiに送る回答数
};
/**
* メイン実行関数 - 未分析の回答を処理する
*/
function analyzeFormResponses() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sourceSheet = ss.getSheetByName(CONFIG.sourceSheetName);
const analysisSheet = getOrCreateAnalysisSheet(ss);
// 未分析行を特定
const lastSourceRow = sourceSheet.getLastRow();
const lastAnalysisRow = analysisSheet.getLastRow();
const startRow = Math.max(2, lastAnalysisRow + 1); // ヘッダー行をスキップ
if (startRow > lastSourceRow) {
Logger.log('新規未分析回答はありません');
return;
}
Logger.log(`${startRow}行目から${lastSourceRow}行目を分析します`);
// バッチ処理
for (let row = startRow; row <= lastSourceRow; row += CONFIG.batchSize) {
const endRow = Math.min(row + CONFIG.batchSize - 1, lastSourceRow);
const batch = [];
for (let r = row; r <= endRow; r++) {
const texts = CONFIG.targetColumns.map(col =>
sourceSheet.getRange(r, col).getValue()
).filter(text => text && text.toString().trim().length > 0);
if (texts.length > 0) {
batch.push({ row: r, texts: texts });
}
}
if (batch.length > 0) {
const results = analyzeBatch(batch);
writeAnalysisResults(analysisSheet, results);
}
// APIレート制限対策
Utilities.sleep(1000);
}
Logger.log('分析完了!');
}
/**
* バッチ分析 - 複数回答をまとめてGeminiに送信
*/
function analyzeBatch(batch) {
const prompt = buildAnalysisPrompt(batch);
const payload = {
contents: [{
parts: [{ text: prompt }]
}],
generationConfig: {
temperature: 0.1, // 分析の一貫性を高めるため低温度設定
responseMimeType: 'application/json', // JSON形式で確実に受け取る
}
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true,
};
const url = `${GEMINI_API_URL}?key=${GEMINI_API_KEY}`;
const response = UrlFetchApp.fetch(url, options);
if (response.getResponseCode() !== 200) {
Logger.log('APIエラー: ' + response.getContentText());
return batch.map(b => ({ row: b.row, error: 'API Error' }));
}
const responseData = JSON.parse(response.getContentText());
const resultText = responseData.candidates[0].content.parts[0].text;
try {
return JSON.parse(resultText);
} catch (e) {
Logger.log('JSONパースエラー: ' + resultText);
return batch.map(b => ({ row: b.row, error: 'Parse Error' }));
}
}
/**
* 分析プロンプト生成
*/
function buildAnalysisPrompt(batch) {
const responses = batch.map((b, i) =>
`回答ID: ${b.row}\n回答内容: ${b.texts.join(' / ')}`
).join('\n\n');
return `
以下のアンケート自由記述回答を分析してください。
${responses}
各回答について以下の形式でJSONを返してください:
{
"results": [
{
"row": 回答IDの数値,
"sentiment": "ポジティブ" or "ネガティブ" or "ニュートラル",
"sentiment_score": -1.0から1.0の数値(-1=非常にネガティブ、1=非常にポジティブ),
"main_themes": ["テーマ1", "テーマ2"],
"key_insight": "この回答から得られる最も重要なインサイト(50文字以内)",
"action_suggestion": "改善・活用のための具体的な提案(50文字以内)"
}
]
}
分析の基準:
- sentimentは回答全体のトーンで判定する
- main_themesは回答から抽出したキーワード・概念(最大3つ)
- key_insightは具体的かつ実用的な洞察を記載する
- action_suggestionは実行可能な行動提案にする
`;
}
/**
* 分析結果をスプレッドシートに書き込む
*/
function writeAnalysisResults(sheet, results) {
if (!results.results) return;
results.results.forEach(result => {
if (result.error) return;
const row = result.row;
const outputRow = sheet.getLastRow() + 1;
sheet.getRange(outputRow, 1).setValue(row); // 元データ行番号
sheet.getRange(outputRow, 2).setValue(result.sentiment);
sheet.getRange(outputRow, 3).setValue(result.sentiment_score);
sheet.getRange(outputRow, 4).setValue(
Array.isArray(result.main_themes) ? result.main_themes.join(', ') : ''
);
sheet.getRange(outputRow, 5).setValue(result.key_insight);
sheet.getRange(outputRow, 6).setValue(result.action_suggestion);
sheet.getRange(outputRow, 7).setValue(new Date()); // 分析日時
// センチメントで行を色付け
const color = result.sentiment === 'ポジティブ' ? '#e8f5e9' :
result.sentiment === 'ネガティブ' ? '#ffebee' : '#f5f5f5';
sheet.getRange(outputRow, 1, 1, 7).setBackground(color);
});
SpreadsheetApp.flush();
}
/**
* 分析結果シートを取得または作成
*/
function getOrCreateAnalysisSheet(ss) {
let sheet = ss.getSheetByName(CONFIG.analysisSheetName);
if (!sheet) {
sheet = ss.insertSheet(CONFIG.analysisSheetName);
// ヘッダー行を追加
const headers = [
'元データ行', 'センチメント', 'スコア', 'テーマ',
'インサイト', '改善提案', '分析日時'
];
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.getRange(1, 1, 1, headers.length)
.setBackground('#1a73e8')
.setFontColor('#ffffff')
.setFontWeight('bold');
sheet.setFrozenRows(1);
}
return sheet;
}Step 3: 集計サマリーレポートの自動生成
個別回答の分析だけでなく、全体のトレンドを把握するためのサマリーレポートを生成します。
/**
* 分析結果の集計サマリーをGeminiで生成
*/
function generateSummaryReport() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const analysisSheet = ss.getSheetByName(CONFIG.analysisSheetName);
if (!analysisSheet || analysisSheet.getLastRow() < 2) {
Logger.log('分析データが不足しています');
return;
}
// 分析データを収集
const data = analysisSheet.getDataRange().getValues();
const headers = data[0];
const rows = data.slice(1);
// 集計情報
const total = rows.length;
const sentimentCounts = { 'ポジティブ': 0, 'ネガティブ': 0, 'ニュートラル': 0 };
const allThemes = [];
const allInsights = [];
rows.forEach(row => {
const sentiment = row[1];
if (sentimentCounts.hasOwnProperty(sentiment)) {
sentimentCounts[sentiment]++;
}
if (row[3]) allThemes.push(...row[3].split(', '));
if (row[4]) allInsights.push(row[4]);
});
// テーマの頻度集計
const themeCounts = {};
allThemes.forEach(t => { themeCounts[t] = (themeCounts[t] || 0) + 1; });
const topThemes = Object.entries(themeCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([theme, count]) => `${theme}(${count}件)`)
.join('、');
// Geminiにサマリー生成を依頼
const summaryPrompt = `
以下はアンケート分析の集計データです。経営・マーケティング担当者向けの実用的なサマリーレポートを日本語で作成してください。
【集計データ】
- 総回答数: ${total}件
- センチメント分布: ポジティブ${sentimentCounts['ポジティブ']}件(${Math.round(sentimentCounts['ポジティブ']/total*100)}%)、ネガティブ${sentimentCounts['ネガティブ']}件(${Math.round(sentimentCounts['ネガティブ']/total*100)}%)、ニュートラル${sentimentCounts['ニュートラル']}件(${Math.round(sentimentCounts['ニュートラル']/total*100)}%)
- 頻出テーマ上位10: ${topThemes}
- 代表的なインサイト(最大20件):
${allInsights.slice(0, 20).map((ins, i) => `${i+1}. ${ins}`).join('\n')}
以下の構成でサマリーレポートを作成してください:
1. 全体傾向の3行サマリー
2. 特筆すべき発見(箇条書き3〜5点)
3. ネガティブ回答からの優先改善事項(箇条書き3点)
4. 次のアクションへの推薦(具体的かつ実行可能な3提案)
読み手が即座に意思決定できる、具体性のあるレポートにしてください。
`;
const summaryResult = callGeminiAPI(summaryPrompt);
// サマリーシートに書き込む
let summarySheet = ss.getSheetByName('サマリーレポート');
if (!summarySheet) {
summarySheet = ss.insertSheet('サマリーレポート');
} else {
summarySheet.clear();
}
const timestamp = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy年MM月dd日 HH:mm');
summarySheet.getRange('A1').setValue(`アンケート分析サマリーレポート - ${timestamp}`);
summarySheet.getRange('A1').setFontSize(16).setFontWeight('bold');
summarySheet.getRange('A2').setValue(summaryResult);
summarySheet.getRange('A2').setWrap(true);
summarySheet.setColumnWidth(1, 600);
Logger.log('サマリーレポートを生成しました');
return summaryResult;
}
/**
* Gemini API呼び出しのユーティリティ関数
*/
function callGeminiAPI(prompt) {
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.3 }
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true,
};
const url = `${GEMINI_API_URL}?key=${GEMINI_API_KEY}`;
const response = UrlFetchApp.fetch(url, options);
if (response.getResponseCode() !== 200) {
throw new Error('Gemini API Error: ' + response.getResponseCode());
}
const data = JSON.parse(response.getContentText());
return data.candidates[0].content.parts[0].text;
}Step 4: Gmailによるレポート自動送信
分析完了後、関係者に自動でメール通知を送る機能を追加します。
/**
* 分析レポートをGmailで自動送信
*/
function sendReportEmail() {
const summary = generateSummaryReport();
const recipients = ['manager@example.com', 'marketing@example.com']; // 送信先
const subject = `【Gemini分析レポート】アンケート結果 - ${
Utilities.formatDate(new Date(), 'Asia/Tokyo', 'MM月dd日')
}`;
const htmlBody = `
<div style="font-family: 'Hiragino Sans', sans-serif; max-width: 700px; margin: 0 auto;">
<h2 style="color: #1a73e8; border-bottom: 2px solid #1a73e8; padding-bottom: 8px;">
アンケート分析レポート(Gemini AI自動分析)
</h2>
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; white-space: pre-wrap;">
${summary.replace(/\n/g, '<br>')}
</div>
<p style="color: #666; font-size: 12px; margin-top: 16px;">
このレポートはGemini APIによって自動生成されました。
詳細は<a href="${SpreadsheetApp.getActiveSpreadsheet().getUrl()}">スプレッドシート</a>をご確認ください。
</p>
</div>
`;
recipients.forEach(recipient => {
GmailApp.sendEmail(recipient, subject, summary, {
htmlBody: htmlBody,
name: 'Gemini分析システム'
});
});
Logger.log(`レポートを ${recipients.join(', ')} に送信しました`);
}Step 5: Python実装(大規模データ対応版)
Apps Script では6分の実行時間制限があるため、回答数が多い場合や定期バッチ処理を行う場合は Python で実装します。
環境準備
pip install google-generativeai google-auth google-auth-oauthlib \
google-auth-httplib2 google-api-python-client \
pandas openpyxl python-dotenvPython メイン実装
"""
Google Forms × Gemini API 回答分析システム (Python版)
"""
import os
import json
import time
from datetime import datetime
from typing import Optional
import pandas as pd
from dotenv import load_dotenv
import google.generativeai as genai
from googleapiclient.discovery import build
from google.oauth2.service_account import Credentials
load_dotenv()
# 設定
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
SPREADSHEET_ID = os.getenv('SPREADSHEET_ID') # Google SheetsのID
SERVICE_ACCOUNT_FILE = 'service_account.json' # サービスアカウントキー
# Gemini API設定
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel('gemini-2.5-flash')
# Google Sheets API設定
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
def get_sheets_service():
"""Google Sheets APIクライアントを取得"""
creds = Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
return build('sheets', 'v4', credentials=creds)
def fetch_form_responses(service, sheet_name: str = 'フォームの回答 1') -> pd.DataFrame:
"""Google Sheetsからフォーム回答を取得"""
result = service.spreadsheets().values().get(
spreadsheetId=SPREADSHEET_ID,
range=f"'{sheet_name}'!A:Z"
).execute()
values = result.get('values', [])
if not values:
return pd.DataFrame()
df = pd.DataFrame(values[1:], columns=values[0])
print(f"📊 {len(df)}件の回答を取得しました")
return df
def analyze_with_gemini(texts: list[str]) -> dict:
"""Gemini APIで回答を分析"""
if not texts:
return {}
# 複数テキストを結合して分析
combined_text = ' / '.join([t for t in texts if t.strip()])
prompt = f"""
以下のアンケート回答を分析して、JSONで結果を返してください。
回答: {combined_text}
返却形式(JSONのみ、マークダウンなし):
{{
"sentiment": "ポジティブ" or "ネガティブ" or "ニュートラル",
"sentiment_score": -1.0から1.0の数値,
"main_themes": ["テーマ1", "テーマ2"],
"key_insight": "主要インサイト(50文字以内)",
"action_suggestion": "改善提案(50文字以内)",
"urgency": "高" or "中" or "低"
}}
"""
try:
response = model.generate_content(
prompt,
generation_config=genai.GenerationConfig(
temperature=0.1,
response_mime_type='application/json'
)
)
return json.loads(response.text)
except Exception as e:
print(f"❌ 分析エラー: {e}")
return {
'sentiment': 'ニュートラル',
'sentiment_score': 0.0,
'main_themes': [],
'key_insight': 'エラーが発生しました',
'action_suggestion': '手動確認が必要です',
'urgency': '低'
}
def analyze_all_responses(df: pd.DataFrame, text_columns: list[str]) -> pd.DataFrame:
"""全回答を分析してDataFrameを返す"""
results = []
total = len(df)
for i, (idx, row) in enumerate(df.iterrows()):
print(f"⏳ 分析中: {i+1}/{total}件目...")
texts = [str(row[col]) for col in text_columns
if col in df.columns and str(row[col]).strip()]
analysis = analyze_with_gemini(texts)
result = {
'original_index': idx,
**{col: row[col] for col in df.columns},
'sentiment': analysis.get('sentiment', ''),
'sentiment_score': analysis.get('sentiment_score', 0),
'main_themes': ', '.join(analysis.get('main_themes', [])),
'key_insight': analysis.get('key_insight', ''),
'action_suggestion': analysis.get('action_suggestion', ''),
'urgency': analysis.get('urgency', '低'),
'analyzed_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
results.append(result)
# APIレート制限対策(60リクエスト/分を超えないよう調整)
time.sleep(1)
return pd.DataFrame(results)
def generate_summary_report(results_df: pd.DataFrame) -> str:
"""全体サマリーレポートを生成"""
total = len(results_df)
sentiment_dist = results_df['sentiment'].value_counts().to_dict()
top_themes = results_df['main_themes'].str.split(', ').explode().value_counts().head(10)
urgent_count = len(results_df[results_df['urgency'] == '高'])
# Geminiにサマリー生成を依頼
insights_sample = results_df['key_insight'].dropna().head(20).tolist()
prompt = f"""
アンケート分析サマリーを作成してください。
データ:
- 総回答数: {total}件
- センチメント: {sentiment_dist}
- 緊急度「高」件数: {urgent_count}件
- 頻出テーマ: {dict(top_themes)}
- 代表的インサイト: {insights_sample[:10]}
以下の形式でレポートを作成してください:
1. エグゼクティブサマリー(3行)
2. 主な発見事項(3〜5点)
3. 優先対応事項(緊急度「高」回答から)
4. 推奨アクション(具体的な3提案)
"""
response = model.generate_content(prompt)
return response.text
def upload_results_to_sheets(service, results_df: pd.DataFrame):
"""分析結果をGoogle Sheetsに書き込む"""
# DataFrameをリストに変換
headers = results_df.columns.tolist()
values = [headers] + results_df.values.tolist()
# 既存データをクリアして書き込み
service.spreadsheets().values().clear(
spreadsheetId=SPREADSHEET_ID,
range='分析結果!A:Z'
).execute()
service.spreadsheets().values().update(
spreadsheetId=SPREADSHEET_ID,
range='分析結果!A1',
valueInputOption='RAW',
body={'values': [[str(v) for v in row] for row in values]}
).execute()
print(f"✅ {len(results_df)}件の分析結果をGoogle Sheetsに書き込みました")
def main():
print("🚀 Google Forms × Gemini API 分析システム起動")
service = get_sheets_service()
# 回答データを取得
df = fetch_form_responses(service)
if df.empty:
print("⚠️ 回答データがありません")
return
# 分析対象の列を指定(自由記述欄の列名を設定)
text_columns = ['ご意見・ご要望', 'よかった点', '改善点']
# 全回答を分析
results_df = analyze_all_responses(df, text_columns)
# サマリーレポートを生成
print("\n📝 サマリーレポートを生成中...")
summary = generate_summary_report(results_df)
print("\n=== サマリーレポート ===")
print(summary)
# 結果をSheetsに書き込む
upload_results_to_sheets(service, results_df)
# Excelファイルとしても保存
output_file = f"analysis_report_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
results_df.to_excel(output_file, index=False)
print(f"💾 Excelレポートを保存しました: {output_file}")
if __name__ == '__main__':
main()Step 6: トリガー設定と自動化
Apps Script のトリガー設定
分析を自動実行するためのトリガーを設定します。
/**
* トリガーを一括設定する関数
* 初回のみ手動実行する
*/
function setupTriggers() {
// 既存トリガーを削除
ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t));
// フォーム送信時にリアルタイム分析
const ss = SpreadsheetApp.getActiveSpreadsheet();
ScriptApp.newTrigger('analyzeFormResponses')
.forSpreadsheet(ss)
.onChange()
.create();
// 毎日9時にサマリーレポートメール送信
ScriptApp.newTrigger('sendReportEmail')
.timeBased()
.everyDays(1)
.atHour(9)
.create();
Logger.log('トリガーを設定しました');
}Python の定期実行設定(cron / Cloud Scheduler)
# crontabに追加する場合(毎日8時に実行)
# crontab -e で編集
0 8 * * * cd /path/to/project && python3 main.py >> logs/analysis.log 2>&1
# Google Cloud Schedulerを使う場合(Cloud Runと組み合わせ)
gcloud scheduler jobs create http forms-analysis-job \
--schedule="0 8 * * *" \
--uri="https://YOUR_CLOUD_RUN_URL/analyze" \
--time-zone="Asia/Tokyo" \
--http-method=POSTStep 7: 応用活用 — Looker Studioへのデータ連携
Google Sheetsに分析結果を蓄積しておけば、Looker Studio(旧Google Data Studio)と接続してビジュアルダッシュボードを作成できます。
連携手順
Looker Studio(lookerstudio.google.com)にアクセスし、「データソースを追加」→「Google スプレッドシート」を選択して、分析結果シートを指定します。
推奨チャート構成
- センチメント分布 → ドーナツチャート
- センチメントスコアの時系列推移 → 折れ線グラフ
- テーマ出現頻度 → 棒グラフ
- 緊急度別回答一覧 → テーブル(緊急度「高」をフィルタで常時表示)
よくあるエラーと対処法(FAQ)
Q1. Apps ScriptでAPIキーが無効とエラーが出る
スクリプトプロパティに正しくキーが保存されているか確認します。setApiKey() 関数を再実行し、Logger.log(GEMINI_API_KEY) でキーが表示されるかテストします。APIキーの末尾に余分なスペースが入っている場合も tr -d '[:space:]' で解決できます。
Q2. 分析結果のJSONが正しく解析できない
Gemini のレスポンスにマークダウンのコードブロック記法(json ... )が含まれることがあります。responseMimeType: 'application/json' を明示的に指定することで、純粋なJSONのみが返るようになります。確実に解析したい場合は、Python 側でレスポンス文字列からコードフェンスを取り除いてから json.loads() に渡します。
import re, json
def parse_gemini_json(text: str) -> dict:
# ```json ... ``` のようなフェンスを取り除いてから解析する
cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip())
return json.loads(cleaned)この前処理を挟んでおくと、Gemini が稀にフェンス付きで返してきた場合でも json.loads() が落ちません。
Q3. 大量の回答で処理が途中で止まる
Apps Script の場合は6分の実行時間制限があります。回答が多い場合は CONFIG.batchSize を5に下げ、続きの処理を別の実行に分けるようにトリガーを工夫します。Python では time.sleep(1) を入れているため問題ありませんが、APIのQuota上限(デフォルトは60RPM)に注意してください。Gemini APIのダッシュボードでQuotaを確認・増加申請ができます。
Q4. 日本語の分析精度が低い
プロンプトに分析例(few-shot)を追加すると精度が向上します。また、業界固有の用語はプロンプトにグロッサリーとして追加します(例:「〇〇サービス固有の用語として△△は□□を意味します」)。
Q5. 機密性の高い回答データをAPIに送っても大丈夫か
Gemini API の利用規約上、入力データはモデルのトレーニングに使われない設定にできます(Google AI Studio のプロジェクト設定で「データを改善に使用しない」を選択)。個人情報が含まれる場合は、送信前に氏名・メールアドレスなどを匿名化する前処理を追加することを強く推奨します。
Q6. Google Workspace の有料プランが必要か
Apps Script と Google Sheetsは無料プランでも利用可能です。Gemini API は Google AI Studio で発行したAPIキーで、月あたりの無料枠(15 RPM / 1,500,000 TPM)の範囲で利用できます。商用・大規模利用の場合は有料プランへの移行を検討してください。
まとめ:Forms × Gemini APIで分析業務を変革する
ここではGoogle Forms の回答を Gemini API で自動分析するシステムを構築するための、Apps Script と Python の完全な実装コードを提供しました。
このシステムを導入することで得られる主な効果をまとめます。
- 時間削減: 手動分析の作業時間を90%以上削減(100件の分析が数時間→数分)
- 品質向上: 感情分析・テーマ抽出が一貫した基準で実施され、分析者の主観が排除される
- スピード向上: フォーム送信と同時にリアルタイム分析が可能
- インサイト深化: 単なる集計を超えた「なぜそう感じたか」「何を改善すべきか」まで抽出
次のステップとして、本システムをさらに発展させる方向性として、回答のクラスタリング(K-meansでの自動グルーピング)、時系列での感情トレンド可視化、他のGoogle Workspaceツール(Gmail・Docs)との連携による自動改善提案レポートの生成なども検討してみてください。
Gemini APIと Google Workspace の組み合わせは、まだまだ多くの業務自動化の可能性を秘めています。本記事が皆さんのアンケート運用改革の第一歩になれば幸いです。
本記事で構築したシステムと組み合わせると特に効果的な関連ガイドも参照ください。分析結果のGoogle Sheetsへの書き込みをさらに発展させたい方にはGemini × Google Sheets Apps Script で構築するAI自動レポート生成パイプラインが、Looker Studioでのビジュアル化を深掘りしたい方にはGoogle Looker Studio × Gemini API でビジネスレポートをAI自動分析する実践ガイドが、複数のWorkspaceツールを横断した自動化に挑戦したい方にはGemini API × Apps Script で Gmail・Calendar・Drive を横断する全自動 AI ワークフロー構築ガイドが参考になります。
本記事の実装