Node.jsからPythonのコードを呼び出すPython-shellを使ってみた

# Node.jsからPythonの処理を実行したくて・・・
ちょっとばかし、簡易的なデモアプリを作っていて、Node.jsでアプリを書き始めていました。しかし、急遽Python使って機械学習モデルを使った予測結果をNode.js側で表示する必要が出てきました。Flaskとか使ってRESTの口を用意することはすぐに思いついたのですが、同じPC内で動かすならそこまで頑張る必要もないと思い、もっと簡易的な方法が無いのかなと探したところ、Python-shellというNode.jsのライブラリがあるではないですか！
ということで、それを使ってみた記録になります。

# Python-shellとは
Node.jsのライブラリで、Node.js上でPythonスクリプトを実行するができます。
マニュアルは以下になります。
https://www.npmjs.com/package/python-shell

他に深い説明はないです。というかできないです。ホントこれだけ。

# Python-shellってどう使うの？
## まずインストールする
Node.jsおよびNPMはインストールされていることは前提になります。以下のコマンドでインストールです。

``` 
npm install python-shell
```

はい、簡単ですね。インストールもすぐに終わります。

## Node.js上に書いてみる。
以下のようにpython-shellを呼び出します。
変数名に{}を付けることがポイントのようです。

```test.js
var {PythonShell} = require('python-shell');
```

呼び出し方は３種類あって、それぞれ説明します。

### 書き方その①：コードべた書きパターン
まずはNode.jsのコードに中で、Pythonのコードをべた書きする方法です。
その時はrunStringを使います。
左から、第１引数がPythonのコード、第２引数が実行時のオプション（後述）、第３引数がNode.jsのコールバック関数になります。

```test.js
var {PythonShell} = require('python-shell');
PythonShell.runString('x=1+1;print(x)', null, function (err,data) {
  if (err) throw err;
  console.log(data)
  console.log('finished');
});
```

怠かったんで、マニュアルからそのままパクってきました。
こうすると、計算結果の2が返ってきます。
ポイントはPython内でprintした標準出力の結果が、Node.js側にレスポンスとして返されるようです。


### 書き方その②：スクリプト読み込みパターン
Pythonのコードが長くなったら、べた書きはありえません。なので、スクリプトを別に分けてコードを書くことが一般的でしょう。my_script.pyというPythonコードを用意したとして、以下のように呼び出します。

```test.js
var {PythonShell} = require('python-shell');
PythonShell.run('my_script.py', null, function (err) {
  if (err) throw err;
  console.log('finished');
});
```

これも、怠かったんで、マニュアルからそのままパクってきました。

### 書き方その③：インスタンス化して実行するパターン
newを使ってインスタンスを作ります。その後、sendで文字列を用意して、'message'を使ってPython側に送り込んで、コールバック内の変数messageにPython側でのレスポンスが返ってきます。

```test.js
var pyshell = new PythonShell('my_script.py');
 
// sends a message to the Python script via stdin
pyshell.send('hello');
 
pyshell.on('message', function (message) {
  // received a message sent from the Python script (a simple "print" statement)
  console.log(message);
);
```

## オプションはどう指定するのさ？
JSON形式で用意します。代表的なものは以下かなと。
特に重要なものを少し説明します。
pythonPathは明示的に指定しましょう。Python2と3が共存している環境の場合に、ちゃんと3を実行してくれるようにパスを指定することを強く推奨します。Python2の状態で実行しようとしたら動かなかったので、ここでは絶対に欠かさないように！

```test.js
var {PythonShell} = require('python-shell');
var options = {
  mode: 'text', // textもしくはjson
  pythonPath: 'path/to/python', // Python3のパスを指定しないと動かないので注意
  pythonOptions: ['-u'], 
};

PythonShell.run('my_script.py', options, function (err) { // 第２引数にオプションを入れる
  if (err) throw err;
  console.log('finished');
});
```

# 具体的にやってみたサンプルコード
Python側も具体的にやってみたサンプルを載せます。
やりたいことは、Node.js側からjson形式のデータをPython側に送りつけて、それをpandasに入れて予測モデルを実行し、その結果を返すまでのコードです。

## Python側のコード
①Node.jsから受け取ったJSONデータをPandasに入れる
②LightGBMで書かれたモデルをpickleで読み込む
③スコアリングをする


```test.py
import sys,json,pickle,lightgbm
import pandas as pd
jsonData =sys.stdin.readline() #  ①データはこうやって読み込むらしいがテキスト形式になっているので注意！
valid_x = pd.io.json.json_normalize(json.loads(jsonData) ) # json形式に変換してから、Pandasに読み込ませる

with open('./model.pickle', mode='rb') as fp: # ②モデルの読み込み
   fit = pickle.load(fp)

y_pred = fit.predict(valid_x, num_iteration=fit.best_iteration)
print(y_pred) # ③スコアリングの実施。この標準出力結果がNode.jsに返される
```

### Node.js側のコード
json形式のデータを受け取って、それをPythonに送って実行させて、その結果を表示する。
pythonに送るときに、データ形式をテキストに直さないと動かないのがポイントでした。（もしかしたら、適切な方法があるかもですが、オプションとか変更しても違いがわかりませんでした）

```test.js
var testData = require('./test.json'); // JSON形式のデータを変数に格納
var {PythonShell} = require('python-shell');

var options = {
    pythonPath: 'C:/Users/XXXXXXXXX/Anaconda3/python' ,
    pythonOptions: ['-u'], 
    scriptPath: './'
};
   
var pyshell = new PythonShell('./test.py',options);
pyshell.send(JSON.stringify(testData)) ; // 文字列に直してからじゃないと送れないみたい・・・。JSONのままだと怒られる。modeをjsonで指定しても変化はなかった。
pyshell.on('message',function(message){
     console.log("予測結果:"+JSON.parse(message) ); // JSONにパースする必要はないけども、これが割と求めている表示になってくれたので、こうしている
});
```

```出力
予測結果:0.1
```


とまあ、こんな感じでした。
他にもポツポツ落とし穴がありそうな雰囲気ですが、とりあえず自分がやりたいことはこれでできたので、大丈夫と思います。