Node-REDのfeedparserを改造しフローからURLを指定できるようにする

node-red myfeedparser
Node-REDを使って遊んでいましたが1か月もするとやった事を忘れてしまいます。やはりメモしておかないと・・・
ノードfeedparserはatomやRSSなどのフィードを定期的に取得し使いやすいjsonオブジェクトにしてくれます。ですがフィードURLは固定の一つだけになってしまうのが不便です。巡回間隔もコントロールできません。そこで、入力端子を追加してフィードURLを受け取るように改造してみます。
Node-REDで自分のノードを作る練習にもなります。

Node-REDはjavascriptのプログラミングツールであり、Raspberry PiのOS Raspbianには最初から入っています。ネットワーク通信を簡単に書くことができます。
関連記事:Google HomeとRaspberry PiのNode-REDで赤外線リモコンのコントロールをする

Node-REDにはフィードを取得するノードnode-red-node-feedparserが組み込まれています。
node-red-node-feedparser
A Node-RED node to get RSS Atom feeds.
パーサーと言う通りフィードの形式に依らずほぼ同じjsonデータとして扱えるようにしてくれます。インターバルタイマー機能もあるため1時間に一回など定期的にフィードを取得する事もできます。IoT的な使い方にはとても便利そうです。
しかし、フィードURLは初期設定した一つだけ。複数のフィードを取得したい場合はfeedparserをURLの数だけ並べなくてはなりません。URLの追加や変更には一旦flowを停止する必要があります。
お手軽IoTにはとても便利そうなノードなのですが、本格的なフィード取得や管理には対応できません。
ライブラリを探してもフィードURLを入力できるfeedparserノードは無いようです。

Node-REDの中身はNode.jsです。node-red-node-feedparserはNode-RED独自の機能ではなくNode.jsのfeedparserをNode-REDで呼び出しやすくラップしたようなものです。
feedparser
Feedparser - Robust RSS, Atom, and RDF feed parsing in Node.js
ならば自分で使いやすいノードを作ってしまえば良いのだ!
…と思ったのですが、ゼロから作るのはいろいろしんどい。そんなに難しくないのはわかるのですが仕様の解釈をすり合わせて行くのは気力がいります。
node-red-node-feedparserのソースを見るとfeedparserをタイマーで呼び出す短いコードだったので、これをテンプレートとしてそのまま使って改造する事にしました。

node-red-node-feedparser ソースコードのコピー

早速 node-red-node-feedparser のソースコードを取得します。ソースはGitHubのnode-red-nodesの中にあります。
node-red-nodes/social/feedparser/
node-red-nodes全体は大きすぎるのでfeedparserディレクトリだけ取得したいです。ですがgitではレポジトリのサブディレクトリだけの取得はできないようです。subversionの機能を使うとサブディレクトリだけ取得できるそうです。

subversionをインストールします。
sudo apt install subversion
ソースコードはホームディレクトリ下のDocumentsフォルダにコピーする事にします。
cd ~/Documents
svn export https://github.com/node-red/node-red-nodes/trunk/social/feedparser
これでfeedparserディレクトリ以下にソースコードのコピーができました。

ソースコードを改造する

feedparserディレクトリの中身を見てみます。一つのディレクトリと5つのファイルがあります。
  • localesディレクトリ
  • 32-feedparse.html
  • 32-feedparse.js
  • package.json
  • LICENSE
  • README.md
localesディレクトリには言語別の翻訳データが入っているようです。
LICENSEとREADME.mdファイルはプログラムと関係がないので、いじる必要があるファイルは残りの3つです。
32-feedparse.jsはjavascriptファイルですからプログラムの本体です。
32-feedparse.htmlはhtmlファイルです。編集画面の表示やフロー上でのノードの見せ方を設定するようです。
package.jsonはjsonデータファイルです。ノードのバージョン、ソースの場所や依存するライブラリに関する情報を記述しているようです。

名前を変える

最初にノードの名前を変えます。feedparserのソースコードをコピーした状態なので、既にあるfeedparserと区別できるようにしなくてはなりません。
今回は"myfeedparser"としてみます。

