はじめに
こんにちは!キャンプ歴4年目のズボラキャンパーです。
前回、「Geminiにキャンプ予約メールを読ませてGoogleカレンダーに自動登録する」 というGASを作りました。
こんな感じで表示できるようになっています!
誰か私にほったらかしキャンプ場の予約をとれる力を💪!
これで予約忘れはなくなりましたが、運用していて1つだけ 「めんどくさい」 と思うことがありました。
「新しいキャンプ場に行くたびに、GASのコードを開いてリストを更新するのがダルい」
スマホでキャンプ場を予約した後、PCを開いてスクリプトエディタを立ち上げて、ドメインを調べて追記して……。
いや、もっと楽にしたい。スマホからURLをペタッと貼るだけでリスト更新したい。
というわけで今回は、前回の仕組みを 「スプレッドシート連携版」 に進化させます。
今回の改善点
Before(前回)
- GASのコード内に許可リストを直書き。
- 更新するにはスクリプトエディタを開く必要がある。
After(今回)
- スプレッドシートに予約サイトのURLを貼るだけ。
- 関数が自動でドメインを抽出。
- GASはシートを見に行くだけ。コードへの追加不要!
実装ステップ
1. 管理用スプレッドシートの作成
まずはGoogleドライブで新規スプレッドシートを作成し、シート名を「キャンプ場リスト」にします。
以下のような列を作ります。
- A列: キャンプ場名(自分がわかればOK)
- B列: 予約サイトのURL(ここをコピペする)
- C列: ドメイン/キーワード(自動抽出エリア)
魔法の数式(ここがミソ!)
C2セルに、以下の数式を入力して、下の方までオートフィル(コピー)しておきます。
=IFERROR(REGEXEXTRACT(B2, "https?://([^/]+)/"), B2)
何をしているの?
B列に https://www.nap-camp.com/shizuoka/1234 と入力されると、www.nap-camp.com だけを抜き出してくれます。
もしURLじゃない文字(例:「楽天トラベル」など)を入力した場合は、そのままの文字を表示します。
これで、「B列にURLを貼るだけで、C列にGAS用の検索キーワードが出来上がる」 状態になりました。スマホのGoogleスプレッドシートアプリからも簡単に追記できますね!
2. GASのコード修正
前回のコードを少し修正して、このスプレッドシートを読みに行くようにします。
まず、スプレッドシートのURLに含まれるID(d/と/editの間の文字列)をコピーしておきます。
🏕️ newコード.gs 全文(クリックして展開)
/**
* キャンプ場予約メール自動カレンダー登録スクリプト
* - Gmailから予約メールを検索
* - スプレッドシートのホワイトリストでフィルタリング
* - Gemini APIで内容を解析
* - Googleカレンダーに登録
*/
// ==========================================
// 1. 設定エリア
// ==========================================
// ★重要: これより上にコードがないことを確認してください
const SPREADSHEET_ID = 'スプレッドシートID'; // スプレッドシートID
const SHEET_NAME = 'キャンプ場リスト'; // シート名
// 検索クエリ(2025年以降、予約関連、未処理のもの)
const SEARCH_QUERY = 'subject:("予約" OR "完了" OR "Reservation") -subject:予約受付 -subject:仮予約 -label:カレンダー登録済 after:2025/01/01';
const CALENDAR_ID = 'アカウントメールアドレス'; // 登録先カレンダーID
// ==========================================
// 2. メイン処理
// ==========================================
function main() {
console.log('処理を開始します...');
// Gmailから検索
const threads = GmailApp.search(SEARCH_QUERY, 0, 20);
if (threads.length === 0) {
console.log('検索条件にヒットする未処理メールは0件でした。');
return;
}
console.log(`${threads.length} 件のスレッドがヒットしました。処理を開始します。`);
// スプレッドシートから許可リストを取得(1回だけ実行)
const allowList = getAllowListFromSheet();
if (allowList.length === 0) {
console.warn('【警告】許可リストが空、もしくは取得できませんでした。処理を中断します。');
return;
}
console.log(`許可リスト読み込み完了: ${allowList.length}件のキーワード`);
// スレッドごとのループ
threads.forEach(thread => {
const messages = thread.getMessages();
let threadHasProcessed = false; // このスレッド内で1つでも処理成功したかどうかのフラグ
// スレッド内のメッセージ(メール)ごとのループ
messages.forEach(message => {
// 判定ロジック:件名やFromがリストに含まれるか
if (isTargetEmail(message, allowList)) {
console.log(`処理対象を発見: ${message.getSubject()}`);
// Gemini実行 & カレンダー登録処理
const isSuccess = processReservation(message);
// 成功した場合、フラグを立てる
if (isSuccess) {
threadHasProcessed = true;
}
} else {
// 対象外の場合はデバッグログ(必要に応じてコメントアウトを外してください)
console.log(`対象外スキップ: ${message.getSubject()} / From: ${message.getFrom()}`);
}
});
// スレッド内のいずれかのメール処理が成功していれば、スレッド全体にラベルを貼る
if (threadHasProcessed) {
markAsProcessed(thread);
console.log('ラベル付与完了');
}
});
console.log('処理終了');
}
// ==========================================
// 3. サブ関数群
// ==========================================
/**
* スプレッドシートから許可リスト(ドメインやキーワード)を取得
*/
function getAllowListFromSheet() {
try {
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
if (!sheet) {
console.error(`シート「${SHEET_NAME}」が見つかりません。`);
return [];
}
const lastRow = sheet.getLastRow();
if (lastRow < 2) return []; // データがない場合
// C列(3列目)の2行目からデータがある最後まで取得
const data = sheet.getRange(2, 3, lastRow - 1, 1).getValues();
// 空白を除去して一次元配列にする
return data.flat().filter(String);
} catch (e) {
console.error('スプレッドシート読み込みエラー:', e);
return [];
}
}
/**
* メールが処理対象かどうかを判定
* @param {GoogleAppsScript.Gmail.GmailMessage} message
* @param {Array} list 許可キーワードの配列
*/
function isTargetEmail(message, list) {
const subject = message.getSubject();
const from = message.getFrom();
// リスト内のキーワードがいずれか含まれていればTrue
return list.some(keyword => {
return subject.includes(keyword) || from.includes(keyword);
});
}
/**
* 予約メールを処理してカレンダーに登録
*/
function processReservation(message) {
const body = message.getPlainBody();
const subject = message.getSubject();
// Geminiに解析を依頼
const eventDetails = parseEmailWithGemini(body);
// 解析失敗またはカレンダー登録不要なメールだった場合
if (!eventDetails) return false;
const start = new Date(eventDetails.startTime);
const end = new Date(eventDetails.endTime);
// 日付チェック
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
console.error(`日付形式エラー: start=${eventDetails.startTime}, end=${eventDetails.endTime}`);
return false;
}
// カレンダーのメモ欄用に、時間を整形して取り出しておく
const timeZone = Session.getScriptTimeZone();
const timeStrStart = Utilities.formatDate(start, timeZone, 'HH:mm');
const timeStrEnd = Utilities.formatDate(end, timeZone, 'HH:mm');
// 終日イベントのバーを正しく引くため、終了日を+1日する(Googleカレンダーの仕様)
const endForAllDay = new Date(end);
endForAllDay.setDate(endForAllDay.getDate() + 1);
const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
try {
// 終日イベントとして登録
calendar.createAllDayEvent(`🏕 ${eventDetails.title}`, start, endForAllDay, {
description: `【時間】 ${timeStrStart} IN 〜 ${timeStrEnd} OUT\n\n【詳細】\n元メール: ${subject}\nメモ: ${eventDetails.memo || 'なし'}`
});
console.log(`カレンダー登録成功: ${eventDetails.title} (${Utilities.formatDate(start, timeZone, 'yyyy/MM/dd')})`);
return true;
} catch (e) {
console.error('カレンダー登録エラー:', e);
return false;
}
}
/**
* Gemini APIを呼び出してメール本文からJSONを抽出
*/
function parseEmailWithGemini(emailBody) {
const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
if (!apiKey) {
console.error('スクリプトプロパティに GEMINI_API_KEY が設定されていません');
return null;
}
// モデル設定 (安定版の gemini-1.5-flash を推奨)
const model = 'gemini-1.5-flash';
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
const prompt = `
以下のキャンプ場予約メールの本文から、カレンダー登録に必要な情報を抽出してJSONのみを返してください。
【重要:日付の抽出ルール】
- チェックイン日とチェックアウト日を正確に抽出してください。
- 年号がない場合は、メール受信日や文脈から2025年などを推測してください。
【抽出項目】
- title: キャンプ場名
- startTime: チェックイン日 (YYYY-MM-DD形式)
- endTime: チェックアウト日 (YYYY-MM-DD形式) ※1泊2日なら翌日の日付
- memo: サイト名やプラン名など
【メール本文】
${emailBody}
`;
const payload = { contents: [{ parts: [{ text: prompt }] }] };
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
const responseCode = response.getResponseCode();
const json = JSON.parse(response.getContentText());
if (responseCode !== 200) {
console.error(`Gemini API Error (${responseCode}):`, json);
return null;
}
if (!json.candidates || json.candidates.length === 0) return null;
const text = json.candidates[0].content.parts[0].text;
// コードブロック記号(```json 等)を除去してパース
const cleanJson = text.replace(/```json/g, '').replace(/```/g, '').trim();
return JSON.parse(cleanJson);
} catch (e) {
console.error('Gemini解析中の例外:', e);
return null;
}
}
/**
* スレッドに「処理済み」ラベルを付与
*/
function markAsProcessed(thread) {
const labelName = 'カレンダー登録済';
let label = GmailApp.getUserLabelByName(labelName);
if (!label) {
// ラベルがなければ作成
label = GmailApp.createLabel(labelName);
}
thread.addLabel(label);
}
// ※ processReservation, parseEmailWithGemini, markAsProcessed は前回と同じなので省略
// (前回のコードの下にそのまま残しておいてOKです)
※前回の記事の parseEmailWithGemini 関数などはそのまま使えます。
前回のコードの下にそのまま残しておいてOKです。
isTargetEmail 関数だけ少し変わっているので注意して書き換えてください。
仕組みの解説図
これでシステムは以下のようになりました。
- ユーザはスプレットシートにキーワードやURLを張り付けリストを作成
- リストを元にGASプログラムを実行する。
Gmail検索⇒本文取得⇒Gemini解析⇒Googleカレンダーに登録する - 定期実行を設定する(botとして使用する場合に設定)
さいごに
これで、「新しいキャンプ場の予約」から「自動化リストへの追加」まで、すべてがスマホ1つで完結するようになりました。
コード編集というハードルを取り払うことで、楽に 「使い続けられる自動化」 になったと思います。
皆さんも、スプレッドシート関数とGASを組み合わせて、自分だけの最強のキャンプ管理botを作ってみてください!
次回は…Gemを使用して進化できないかに挑戦します。
興味のある方は、ぜひ いいね や ストック をしてお待ちください!
