Node-RED+python-shellでNode-REDからpythonを呼び出すノードを自作する

Node-REDでpythonスクリプトを呼び出すノードを自作してみました。
#はじめに
WebスクレイピングをpythonのBeautiful Soupで作ったのですが、それをどうしてもNode-REDに組み込みたかったので、**python-shell**を使ってNode-REDでpythonを実行できるノードを自作してみました。

これを利用すると例えば、tensor-flow や keras で作成した AI を簡単にNode-REDに埋め込むことも可能となります。

#使ったもの
- Node.js v10.16.3
- Node-RED v0.20.7
- python 3.7
- python-shell

#前提
- Node.jsとNode-REDはすでにインストールされて使える状態になっていることを前提に話を進めます。
- Node-REDの自作ノードの作成方法については詳細に説明しません。

#最終的にはこんな感じになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/9772d440-a5c8-0d8d-3865-e2e29526a2ee.png)

真ん中の青っぽいノードがPythonスクリプトを実行する**すごい**ノードです。

#python-shellをインストール
python-shellは、Node.jsからpythonのコードやファイルを実行できるモジュールです。
インストールは以下のコマンドを実行します。

```
npm install python-shell
```
#自作ノードの構成
以下のような構成になります。

```title:ディレクトリ構成
└── node-red-contrib-python
    ├── python-shell
    │   ├── icons
    │   │   └── python.png
    │   ├── python-shell.html
    │   └── python-shell.js
    └── package.json
```

※「node-red-contrib-python」という一般的な名前にしてしまったので、必要に応じて適宜変更してください。

#package-json
package-jsonは、Node.jsモジュールの内容を記述するためのファイルです。
直接作成してもよいですし、以下のコマンドでも作成できます。

```
npm init
```
このコマンドは、対話的にpackage.jsを作成することができます。

```json:package.json
{
    "name": "node-red-contrib-python",
    "version": "1.0.0",
    "description": "",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "keywords": [ "node-red" ],
    "node-red" : {
        "nodes": {
            "python-shell": "python-shell/python-shell.js"
        }
    }
} 
```

nameにパッケージ名、node-red/nodes/python-shellにJavaScriptへの相対パスを記述します。
license等は適宜変更してください。

#python-shell.html
python-shell.htmlは、Node-REDでプロパティを定義させる画面を記述します。ここでは、以下のプロパティを設定します。

| プロパティ       | id              |    説明    | 種別 |
|:-----------------|----------------------:|:------------------:|:-------:|
| 名前             | node-input-name |        ノードの表示名を設定する       |text|
| コード           | node-input-python |       pythonのコードを設定する       |hidden|
| エディタ         | node-input-python-editor | pythonコードを編集するエディタを定義する |editor |
| pythonパス       | node-input-pypath | pythonのパスを指定する | text |
| python-shellパス | node-input-pyshellpath | python-shellのパスを指定する | text |

以下のようなプロパティページを定義します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/1874e126-79c0-31f9-8f9c-44f1f2a076fd.png)

以下がHTMLの全量です。

```html:python-shell.html
<script type="text/x-red" data-template-name="python-shell">
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> 名前</label>
        <input type="text" id="node-input-name">
    </div>
    <div class="form-row" style="margin-bottom: 0px;">
        <label for="node-input-python"><i class="fa fa-wrench"></i> コード</label>
        <input type="hidden" id="node-input-python" autofocus="autofocus">
    </div>
    <div class="form-row node-text-editor-row">
        <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-python-editor" ></div>
    </div>
    <div class="form-row">
        <label for="node-input-pypath"><i class="fa fa-tag"></i> pythonパス</label>
        <input type="text" id="node-input-pypath">
    </div>
    <div class="form-row">
        <label for="node-input-pyshellpath"><i class="fa fa-tag"></i> python-shellパス</label>
        <input type="text" id="node-input-pyshellpath">
    </div>
</script>
<script type="text/x-red" data-help-name="python-shell">
    <p>python shell</p>
</script>
<script type="text/javascript">
    RED.nodes.registerType('python-shell',{
        color:"#4682b4",
        category: 'python',
        defaults: {
            name: {value:""},
            python: {value:"\nprint('hoge')"},
            pypath: {value:"python"},
            pyshellpath: {value:"./python-shell"}
        },
        inputs:1,
        outputs:1,
        icon: "python.png",
        label: function() {
            return this.name;
        },
        oneditprepare: function() {
            var that = this;
            this.editor = RED.editor.createEditor({
                id: 'node-input-python-editor',
                mode: 'ace/mode/none',
                value: $("#node-input-python").val()
            });
            RED.library.create({
                url:"functions", // where to get the data from
                type:"function", // the type of object the library is for
                editor:this.editor, // the field name the main text body goes to
                fields:['name','outputs']
            });
            this.editor.focus();
        },
        oneditsave: function() {
            var annot = this.editor.getSession().getAnnotations();
            $("#node-input-python").val(this.editor.getValue());
            this.editor.destroy();
            delete this.editor;
        },
        oneditcancel: function() {
            this.editor.destroy();
            delete this.editor;
        },
        oneditresize: function(size) {
            var rows = $("#dialog-form>div:not(.node-text-editor-row)");
            var height = $("#dialog-form").height();
            for (var i=0; i<rows.size(); i++) {
                height -= $(rows[i]).outerHeight(true);
            }
            var editorRow = $("#dialog-form>div.node-text-editor-row");
            height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
            $(".node-text-editor").css("height",height+"px");
            this.editor.resize();
        }
    });
</script>
```

###oneditprepare: function()
エディタを準備するときの処理を記述しています。RED.editorのcreateEditorを呼び出し、エディタを構築しています。modeは一旦noneとしています。