ダウンロードしたソースディレクトリはそのままにしてmyfeedparserというディレクトリ名でコピーします。
cd ~/Documents
cp -r feedparser myfeedparser
コピーしたディレクトリ内のファイルでノード名に相当する所を"myfeedparser"に変えていきます。
詳細は以後の修正に含めます。

ファイルの修正

package.json

package.jsonを編集します。名称、バージョン番号やノード名を変更します。名前は"node-red-contrib"で始まるようにするようです。
元ファイル(Ver.0.1.12)との差分は次のようになります。
--- ./feedparser/package.json 2018-03-30 22:50:51.000000000 +0900
+++ ./myfeedparser/package.json 2018-04-09 13:09:35.025390745 +0900
@@ -1,6 +1,6 @@
 {
-  "name": "node-red-node-feedparser",
-  "version": "0.1.12",
+  "name": "node-red-contrib-myfeedparser",
+  "version": "0.0.1",
   "description": "A Node-RED node to get RSS Atom feeds.",
   "dependencies": {
     "feedparser": "^2.2.9",
@@ -8,7 +8,7 @@
   },
   "repository": {
     "type": "git",
-    "url": "https://github.com/node-red/node-red-nodes/tree/master/social/feedparser"
+    "url": ""
   },
   "license": "Apache-2.0",
   "keywords": [
@@ -18,7 +18,7 @@
   ],
   "node-red": {
     "nodes": {
-      "feedparse": "32-feedparse.js"
+      "myfeedparse": "32-feedparse.js"
     }
   }
 }

32-feedparse.html

ノード名の変更、インターバルの入力ボックスの削除、ノード入力の追加などを行います。元ファイル(Ver.0.1.12)との差分は次のようになります。
--- ./feedparser/32-feedparse.html 2018-03-30 22:50:51.000000000 +0900
+++ ./myfeedparser/32-feedparse.html 2018-04-09 14:49:49.466857267 +0900
@@ -1,20 +1,16 @@
 
-<script type="text/x-red" data-template-name="feedparse">
+<script type="text/x-red" data-template-name="myfeedparse">
     <div class="form-row">
-        <label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="feedparse.label.feedurl"></span></label>
+        <label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="myfeedparse.label.feedurl"></span></label>
         <input type="text" id="node-input-url">
     </div>
     <div class="form-row">
-        <label for="node-input-interval"><i class="fa fa-repeat"></i> <span data-i18n="feedparse.label.refresh"></span></label>
-        <input type="text" id="node-input-interval" style="width:60px"> <span data-i18n="feedparse.label.minutes"></span>
-    </div>
-    <div class="form-row">
         <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
         <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
     </div>
 </script>
 
-<script type="text/x-red" data-help-name="feedparse">
+<script type="text/x-red" data-help-name="myfeedparse">
     <p>Monitors an RSS/atom feed for new entries.</p>
     <p>The <code>msg.topic</code> contains the original article link. The <code>msg.payload</code>
     contains the description, and <code>msg.article</code> contains the complete article object,
@@ -23,19 +19,18 @@
 </script>
 
 <script type="text/javascript">
