🦆

【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 回のトリガー設定

  1. スクリプトエディタ画面上部 ⏰ Triggers+ Add Trigger
  2. 設定値を以下のように選択
    項目 設定
    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)
  3. 保存 → 認可ダイアログで 続行 → アカウント選択 → 許可

4. AppSheet 側での利用ヒント(拡張ポイント)

  • db シートをテーブルとして読み込み、
    • Slice[timestamp] > (TODAY() - 7) で直近 1 週間分だけ表示
    • Detail Viewcontent を LongText コンポーネントに
  • 新着通知を Slack へ送りたい場合は、上記 GAS に Webhook or Chat.PostMessage 追加可能。

完了!

これで毎日自動で RSS が収集・蓄積され、過去分との重複も残りません。
必要に応じてログ監視や通知を追加し、メンテナンスしてください。

所感

  • 明日は始発移動、最終の飛行機戻りなので確認は明後日にしよう
GitHubで編集を提案

Discussion