これは何ですか?

GASのスクリプトでがす。指定したホームページが更新されたら「更新されたよ」って教えてくれるし、指定キーワードがあった場合はその部分も通知してくれます。複数サイトのチェック可、複数ワードにも対応。メール以外にDiscordにも送れます。
作ってみましょう
1.GoogleDriveを開く

2.空白のスプレッドシートを作る

3.拡張機能→AppsScript

4.設定→プロジェクトの設定

5.スクリプトプロパティに追加

プロパティ「EMAIL_RECIPIENTS」に通知先メールアドレス 複数アドレスに送信する場合はカンマ区切り
プロパティ「FROM_EMAIL_NAME」にメール送信者名
プロパティ「DISCORD_WEBHOOK_URL」に通知を送りたいDiscordチャンネルのWebhook URL 不要な場合は空白
6.エディタ→コード.gs

下記の内容をコピペする
// --- 設定 ---
const SHEET_NAME = '設定'; // スプレッドシートのシート名
const URL_COLUMN = 1; // URLが入力されている列番号 (A列 = 1)
const KEYWORD_COLUMN = 2; // キーワードが入力されている列番号 (B列 = 2)
const ENABLED_COLUMN = 3; // 有効フラグが入力されている列番号 (C列 = 3)
const VENUE_NAME_COLUMN = 4; // 会場名が入力されている列番号 (D列 = 4)
const ENABLED_VALUE = 'TRUE'; // 有効を示す値(大文字小文字区別なし)
const SNIPPET_LENGTH = 150; // キーワード周辺テキストの取得文字数(前後それぞれ)
const DEBUG_LOG_CONTENT = false; // ★ trueにすると取得したコンテンツの一部をログに出力(デバッグ用、実行後はfalseに戻す)
// --- 設定ここまで ---
// --- グローバル変数 ---
const SCRIPT_PROPERTIES = PropertiesService.getScriptProperties();
const EMAIL_RECIPIENTS = SCRIPT_PROPERTIES.getProperty('EMAIL_RECIPIENTS');
const DISCORD_WEBHOOK_URL = SCRIPT_PROPERTIES.getProperty('DISCORD_WEBHOOK_URL');
const FROM_EMAIL_NAME = SCRIPT_PROPERTIES.getProperty('FROM_EMAIL_NAME') || 'Website Update Checker';
const PREVIOUS_HASHES_KEY = 'previousHashes';
// --- グローバル変数ここまで ---
/**
* メインの処理関数:トリガーから呼び出される
*/
function checkWebsites() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
Logger.log(`エラー: シート "${SHEET_NAME}" が見つかりません。`);
return;
}
const dataRange = sheet.getDataRange();
const values = dataRange.getValues();
const previousHashes = loadPreviousHashes();
const currentHashes = {};
let updatedPages = [];
// キーワード情報はURLごとに集約 { url: { venueName: "会場名", keywords: ["KW1"], snippets: {"KW1": "周辺テキスト1"} } }
let keywordFoundDetails = {};
let errorPages = [];
// ヘッダー行を除外してループ
for (let i = 1; i < values.length; i++) {
const row = values[i];
const isEnabled = String(row[ENABLED_COLUMN - 1]).toUpperCase() === ENABLED_VALUE.toUpperCase();
const url = row[URL_COLUMN - 1];
const rawKeywords = row[KEYWORD_COLUMN - 1] ? String(row[KEYWORD_COLUMN - 1]).split(',').map(kw => kw.trim()).filter(kw => kw !== '') : [];
const venueName = row[VENUE_NAME_COLUMN - 1] || '';
if (!isEnabled || !url) {
continue; // 無効またはURLが空ならスキップ
}
Logger.log(`チェック中: ${venueName ? venueName + ' (' + url + ')' : url}`);
let currentContent = '';
let currentHash = '';
let errorMessage = null;
let pageUpdated = false;
try {
// リダイレクトを追従し、HTTPエラー時も例外を投げないようにする
const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true, followRedirects: true });
const responseCode = response.getResponseCode();
if (responseCode === 200) {
// 文字コード判定とコンテンツ取得
let charset = 'UTF-8';
const contentType = response.getHeaders()['Content-Type'];
if (contentType) {
const match = contentType.match(/charset=([^;]+)/i);
if (match && match[1] && match[1].toUpperCase() !== 'UTF-8') {
try {
currentContent = response.getContentText(match[1]);
charset = match[1];
Logger.log(` 文字コード ${charset} で取得しました。 (${url})`);
} catch (e) {
Logger.log(` 文字コード ${match[1]} での取得に失敗。UTF-8で試行します。(${url}) Error: ${e.message}`);
currentContent = response.getContentText('UTF-8');
}
} else {
currentContent = response.getContentText('UTF-8');
}
} else {
currentContent = response.getContentText('UTF-8');
}
if (DEBUG_LOG_CONTENT) {
Logger.log(` 取得コンテンツ (${url}) の先頭500文字:\n${currentContent.substring(0, 500)}`);
}
// ハッシュ計算用の正規化 (HTMLコメント、script, style, タグ除去、空白正規化)
const normalizedContent = currentContent
.replace(/<!--.*?-->/gs, '') // HTMLコメント削除
.replace(/<script[^>]*>.*?<\/script>/gis, '') // scriptタグ削除
.replace(/<style[^>]*>.*?<\/style>/gis, '') // styleタグ削除
.replace(/<[^>]+>/g, ' ') // 残りのHTMLタグをスペースに置換
.replace(/\s+/g, ' ') // 連続する空白を1つに
.trim();
currentHash = calculateHash(normalizedContent);
currentHashes[url] = currentHash;
// --- 更新チェック ---
const previousHash = previousHashes[url];
if (!previousHash || currentHash !== previousHash) {
Logger.log(` 更新検知: ${url}`);
updatedPages.push({ url: url, venueName: venueName });
pageUpdated = true;
} else {
Logger.log(` 変更なし: ${url}`);
currentHashes[url] = currentHash; // 変更なくても現在のハッシュを引き継ぐ
}
// --- キーワードチェック (更新有無に関わらず実行) ---
if (rawKeywords.length > 0 && currentContent) {
// 検索対象テキスト(HTMLタグ除去済み)
const textForSearch = normalizedContent;
const textForSearchLower = textForSearch.toLowerCase();
if (DEBUG_LOG_CONTENT) {
Logger.log(` キーワード検索対象テキスト(正規化後 ${url})の先頭300文字:\n${textForSearch.substring(0, 300)}`);
}
for (const keyword of rawKeywords) {
const keywordLower = keyword.toLowerCase();
let index = -1;
let foundInRawContent = false; // 元のHTMLで見つかったかフラグ
// まず正規化テキストで検索
index = textForSearchLower.indexOf(keywordLower);
// 見つからなければ元のHTMLでも検索(デバッグ・代替用)
if (index === -1) {
const rawContentLower = currentContent.toLowerCase();
let rawIndex = rawContentLower.indexOf(keywordLower);
if (rawIndex !== -1) {
index = rawIndex;
foundInRawContent = true;
Logger.log(` キーワード "${keyword}" は正規化テキストで見つからず、元のHTML内で発見 (${url})`);
}
}
if (index !== -1) {
// このURLで初めてキーワードが見つかった場合、初期化
if (!keywordFoundDetails[url]) {
keywordFoundDetails[url] = { venueName: venueName, keywords: [], snippets: {} };
}
// まだこのキーワードが追加されていなければ追加
if (!keywordFoundDetails[url].keywords.includes(keyword)) {
keywordFoundDetails[url].keywords.push(keyword);
Logger.log(` キーワード "${keyword}" を発見 (${url})`);
// 周辺テキストを取得 (見つかった方のテキストソースから)
const sourceText = foundInRawContent ? currentContent : textForSearch;
const startIndex = Math.max(0, index - SNIPPET_LENGTH);
const endIndex = Math.min(sourceText.length, index + keywordLower.length + SNIPPET_LENGTH);
let snippet = sourceText.substring(startIndex, endIndex);
// 前後の省略記号を追加
if (startIndex > 0) snippet = "..." + snippet;
if (endIndex < sourceText.length) snippet = snippet + "...";
// 余分な空白を整理して保存
keywordFoundDetails[url].snippets[keyword] = snippet.replace(/\s+/g, ' ').trim();
}
} else {
// DEBUGログが有効なら、見つからなかったキーワードも記録
if (DEBUG_LOG_CONTENT) {
Logger.log(` キーワード "${keyword}" は見つかりませんでした (${url})`);
}
}
} // keyword loop end
} // keyword check end
} else {
// --- アクセスエラー処理 ---
errorMessage = `アクセスエラー: ステータスコード ${responseCode} (${url})`;
Logger.log(errorMessage);
errorPages.push({ url: url, venueName: venueName, error: `ステータスコード ${responseCode}` });
if (previousHashes[url]) {
currentHashes[url] = previousHashes[url]; // エラー時は前回のハッシュを維持
}
continue;
}
} catch (e) {
// --- 取得エラー処理 (ネットワークエラーなど) ---
errorMessage = `取得エラー: ${e.message} (${url})`;
Logger.log(errorMessage);
// エラーメッセージにスタックトレースを含めるとデバッグに役立つ場合がある
Logger.log(`Stack trace: ${e.stack}`);
errorPages.push({ url: url, venueName: venueName, error: e.message });
if (previousHashes[url]) {
currentHashes[url] = previousHashes[url]; // エラー時は前回のハッシュを維持
}
continue;
}
} // End of URL loop
// --- 新しいハッシュを保存 ---
saveCurrentHashes(currentHashes);
// --- 通知判定と送信 ---
if (updatedPages.length > 0 || Object.keys(keywordFoundDetails).length > 0 || errorPages.length > 0) {
sendNotifications(updatedPages, keywordFoundDetails, errorPages);
} else {
Logger.log("更新されたページ、キーワードが見つかったページ、エラーはありませんでした。通知は送信しません。");
}
Logger.log("チェック完了");
}
/**
* 文字列のMD5ハッシュを計算してBase64エンコードで返す
*/
function calculateHash(text) {
const digest = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, text, Utilities.Charset.UTF_8);
return Utilities.base64Encode(digest);
}
/**
* スクリプトプロパティから前回のハッシュ値を読み込む
*/
function loadPreviousHashes() {
const hashesJson = SCRIPT_PROPERTIES.getProperty(PREVIOUS_HASHES_KEY);
if (hashesJson) {
try {
return JSON.parse(hashesJson);
} catch (e) {
Logger.log(`前回のハッシュ値の読み込みに失敗しました: ${e}`);
return {};
}
}
return {};
}
/**
* スクリプトプロパティに現在のハッシュ値を保存する
*/
function saveCurrentHashes(hashes) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
Logger.log(`ハッシュ保存スキップ: シート "${SHEET_NAME}" が見つかりません。`);
return;
}
const lastRow = sheet.getLastRow();
let currentUrls = [];
// ヘッダー行を除いてURLを取得
if (lastRow > 1) {
currentUrls = sheet.getRange(2, URL_COLUMN, lastRow - 1, 1)
.getValues()
.flat() // 2次元配列を1次元に
.filter(url => url); // 空のURLを除外
}
const currentUrlSet = new Set(currentUrls);
const cleanedHashes = {};
// 現在シートに存在するURLのハッシュのみを保持(古いデータを削除)
for (const url in hashes) {
if (currentUrlSet.has(url)) {
cleanedHashes[url] = hashes[url];
} else {
// Logger.log(`削除されたURLのハッシュをクリーンアップ: ${url}`); // 必要ならログ出力
}
}
const hashesJson = JSON.stringify(cleanedHashes);
// サイズチェック (Script Propertiesの上限は約500KB、値は9KB、合計500KB)
// 念のため一つの値の上限(9KB)も考慮するが、複数URLの場合合計サイズが問題になる
if (hashesJson.length > 480000) { // 500KBに近づいたら警告・削除
Logger.log(`警告: 保存するハッシュデータサイズ(${hashesJson.length} bytes)が上限に近いため、古いデータを削除します。`);
SCRIPT_PROPERTIES.deleteProperty(PREVIOUS_HASHES_KEY); // 古いデータを全削除
// 再度保存を試みる(サイズ超過でなければ)
if (hashesJson.length < 500000) {
SCRIPT_PROPERTIES.setProperty(PREVIOUS_HASHES_KEY, hashesJson);
Logger.log('古いハッシュ削除後、再保存しました。');
} else {
Logger.log('エラー: クリーンアップ後もハッシュデータが大きすぎます。保存できません。');
// ここで別途エラー通知を行うことも検討
}
} else {
SCRIPT_PROPERTIES.setProperty(PREVIOUS_HASHES_KEY, hashesJson);
}
} catch (e) {
Logger.log(`ハッシュ値の保存中にエラーが発生しました: ${e}.`);
// 保存エラー時も古いデータを削除してリトライするロジックは維持
try {
Logger.log('ハッシュ保存エラーのため、古いデータを削除して再保存を試みます。');
SCRIPT_PROPERTIES.deleteProperty(PREVIOUS_HASHES_KEY);
// cleanedHashes は try ブロック内で定義されているため、再計算か、より広いスコープで定義が必要
// ここでは簡易的に hashes (引数) を使うが、cleanedHashesを使うのがより正確
const hashesJsonRetry = JSON.stringify(hashes); // 削除済みURL含む可能性あり
if (hashesJsonRetry.length < 500000) {
SCRIPT_PROPERTIES.setProperty(PREVIOUS_HASHES_KEY, hashesJsonRetry);
Logger.log('古いハッシュを削除して再保存を試みました。');
} else {
Logger.log('エラー: リトライ時もハッシュデータが大きすぎます。保存できません。');
}
} catch (e2) {
Logger.log(`ハッシュの再保存にも失敗しました: ${e2}`);
}
}
}
/**
* 通知を送信する
*/
function sendNotifications(updatedPages, keywordFoundDetails, errorPages) {
const subject = `【Web更新通知】${new Date().toLocaleString('ja-JP')}`;
let emailBody = `<html><body style="font-family: sans-serif;"><h2>Webサイト更新チェック結果 (${new Date().toLocaleString('ja-JP')})</h2>`;
let discordBaseContent = `**Webサイト更新チェック結果 (${new Date().toLocaleString('ja-JP')})**\n\n`; // Discord分割送信用
// --- 更新されたページ ---
let updateSectionEmail = "";
let updateSectionDiscord = "";
if (updatedPages.length > 0) {
updateSectionEmail += "<h3 style=\"color: #0056b3;\">■ 更新が検知されたページ</h3><ul>";
updateSectionDiscord += "**■ 更新が検知されたページ**\n";
updatedPages.forEach(info => {
const displayName = info.venueName ? `${info.venueName} (${info.url})` : info.url;
const displayLink = info.venueName ? `<a href="${info.url}" target="_blank" rel="noopener noreferrer">${info.venueName}</a>` : `<a href="${info.url}" target="_blank" rel="noopener noreferrer">${info.url}</a>`;
updateSectionEmail += `<li style="margin-bottom: 5px;">${displayLink}${info.venueName ? ` (<a href="${info.url}" target="_blank" rel="noopener noreferrer" style="color:#555; text-decoration:none;">${info.url}</a>)`: ''}</li>`;
updateSectionDiscord += `- ${displayName}\n`;
});
updateSectionEmail += "</ul>";
updateSectionDiscord += "\n";
}
// --- キーワードが見つかったページ ---
let keywordSectionEmail = "";
let keywordSectionDiscord = "";
const keywordUrls = Object.keys(keywordFoundDetails);
if (keywordUrls.length > 0) {
keywordSectionEmail += "<h3 style=\"color: #d9534f;\">■ 指定キーワードが発見されたページ</h3>";
keywordSectionDiscord += "**■ 指定キーワードが発見されたページ**\n";
for (const url of keywordUrls) {
const details = keywordFoundDetails[url];
const displayName = details.venueName ? `${details.venueName}` : url;
const displayLink = `<a href="${url}" target="_blank" rel="noopener noreferrer">${displayName}</a>`;
keywordSectionEmail += `<div style="margin-bottom: 15px; border-left: 3px solid #d9534f; padding-left: 10px;">`;
keywordSectionEmail += `<p style="margin-bottom: 5px;"><b>会場/URL:</b> ${displayLink}${details.venueName ? ` (<a href="${url}" target="_blank" rel="noopener noreferrer" style="color:#555; text-decoration:none;">${url}</a>)`: ''}</p>`;
keywordSectionEmail += `<p style="margin-top: 0; margin-bottom: 8px;"><b>発見されたキーワード:</b> ${details.keywords.map(kw => escapeHtml(kw)).join(', ')}</p>`;
keywordSectionEmail += `<div style="font-size: 0.9em;">`; // 周辺テキストのコンテナ
keywordSectionDiscord += `* **会場/URL:** ${details.venueName ? details.venueName + ' ('+ url + ')' : url}\n`;
keywordSectionDiscord += `* **発見されたキーワード:** ${details.keywords.join(', ')}\n`; // Discordはエスケープ不要
for (const keyword of details.keywords) {
const snippet = details.snippets[keyword] || '(周辺テキスト取得失敗)';
// HTML用にエスケープし、キーワードを太字にする試み(単純置換)
const escapedSnippet = escapeHtml(snippet);
// 大文字小文字区別せずにキーワードを太字にする正規表現
const keywordRegex = new RegExp(`(${escapeHtml(keyword).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
const highlightedSnippet = escapedSnippet.replace(keywordRegex, '<b>$1</b>');
keywordSectionEmail += `<div style="margin-left: 15px; margin-bottom: 8px; border: 1px solid #eee; padding: 8px; background-color:#fdfdfd;">`;
keywordSectionEmail += `<div><b>"${escapeHtml(keyword)}":</b></div>`;
keywordSectionEmail += `<pre style="white-space: pre-wrap; word-wrap: break-word; margin-top: 4px; margin-bottom: 0; font-size: 0.95em; color: #333;">${highlightedSnippet}</pre>`; // preタグで表示
keywordSectionEmail += `</div>`;
// Discord用に整形 (長すぎる場合は省略)
let discordSnippetText = `"${keyword}":\n${snippet}`; // 元のスニペットを使用
// 各スニペットの長さを制限(全体で2000文字超えないように、かつ個々も長くならないように)
const maxSnippetLength = Math.max(100, Math.floor(1800 / details.keywords.length) - keyword.length - 10);
if (discordSnippetText.length > maxSnippetLength) {
discordSnippetText = discordSnippetText.substring(0, maxSnippetLength) + "...(省略)";
}
keywordSectionDiscord += `\`\`\`\n${discordSnippetText}\n\`\`\`\n`;
}
keywordSectionEmail += `</div></div>`; // 周辺テキストコンテナとURLごとのdivを閉じる
keywordSectionDiscord += "\n";
}
}
// --- エラーが発生したページ ---
let errorSectionEmail = "";
let errorSectionDiscord = "";
if (errorPages.length > 0) {
errorSectionEmail += "<h3 style='color: #c9302c;'>■ エラーが発生したページ</h3><ul>";
errorSectionDiscord += "**■ エラーが発生したページ**\n";
errorPages.forEach(info => {
const displayName = info.venueName ? `${info.venueName} (${info.url})` : info.url;
const escapedDisplayName = escapeHtml(displayName);
const escapedError = escapeHtml(info.error);
errorSectionEmail += `<li style="margin-bottom: 5px;">${escapedDisplayName} - <span style="color:#c9302c;">${escapedError}</span></li>`;
errorSectionDiscord += `- ${displayName} - ${info.error}\n`; // Discordはエスケープ不要
});
errorSectionEmail += "</ul>";
errorSectionDiscord += "\n";
}
// --- メール本文の結合 ---
emailBody += updateSectionEmail + keywordSectionEmail + errorSectionEmail + "</body></html>";
// --- Discord本文の結合 ---
const discordContent = discordBaseContent + updateSectionDiscord + keywordSectionDiscord + errorSectionDiscord;
// --- メール送信 ---
if (EMAIL_RECIPIENTS && (updatedPages.length > 0 || keywordUrls.length > 0 || errorPages.length > 0)) {
try {
Logger.log(`メール送信先: ${EMAIL_RECIPIENTS}`);
MailApp.sendEmail({
to: EMAIL_RECIPIENTS,
subject: subject,
htmlBody: emailBody,
name: FROM_EMAIL_NAME
});
Logger.log("メール通知を送信しました。");
} catch (e) {
Logger.log(`メール送信エラー: ${e}`);
}
} else if (!EMAIL_RECIPIENTS) {
Logger.log("メール送信先が設定されていません。");
}
// --- Discord送信 (分割対応) ---
if (DISCORD_WEBHOOK_URL && (updatedPages.length > 0 || keywordUrls.length > 0 || errorPages.length > 0)) {
sendToDiscordWebhook(discordContent);
} else if (!DISCORD_WEBHOOK_URL) {
Logger.log("Discord Webhook URLが設定されていません。");
}
}
/**
* Discord Webhookにメッセージを送信する(2000文字超えで分割)
* @param {string} content 送信するメッセージ全体
*/
function sendToDiscordWebhook(content) {
const MAX_LENGTH = 2000;
const chunks = [];
let currentChunk = "";
// メッセージを行単位で処理し、チャンクを作成
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 現在のチャンクに次の行を追加しても上限を超えないかチェック
if (currentChunk.length + line.length + 1 <= MAX_LENGTH) {
currentChunk += line + "\n";
} else {
// 上限を超える場合、現在のチャンクを確定し、新しいチャンクを開始
// ただし、1行だけで上限を超える場合は、その行を強制的に分割する必要がある(ここでは単純に切り捨て)
if (line.length > MAX_LENGTH) {
Logger.log(`警告: 1行が長すぎるためDiscordメッセージを切り捨てます: ${line.substring(0, 100)}...`);
// 長すぎる行を分割するロジックを追加することも可能
if (currentChunk) chunks.push(currentChunk); // 前のチャンクがあれば追加
chunks.push(line.substring(0, MAX_LENGTH - 10) + "...(省略)\n"); // 切り捨てて追加
currentChunk = ""; // 新しいチャンクは空
} else {
chunks.push(currentChunk); // 現在のチャンクを追加
currentChunk = line + "\n"; // 新しいチャンクを開始
}
}
// 最後の行の場合、現在のチャンクを追加
if (i === lines.length - 1 && currentChunk) {
chunks.push(currentChunk);
}
}
Logger.log(`Discord通知を ${chunks.length} 個のチャンクに分割して送信します。`);
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i].trim(); // 前後の空白を削除
if (chunk === "") continue; // 空のチャンクはスキップ
const payload = JSON.stringify({ content: chunk });
const options = {
method: 'post',
contentType: 'application/json',
payload: payload,
muteHttpExceptions: true // Discordからのエラーでスクリプト全体を止めない
};
try {
const response = UrlFetchApp.fetch(DISCORD_WEBHOOK_URL, options);
const responseCode = response.getResponseCode();
Logger.log(`Discord通知送信結果 (Chunk ${i + 1}/${chunks.length}): ${responseCode}`);
if (responseCode >= 300) {
Logger.log(`Discord応答 (Chunk ${i + 1}): ${response.getContentText()}`);
// レート制限(429)の場合は少し長く待つ
if (responseCode === 429) {
Logger.log("Discordレート制限のため、5秒待機します...");
Utilities.sleep(5000);
}
} else {
Logger.log(`Discord通知 (Chunk ${i + 1}) を送信しました。`);
}
// 連続送信を避けるための短い待機 (レート制限対策)
if (chunks.length > 1 && i < chunks.length - 1) {
Utilities.sleep(1100); // 1.1秒待機
}
} catch (e) {
Logger.log(`Discord送信エラー (Chunk ${i + 1}): ${e}`);
// エラーが発生した場合も少し待機してから次へ
Utilities.sleep(1500);
}
} // end of chunk loop
}
/**
* HTMLエスケープを行うヘルパー関数
*/
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
//.replace(/'/g, ''');
// ★ preタグや```を使うので、改行の<br>変換は除去
}
/**
* 手動実行用(テスト目的)
*/
function manualCheckAndNotify() {
checkWebsites();
}
/**
* 古いハッシュデータを手動でクリアする関数(デバッグ用)
*/
function clearPreviousHashes() {
try {
SCRIPT_PROPERTIES.deleteProperty(PREVIOUS_HASHES_KEY);
Logger.log(`スクリプトプロパティ "${PREVIOUS_HASHES_KEY}" を削除しました。`);
} catch (e) {
Logger.log(`スクリプトプロパティ "${PREVIOUS_HASHES_KEY}" の削除中にエラーが発生しました: ${e}`);
}
}
7.プロジェクトに名前をつけて保存


