Skip to content

RSS リーダー ​

第4版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

バックグラウンドサーバースクリプトで外部の RSS / Atom フィードを定期取得し、items.Upsert で記事テーブルに自動登録すると、プリザンターを RSS リーダーとして使えます。 記事のリンク URL を Upsert のキーにするため、同じ記事が重複登録されることはありません。取り込んだ記事には、プリザンターのフィルタ・ソート・通知機能をそのまま使えます。

逆向き(プリザンターからフィードを配信する)は標準機能に無い

このページは外部のフィードを取り込む方法です。プリザンターのサイトの新着・更新を RSS / Atom として配信する機能は 1.5.8.1 にはありません。本体を改修して配信する場合の設計は RSS / Atom フィード配信の設計 にまとめています。

処理の流れ ​

図を読み込み中…

  1. 設定した各フィード URL に httpClient.Get() で XML を取得する
  2. XML から記事のタイトル・リンク・概要・公開日時を正規表現で抽出する
  3. リンク URL をキーに items.Upsert で記事テーブルに登録・更新する

前提条件 ​

Script.json ​

App_Data/Parameters/Script.json の BackgroundServerScript を true にします(既定値は false です。Script.json)。

json
{
    "ServerScript": true,
    "BackgroundServerScript": true
}

WARNING

パラメータファイルの変更後はプリザンターの再起動が必要です。パラメータ再読み込み機能を使う場合は、特権ユーザでログインして実行してください。

記事格納用テーブル ​

期限付きテーブルを作成し、サイト ID を控えておきます。

カラム用途説明
タイトル記事タイトルフィードの <title>
内容記事概要フィードの <description> や <summary>
分類Aリンク URLUpsert のキーとして使用
分類Bフィード名どのフィードから取得したかを識別
日付A(DateA)公開日時フィードの <pubDate> や <published>。スクリプトではリクエストの最上位に DateA として渡す

分類 C 以降を使っても構いません。

XML のパース ​

サーバースクリプトは JavaScript(V8 / ClearScript)で動作しますが、DOMParser のようなブラウザ API は使えません。そのため XML は正規表現でパースします。RSS と Atom はタグ名が異なるため、両方に対応します。

関数役割
getTagValueタグの中身を取得する。CDATA セクション(<![CDATA[...]]>)にも対応
getHrefAtom の <link href="..."> から URL を取得する
parseEntries<feed タグが含まれていれば Atom、それ以外は RSS 2.0 として記事の配列を返す

WARNING

この正規表現ベースのパーサは一般的な RSS 2.0 / Atom フィードに対応しますが、名前空間付きタグ(例: <dc:creator>)や複雑な構造には対応していません。対象フィードに合わせてカスタマイズしてください。

バックグラウンドサーバースクリプト ​

登録手順 ​

  1. 特権ユーザでログインする
  2. 「テナント管理」→「サーバスクリプト」タブを開く
  3. 「新規作成」をクリックする
  4. 次を設定して「追加」→「更新」
項目設定値
タイトルRSSフィード取得
条件バックグラウンドサーバスクリプト
スケジュール毎時(任意)
無効チェックなし
サーバスクリプト下記

スクリプト ​

js
// --- 設定 ---
var targetSiteId = 12345; // 記事格納テーブルのサイトID
var feeds = [
    { name: 'Qiita - Pleasanter', url: 'https://qiita.com/tags/pleasanter/feed' },
    { name: 'Qiita - プリザンター', url: 'https://qiita.com/tags/プリザンター/feed' }
];
// --- 設定ここまで ---

feeds.forEach(function (feed) {
    try {
        // フィードを取得
        httpClient.RequestUri = feed.url;
        var response = httpClient.Get();

        if (!httpClient.IsSuccess) {
            logs.LogInfo('フィード取得に失敗: ' + feed.name + ' (' + feed.url + ')');
            return;
        }

        // XMLをパースして記事を抽出
        var entries = parseEntries(response);

        if (entries.length === 0) {
            logs.LogInfo('記事なし: ' + feed.name);
            return;
        }

        var upsertCount = 0;

        entries.forEach(function (entry) {
            try {
                if (!entry.link) return;

                var data = {
                    Keys: ['ClassA'],
                    Title: entry.title || '(無題)',
                    Body: entry.summary || '',
                    ClassA: entry.link,
                    ClassB: feed.name
                };

                // 公開日時をセット
                if (entry.pubDate) {
                    var d = new Date(entry.pubDate);
                    if (!isNaN(d.getTime())) {
                        data.DateA = d.toISOString();
                    }
                }

                var result = items.Upsert(targetSiteId, JSON.stringify(data));

                if (result) {
                    upsertCount++;
                }
            } catch (entryError) {
                logs.LogInfo('エントリ処理エラー: ' + entry.link + ' - ' + entryError.message);
            }
        });

        logs.LogInfo(
            'フィード処理完了: ' + feed.name
            + ', 取得件数=' + entries.length
            + ', Upsert件数=' + upsertCount
        );
    } catch (feedError) {
        logs.LogInfo('フィード処理エラー: ' + feed.name + ' - ' + feedError.message);
    }
});

// --- パース関数 ---

// XMLテキストからタグの値を取得する
function getTagValue(xml, tagName) {
    var pattern = new RegExp(
        '<' + tagName + '[^>]*>([\\s\\S]*?)</' + tagName + '>'
    );
    var match = xml.match(pattern);
    return match ? match[1].replace(/<!\[CDATA\[|\]\]>/g, '').trim() : '';
}