###oneditsave: function()
保存の際は、idがnode-input-pythonのhidden項目に値を移送しています。jsではこのhiddenの値を使用します。

#python-shell.js
Node-REDでノードが呼び出されたときの処理を記述します。
設定されたコードでpython-shellを呼び出し、その戻り値をmsg.payloadに指定して、node.sendしています。

```javascript:python-shell.js
module.exports = function(RED) {
    function PythonShellNode(config) {
        RED.nodes.createNode(this,config);
        this.name = config.name;
        this.python = config.python;
        this.pypath = config.pypath;
        this.pyshellpath = config.pyshellpath;

        var node = this;
        node.on("input", function(msg) {
            try {
                var {PythonShell} = require(node.pyshellpath); 
                let options = {
                    pythonPath: node.pypath, 
                    pythonOptions: ['-u']
                  };

                PythonShell.runString(node.python, options, function (err, result) {
                    if (err) {
                        msg.payload = err;
                    } else {
                        if (result.length > 0) {
                            msg.payload = result[0];
                        } else {
                            msg.payload = "";
                        }
                    }
                    node.send(msg);
                });
            } catch(err) {
                node.error(err.message);
            }
        });
    }
    RED.nodes.registerType("python-shell",PythonShellNode);
}
```

以下のコードでpython-shellをrequireします。python-shellのパスは、プロパティに設定されたパスを使用します。
`var {PythonShell} = require(node.pyshellpath);`

次に、python-shellのオプションを指定します。

```javascript
let options = {
    pythonPath: node.pypath, 
    pythonOptions: ['-u']
};
```
pythonPathにプロパティで指定されたpythonのパスを設定しています。conda等で作成したpythonを利用したい場合は、そのpythonの絶対パスをプロパティで指定すればOKです。

`PythonShell.runString`を呼び出し、pythonを実行します。コールバックを定義し、処理終了後の処理を記述します。
処理終了後は、msg.payloadにresult[0]を設定し、node.send(msg)で次ノードに結果を引き渡しています。

#icons
アイコンは、pythonっぽいアイコンをサイトからダウンロードし、40×60で白色のPNGに編集し、iconsファイルに配置しました。
ノードの色もpythonっぽい色を指定しました。
この指定は、python-shell.htmlのRED.nodes.registerTypeのiconとcolorで変更できます。

#Node-RED実行環境にインストール
`~/.node-red`で以下のコマンドを実行します。

```
npm install <モジュールの配置場所>/node-red-contrib-python
```

モジュールの配置場所は、node-red-contrib-pythonを配置した場所に従い適宜変更してください

インストールに成功すると、Node-REDの左側のノード一覧にパレットが追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/eb842da3-d473-e77e-c135-4bfd31b1fbdf.png)
追加されない場合は、Node-REDを再起動してみてください。

#実際に動かしてみる

今回は、Deep Learningの基本であるパーセプトロンをpythonで実装し、そのXORを計算するノードを作成し、Node-REDに組み込んでみます。

####injectノードを配置する

簡単のため、起動はinjectノードで行います。
左側のノード一覧から、injectノードをドラッグアンドドロップで配置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/3c664081-106e-2aaf-e7aa-dfef7fb71013.png)

ペイロードはデフォルトでタイムスタンプになっているので、文字列に変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/be15c0aa-bb81-db09-edea-29e4315b4a12.png)


####python shellノードを配置する 

同様にpython shellノードを配置し、コネクタでつなぎます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/5124d284-f1df-cb17-13be-dab59c158ce5.png)

プロパティを編集します。
pythonパスとpython-shellパスは、デフォルトだとうまく動かないかもしれませんので、その場合は、フルパスで指定してみてください。

コードには以下のpythonを入力します。

```python:pythonコード
def AND(x1, x2):
    w1, w2, theta = 0.5, 0.5, 0.7
    tmp = x1*w1 + x2*w2
    if tmp <= theta:
        return 0
    elif tmp > theta:
        return 1
    
def NAND(x1, x2):
    w1, w2, theta = -0.5, -0.5, -0.7
    tmp = x1*w1 + x2*w2
    if tmp <= theta:
        return 0
    elif tmp > theta:
        return 1

def OR(x1, x2):
    w1, w2, theta = 0.5, 0.5, 0.3
    tmp = x1*w1 + x2*w2
    if tmp <= theta:
        return 0
    elif tmp > theta:
        return 1

def XOR(x1, x2):
    s1 = NAND(x1, x2)
    s2 = OR(x1, x2)
    y = AND(s1, s2)
    return y

print('%d %d %d %d' % (XOR(0,0),XOR(0,1),XOR(1,0),XOR(1,1)))
```


#### 出力確認用のデバッグノード追加
出力を確認するためのデバッグノードを同様に配置しコネクタでつなげます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/ffc0b706-c4ae-eb78-9e25-c88e59e33aa8.png)

### デプロイして実行する

デプロイして、injectノードを実行すると、デバッグメッセージに、XORの結果が表示されます。
**やった～！！！**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/d6b392f4-e233-a147-cd6a-4e86261515d8.png)

#さいごに
これを使うとpythonで作ったPGを簡単にNode-REDをつかって公開できるようになるので、自分としてはかなり重宝しそうです。前ノードからデータを受け渡したり、pythonオブジェクトを受け渡せるようにしたりできたり、いろいろ拡張していきたいです。
ラズパイとかでNode-REDを使うときとかにも、エッジでのAIのプレディクトとかにも使ってみたいです。

#参考にしたサイト
初めてのノード開発
https://nodered.jp/docs/creating-nodes/first-node

【Node.js入門】python-shellによるNode.jsとPythonのデータ連携方法まとめ
https://www.sejuku.net/blog/84736