0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

RSSやAtomフィードは、ニュースサイトやブログの更新情報を配信する標準的な仕組みです。専用のRSSリーダーを使うのが一般的ですが、普段使っているプリザンターでフィード情報を管理できれば、既存のフィルタ・ソート・通知機能をそのまま活用できて便利です。

今回は、バックグラウンドサーバスクリプトで外部のRSS/Atomフィードを定期取得し、items.Upsertで記事テーブルにデータを自動登録する仕組みを作ってみます。

処理の全体像

バックグラウンドサーバスクリプトがスケジュール実行されると、設定した各フィードURLに対してHTTP GETでXMLを取得します。取得したXMLから記事の情報(タイトル・リンク・概要・公開日時)を正規表現で抽出し、items.UpsertでプリザンターのテーブルにUpsertします。リンクURLをキーにしているため、同じ記事が重複登録されることはありません。

前提条件

Script.json

バックグラウンドサーバスクリプトを有効にするために、App_Data/Parameters/Script.jsonBackgroundServerScripttrueに設定します。

App_Data/Parameters/Script.json
{
    "ServerScript": true,
    "BackgroundServerScript": true
}

記事格納用テーブルの作成

取得した記事を格納する期限付きテーブルを作成します。以下のカラム構成にします。

カラム 用途 説明
タイトル 記事タイトル フィードの<title>を格納
内容 記事概要 フィードの<description><summary>を格納
分類A リンクURL 記事のURLを格納。Upsertのキーとして使用
分類B フィード名 どのフィードから取得したかを識別
期限付き 公開日時 フィードの<pubDate><published>を格納

カラムの種類は環境に合わせて変更してください。ここでは分類A・分類Bを使用していますが、分類C以降でも構いません。

テーブルを作成したら、サイトIDを控えておきます。後述のスクリプトに設定します。

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

XMLパース関数

プリザンターのサーバスクリプトはJavaScript(V8 / ClearScript)エンジンで動作しますが、DOMParserのようなブラウザAPIは使用できません。そのため、XMLのパースには正規表現を使います。

RSSとAtomではタグ名が異なるため、両方に対応するパース関数を用意します。

/**
 * 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;
}

getTagValueはXMLタグの中身を取得する汎用関数です。CDATA セクション(<![CDATA[...]]>)にも対応しています。getHrefはAtomの<link href="...">から URL を取得します。

parseEntriesはフィードの形式を自動判定します。<feedタグが含まれていればAtom形式、それ以外はRSS 2.0形式として処理します。

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

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

登録手順

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

スクリプト

// --- 設定 ---
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.ResponseHeaders.Clear();
        httpClient.RequestUri = feed.url;
        var response = httpClient.Get();

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

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

        if (entries.length === 0) {
            context.Log('記事なし: ' + 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) {
                context.Log('エントリ処理エラー: ' + entry.link + ' - ' + entryError.message);
            }
        });

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

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

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() : '';
}

function getHref(xml, tagName) {
    var pattern = new RegExp('<' + tagName + '[^>]*href="([^"]*)"');
    var match = xml.match(pattern);
    return match ? match[1] : '';
}

function parseEntries(xml) {
    var entries = [];

    if (xml.indexOf('<feed') !== -1) {
        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 {
        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

処理の流れは以下のとおりです。

  1. feeds配列を順に処理する
  2. httpClient.Get()でフィードのXMLを取得する
  3. parseEntriesでXMLをパースして記事エントリの配列を取得する
  4. 各エントリについてitems.Upsertを呼び出す
    • Keys: ['ClassA']でリンクURL(分類A)をキーに指定しているため、同じURLの記事は更新、新しいURLの記事は新規作成される
  5. 処理結果をcontext.Logで出力する

httpClient.ResponseHeaders.Clear() はリクエストの前に必ず呼び出してください。詳しくは「プリザンターのサーバスクリプトでhttpClientを使うときのお約束」を参照してください。

フィードの追加

フィードを追加するには、設定セクションのfeeds配列にオブジェクトを追加するだけです。

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' }
];

プリザンターのログ機能を活用すれば、フィード取得の成否やUpsert件数を確認できます。スケジュール実行のログは「テナント管理」→「サーバスクリプト」から確認可能です。

新着記事の通知

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

通知設定の例

記事格納テーブルの「テーブルの管理」→「通知」タブで通知を追加します。

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

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

通知の詳細な設定方法については「テーブルの管理:通知」を参照してください。

カスタマイズ例

HTMLタグの除去

フィードの概要(<description><summary>)にHTMLタグが含まれている場合、テーブルにそのまま格納すると読みにくくなります。以下のようにHTMLタグを除去できます。

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

Upsert時のデータ構築でBodyに適用します。

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

古い記事の自動削除

フィードを長期運用すると記事が蓄積されていきます。一定期間を過ぎた記事を自動削除したい場合は、同じバックグラウンドサーバスクリプト内でAPIを使って古いレコードを削除する処理を追加できます。

// --- 古い記事の削除(90日以上前) ---
var maxDays = 90;
var cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - maxDays);

httpClient.ResponseHeaders.Clear();
httpClient.RequestUri = context.ApplicationPath + 'api/items/' + targetSiteId + '/get';
httpClient.Content = JSON.stringify({
    ApiKey: 'YOUR_API_KEY',
    View: {
        ColumnFilterHash: {
            DateA: '["' + cutoffDate.toISOString() + ',"]'
        }
    }
});
httpClient.MediaType = 'application/json';
var oldResponse = httpClient.Post();

if (httpClient.IsSuccess) {
    var oldResult = JSON.parse(oldResponse);
    var oldRecords = oldResult.Response.Data || [];

    oldRecords.forEach(function (record) {
        var recordId = record.IssueId;
        if (!recordId) return;

        httpClient.ResponseHeaders.Clear();
        httpClient.RequestUri = context.ApplicationPath
            + 'api/items/' + recordId + '/delete';
        httpClient.Content = JSON.stringify({ ApiKey: 'YOUR_API_KEY' });
        httpClient.MediaType = 'application/json';
        httpClient.Post();
    });

    if (oldRecords.length > 0) {
        context.Log('古い記事を削除: ' + oldRecords.length + '');
    }
}

古い記事の削除にはAPIキーが必要です。対象テーブルの削除権限を持つユーザのAPIキーを作成しておきます。

まとめ

バックグラウンドサーバスクリプトとitems.Upsertを組み合わせて、プリザンターをRSSリーダーにする仕組みを作成しました。

  • バックグラウンドサーバスクリプトでフィードの定期取得を自動化した
  • httpClient.Get()で外部のRSS/AtomフィードのXMLを取得し、正規表現でパースした
  • items.UpsertでリンクURLをキーにして重複なくレコードを登録・更新した
  • RSS 2.0とAtomの両方のフィード形式に対応した
  • フィードの追加は設定配列にURLを追加するだけで対応できる
  • Script.jsonBackgroundServerScript: trueが前提条件
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?