// XMLテキストからhref属性を取得する(Atom用)
function getHref(xml, tagName) {
    var pattern = new RegExp('<' + tagName + '[^>]*href="([^"]*)"');
    var match = xml.match(pattern);
    return match ? match[1] : '';
}

// RSS/Atom XMLをパースして記事の配列を返す
function parseEntries(xml) {
    var entries = [];

    if (xml.indexOf('<feed') !== -1) {
        // Atom フィード
        var entryBlocks = xml.split(/<entry[\s>]/);
        for (var i = 1; i < entryBlocks.length; i++) {
            var block = entryBlocks[i];
            entries.push({
                title: getTagValue(block, 'title'),
                link: getHref(block, 'link'),
                summary: getTagValue(block, 'summary')
                    || getTagValue(block, 'content'),
                pubDate: getTagValue(block, 'published')
                    || getTagValue(block, 'updated')
            });
        }
    } else {
        // RSS 2.0 フィード
        var itemBlocks = xml.split(/<item[\s>]/);
        for (var j = 1; j < itemBlocks.length; j++) {
            var itemBlock = itemBlocks[j];
            entries.push({
                title: getTagValue(itemBlock, 'title'),
                link: getTagValue(itemBlock, 'link'),
                summary: getTagValue(itemBlock, 'description'),
                pubDate: getTagValue(itemBlock, 'pubDate')
            });
        }
    }

    return entries;
}
設定項目説明
targetSiteId記事を格納するテーブルのサイト ID
feeds取得するフィードの配列。name はフィード名(分類B に格納)、url はフィードの URL

Keys: ['ClassA'] でリンク URL(分類A)をキーに指定しているため、同じ URL の記事は更新、新しい URL の記事は新規作成されます。

公開日時は DateHash を使わず、最上位の DateA に入れています。API のモデルは ClassA や DateA を最上位のプロパティとしても受け付けます(_BaseApiModel.cs)。

httpClient.ResponseHeaders は Get() などの呼び出しごとに内部でクリアされる(ServerScriptModelHttpClient.cs)ため、事前に Clear() する必要はありません。

フィード取得の成否や Upsert 件数は logs.LogInfo で SysLogs に記録します(ServerScriptModelLogs.cs)。context.Log の出力は画面へ返す応答(ブラウザのコンソール)に載るだけなので、画面を持たないバックグラウンドサーバースクリプトでは残らないため、logs.LogInfo に修正しています。

フィードの追加 ​

feeds 配列にオブジェクトを追加するだけです。

js
var feeds = [
    { name: 'Qiita - Pleasanter', url: 'https://qiita.com/tags/pleasanter/feed' },
    { name: 'Qiita - プリザンター', url: 'https://qiita.com/tags/プリザンター/feed' },
    { name: 'Zenn - Pleasanter', url: 'https://zenn.dev/topics/pleasanter/feed' }
];

新着記事の通知 ​

items.Upsert でレコードが作成・更新されると、テーブルに設定された通知が発動します。記事格納テーブルの「テーブルの管理」→「通知」タブで通知を追加するだけで、新着記事をメールやチャットで受け取れます。

項目設定値
通知種別メール / HttpClient など
条件作成後
アドレス通知先のメールアドレスや Webhook URL

「作成後」を条件にすると、新しい記事が初めて登録されたときだけ通知されます。同じ記事が更新された場合も通知したい場合は「更新後」も追加します。

カスタマイズ例 ​

HTML タグの除去 ​

概要に HTML タグが含まれる場合は、除去してから Body に格納します。

js
function stripHtml(html) {
    return html.replace(/<[^>]*>/g, '');
}

data.Body = stripHtml(entry.summary || '');

古い記事の自動削除 ​

同じバックグラウンドサーバースクリプト内で、items.BulkDelete を使って一定期間(例: 90 日)より古いレコードをまとめて削除できます。

js
// --- 古い記事の削除(90日以上前) ---
var maxDays = 90;
var cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - maxDays);
var cutoff = cutoffDate.getFullYear() + '/'
    + (cutoffDate.getMonth() + 1) + '/'
    + cutoffDate.getDate();

// 日付の範囲は「開始,終了」。開始を空にすると「終了以前」になる
var deleted = items.BulkDelete(targetSiteId, JSON.stringify({
    View: {
        ColumnFilterHash: {
            DateA: '[",' + cutoff + '"]'
        }
    }
}));

if (deleted > 0) {
    logs.LogInfo('古い記事を削除: ' + deleted + '件');
}

日付項目のフィルタは 開始,終了 の形式で、終了を空にすると「開始以後」、開始を空にすると「終了以前」になります(View.cs)。'["' + 日付 + ',"]' と書くと基準日より新しい記事が対象になるため、開始を空にする形に修正しています。

また、httpClient から context.ApplicationPath + 'api/...' を呼ぶ方法は、ApplicationPath がホスト名を含まないパスのため URL として成り立ちません。サーバースクリプトの items.BulkDelete(サイトID, JSON)(ServerScriptModelApiItems.cs)を使えば API キーも不要なので、こちらに修正しています。

WARNING

items.BulkDelete は、バックグラウンドサーバースクリプトの実行ユーザーの権限で動きます。実行ユーザーが記事テーブルの削除権限を持っている必要があります。

関連ページ ​

変更履歴

第4版外部連携の改修・設計メモ(iCal・RSS/Atom・Webhook 送受信・iPaaS・短縮 URL・POP 受信・マスターデータ同期)を追加
第3版「外部連携・AI」を 1.5.8.1 のソースで検証して修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「外部連携・AI」セクションの記事を追加