-    RED.nodes.registerType('feedparse',{
+    RED.nodes.registerType('myfeedparse',{
         category: 'advanced-input',
         color:"#C0DEED",
         defaults: {
             name: {value:""},
-            url: {value:"", required:true},
-            interval: { value:15, required:true, validate:function(v) {return (!isNaN(parseInt(v)) && (parseInt(v) <= 35790))} }
+            url: {value:""},
         },
-        inputs:0,
+        inputs:1,
         outputs:1,
         icon: "feed.png",
         label: function() {
-            return this.name||this.url||this._("feedparse.feedparse");
+            return this.name||this.url||this._("myfeedparse.myfeedparse");
         },
         labelStyle: function() {
             return this.name?"node_label_italic":"";

localesファイル

localesディレクトリ下のen-USおよびjaディレクトリにあるファイルを修正します。それぞれテキストエディタで開いて、"feedparser"と書かれた部分を"myfeedpaser”と変更します。

32-feedparse.js

入力からのURLを受け取るようにします。
インターバルタイマーのロジックを削除します。インターバルはフローで実現することになります。URLはメッセージmsg.urlの値が読み込まれます。
変更としては少しなのですが、インターバルタイマーの削除でブロックの括弧の位置が微妙に変わりとてもわかりずらいです。変更後のソースを置いておきます。
module.exports = function (RED) {
  "use strict";
  var FeedParser = require("feedparser");
  var request = require("request");
  var url = require('url');

  function MyFeedParseNode(n) {
    RED.nodes.createNode(this, n);
    this.url = n.url;
    var node = this;
    this.on("input", function (msg) {
      if (msg.url) { this.url = msg.url; } //input URL
      this.seen = {};
      var parsedUrl = url.parse(this.url);
      if (!(parsedUrl.host || (parsedUrl.hostname && parsedUrl.port)) && !parsedUrl.isUnix) {
        this.error(RED._("feedparse.errors.invalidurl"));
      } else {
        var getFeed = function () {
          var req = request(node.url, { timeout: 10000, pool: false });
          //req.setMaxListeners(50);
          req.setHeader('user-agent', 'Mozilla/5.0 (Node-RED)');
          req.setHeader('accept', 'text/html,application/xhtml+xml');
          var feedparser = new FeedParser();

          req.on('error', function (err) {
            node.error(err + ':' + node.url);
            req.abort();
          });
          req.on('response', function (res) {
            if (res.statusCode != 200) {
              node.warn(RED._("feedparse.errors.badstatuscode") + " " + res.statusCode);
            }
            else {
              res.pipe(feedparser);
            }
          });

          feedparser.on('error', function (error) {
            node.error(error);
          });

          feedparser.on('readable', function () {
            var stream = this, article;
            while (article = stream.read()) {  // jshint ignore:line
              if (!(article.guid in node.seen) || (node.seen[article.guid] !== 0 && node.seen[article.guid] != article.date.getTime())) {
                node.seen[article.guid] = article.date ? article.date.getTime() : 0;
                //var msg = {
                //  topic: article.origlink || article.link,
                //  payload: article.description,
                //  article: article
                //};
                msg.topic = article.origlink || article.link;
                msg.payload = article.description;
                msg.article = article;
                node.send(msg);
              }
            }
          });

          feedparser.on('meta', function (meta) {
          });
          feedparser.on('end', function () {
            req.abort();
          });
        };
        getFeed();
      }
    });

    this.on("close", function () {

    });
  }

  RED.nodes.registerType("myfeedparse", MyFeedParseNode);
}
【2018/04/14 修正: msgが初期化されflowのデータを引き継がなかったのを修正しました】

ノードモジュールのテスト

改造したノードモジュールを動かしてみます。公開できる完成度ではないので改造したファイルがあるディレクトリをリンクして動くようにします。
Node-REDを停止し改造したファイルのあるディレクトリへ移動しリンクします。
node-red-stop
cd ~/Documents/myfeedparser
sudo npm link
エラーなくリンクができたならNode-REDを動かします。
node-red-start
正しくモジュールを認識するとmyfeedparseが表示されるでしょう。
node-red myfeedparser
入力端子がありますね。msg.urlに値が設定されていればそのURLを使います。

コードの修正する場合はリンクを切断します。Node-REDを停止し作成したノードのディレクトリで
sudo npm unlink
とするとリンクが解除されます。修正後に再びリンクします。

まとめ

Node-REDのfeedparserを改造してフロー入力のメッセージからフィードURLを指定できるようにしました。既存のノードを改造する事でNode-REDで独自のノードを作るときの勉強になりました。
32-feedparse.jsの変更はこれで良いのかまだ自信がありません。基本的には問題ないと思っています。
今回は必要最低限の修正しかしていません。フォークして公開するにはもう少し体裁を整えないと…

javascriptで非同期通信を書くとバグに悩みます。今回のインターバルタイマーの削除も閉じ括弧の位置を間違えて大変悩みました。Node-REDのノード化をすると非同期部分の見通しと切り分けがやりやすくなるはずです。



コメント

最近のコメント

Threaded Recent Comments will be here.