🦆
【GAS】RSSFeedの情報蓄積
なぜ作成したのか
- 現在Powershellで実装している機能をGASに移管しておきたい
質問(to ChatGPT)
指定のRSSFeedの情報を毎日収集し、蓄積するデータベースをGASで作成します
技術スタックは次の通りです。
- GAS
- GoogleSpreadsheet
- Appsheet
スプレッドシートは以下のシート構成としてください
シート「urls」
列1:一意の識別ID
列2:RSSFeedのURL
列3:URLの識別名称
シート「db」
列1:取得もとになったRSSFeedurlの識別ID
列2:記事タイトル
列3:記事のタイムスタンプ
列4:記事のリンク先URL
列5:記事の内容処理はGASで構築し、毎日1回トリガーで実行します。
処理はurlsのレコードごとに情報を取得し、dbシートに情報を登録します。
dbシートが存在しない場合は新規に作成し、1行目はカラム名をセットします。
すべての処理が完了したら前日までに取得していた行データとの重複を排除します。ここまでの処理を実装してください。
回答
以下の手順とスクリプトを そのまま Google Apps Script(GAS)に貼り付ければ、ご要望どおりに毎日 RSS を収集・重複排除する仕組みが完成します。
(処理概要 ▶ コード ▶ トリガー設定 ▶ 拡張ポイント の順で説明します)
1. 処理概要
ステップ | 内容 |
---|---|
① 初期化 | 既定のスプレッドシートを開き、urls シートを必須、db シートをなければ自動生成(ヘッダー行付き) |
② RSS 取得 |
urls シートの各行(ID・URL・名称)を走査し、Feed 全項目を取得 |
③ 行バッファリング | 新記事を [FeedID, Title, PublishedDate(ISO), Link, Content] 形式で配列に格納 |
④ 書き込み | バッファを db 末尾に一括 append |
⑤ 重複排除 | ①で読み取った既存行 + 今回追加行 をキー (FeedID + Link) で重複除去してシートを置換 |
⑥ 終了ログ | 実行結果(追加件数・重複削除件数など)を Stackdriver ログに出力 |
2. GAS スクリプト
/**
* メインエントリ—: 毎日 1 回トリガー実行
*/
function collectRssFeeds() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const urlsSheet = ss.getSheetByName('urls');
if (!urlsSheet) throw new Error('シート "urls" がありません');
// === db シート生成 & ヘッダー付与 ==========================
let dbSheet = ss.getSheetByName('db');
if (!dbSheet) {
dbSheet = ss.insertSheet('db');
dbSheet.appendRow(['feed_id', 'title', 'timestamp', 'link', 'content']);
}
// === 既存データ読み込み(重複判定用) ======================
const existing = dbSheet.getDataRange().getValues();
const header = existing.shift(); // 1行目ヘッダーを除外
const existingSet = new Set(
existing.map(r => `${r[0]}|${r[3]}`) // "feedId|link" をキーに
);
// === urls シートを走査して RSS 収集 =========================
const urlRows = urlsSheet.getDataRange().getValues();
urlRows.shift(); // ヘッダー想定で 1 行削除
const newRows = [];
urlRows.forEach(([feedId, feedUrl]) => {
if (!feedUrl) return;
try {
const feed = UrlFetchApp.fetch(feedUrl, {muteHttpExceptions: true}).getContentText();
const parsed = XmlService.parse(feed);
const root = parsed.getRootElement().getName(); // rss or feed(atom)
if (root === 'rss') { // RSS 2.x
parsed.getRootElement()
.getChild('channel')
.getChildren('item')
.forEach(item => newRows.push(convertRssItem(item, feedId)));
} else if (root === 'feed') { // Atom
parsed.getRootElement()
.getChildren('entry')
.forEach(entry => newRows.push(convertAtomEntry(entry, feedId)));
}
} catch (e) {
console.error(`Feed Fetch Error (${feedUrl}): ${e}`);
}
});
// === 追加予定行から既存重複を除外 ===========================
const rowsToAppend = newRows.filter(row => !existingSet.has(`${row[0]}|${row[3]}`));
if (rowsToAppend.length) dbSheet.getRange(dbSheet.getLastRow()+1, 1, rowsToAppend.length, rowsToAppend[0].length)
.setValues(rowsToAppend);
// === シート全体を再取得し、完全重複を排除 ==================
const allRows = dbSheet.getDataRange().getValues();
const uniqueMap = new Map(); // preserve latest occurrence
allRows.forEach((r,i)=>{ if(i===0) return; uniqueMap.set(`${r[0]}|${r[3]}`, r); });
const deduped = [header, ...Array.from(uniqueMap.values())];
dbSheet.clearContents();
dbSheet.getRange(1,1,deduped.length,deduped[0].length).setValues(deduped);
// === ログ ================================================
console.log(`Added ${rowsToAppend.length} new rows, deduped to ${deduped.length-1} total.`);
}
/* ---- RSS 2.0 item -> row --------------------------------- */
function convertRssItem(item, feedId) {
const get = t => item.getChildText(t) || '';
return [
feedId,
get('title'),
Utilities.formatDate(new Date(get('pubDate')), Session.getScriptTimeZone(), 'yyyy-MM-dd\'T\'HH:mm:ssXXX'),
get('link'),
get('description')
];
}
/* ---- Atom entry -> row ----------------------------------- */
function convertAtomEntry(entry, feedId) {
const ns = entry.getNamespace();
const get = (t, attr) => {
const el = entry.getChild(t, ns);
return el ? (attr ? el.getAttribute(attr).getValue() : el.getText()) : '';
};
return [
feedId,
get('title'),
Utilities.formatDate(new Date(get('updated') || get('published')), Session.getScriptTimeZone(), 'yyyy-MM-dd\'T\'HH:mm:ssXXX'),
get('link', 'href'),
get('content')
];
}
ポイント
- 重複判定のキーを「
feed_id
+link
」に固定。別の粒度が必要な場合はexistingSet
生成部を書き換えてください。- Atom フィードで
content
が長大な場合はサイズ制限(50 MB / 50k cells)を超えないよう注意。必要ならcontent.substring(0, 30000)
などで切り詰めます。
3. 毎日 1 回のトリガー設定
- スクリプトエディタ画面上部 ⏰ Triggers → + Add Trigger
- 設定値を以下のように選択
項目 設定 Which function to run collectRssFeeds
Deployment Head Event source Time-driven Type of time based trigger Day timer Time of day (実行したい時刻を選択。例 02:00–03:00 AM JST) - 保存 → 認可ダイアログで 続行 → アカウント選択 → 許可
4. AppSheet 側での利用ヒント(拡張ポイント)
-
db
シートをテーブルとして読み込み、-
Slice:
[timestamp] > (TODAY() - 7)
で直近 1 週間分だけ表示 -
Detail View:
content
を LongText コンポーネントに
-
Slice:
- 新着通知を Slack へ送りたい場合は、上記 GAS に Webhook or Chat.PostMessage 追加可能。
完了!
これで毎日自動で RSS が収集・蓄積され、過去分との重複も残りません。
必要に応じてログ監視や通知を追加し、メンテナンスしてください。
所感
- 明日は始発移動、最終の飛行機戻りなので確認は明後日にしよう
Discussion