8.スプレッドシートに戻り項目を追加する

・スプレッドシートに任意のタイトルをつける
・A列にチェックするURL *必須
・B列に検知するキーワード *任意 キーワードは複数指定可(キーワード1,キーワード2,キーワード3)
・C列に「TRUE」と記載されたらその行を有効とする *任意 それ以外(空白)の場合は無効となりチェックされない
・D列にはサイト名 *任意
・シートの名前を「設定」にする
9.スクリプトに戻りとりあえず実行を押す

10.プロジェクトを承認する

「権限を確認」

自分のアカウントを選ぶ

「詳細」を押す

「~~~(安全ではないページ)に移動」を押す

すべてを選択して続行
11.動作結果を確認する


前回チェック時から内容が変わったサイトを「更新が検知されたサイト」としてリストアップします。
また、指定したページに指定キーワードがある場合は、更新されていない場合でもリストアップします。
定期的に実行するようにする
13.トリガー

14.トリガーを追加

15.自動実行するタイミングをセット

・実行する関数 → checkWebsites
・実行するデプロイ → Head
・イベントのソースを選択 → 時間主導型
・時間ベースのトリガーのタイプを選択 → 日付ベースのタイマー
・時刻を選択 → 午後7時~8時
・エラー通知設定 → 1週間おきに通知を受け取る
これで、毎日午後7~8時の間にスクリプトが自動実行されます。
チェック内容を初期化する
16.「clearPreviousHashes」を実行する
