Node-red+Line Messaging APIで「しりとり」ボットくらいカンタンに作れるだろとやってみた

#経緯
ボット界隈が何か賑わっていたのと、Node-redのお勉強で何か作りたいと思っていました。
チャットボットで色々作る為の練習として、ローソンのLineBotのあきこちゃんがしりとり出来ていたので、これくらいならちゃちゃっと出来て良い練習だろうと軽い気持ちで取り組みはじめました。

#結論

 - まずしりとりデータ用としてクローラ作ってみたら、処理速度のチューニングが大変だった。
 - しりとり自体の基本ロジック、８割方はまぁテンポよく作れました。
 - ただ、日本語の文字豊富さ、DB、形態素解析の癖、Node-red（Node.js?）の癖により、細々とした所をこちら側で潰さないと恥ずかしい感じになっちゃうので、それらのフォローが必要でした。以下は後ほど解説。
    - DBの検索結果が想定外
    - 形態素解析が理解出来ないカタカナだけ文字
    - ー（棒線）
    - 記号
    - 小さい奴ら（ぁ、ぃ、ぅ、ぇ、ぉ、ゃ、ゅ、ょ）
 - ほぼあきこちゃんクラスの対応が出来るBotに仕上がったし、色々とNode-redの勉強になった。


Shiritoriボット君です。倒してみて下さい。「しりとり」でしりとり開始します。（QRコード読み込みで登録できます）
![Shiritori](http://qr-official.line.me/L/3jbpw2SXV1.png)
<a href="https://line.me/R/ti/p/%40eqe0984y"><img height="36" border="0" alt="友だち追加" src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png"></a>

#前提

 - LINE Messaging APIはすでに使えている（バリデーション含めて）
 - Bluemix環境は使える
 - Cloudantのデータベースの作成やIndex操作を出来る
 - MQ Liteも使える
 - Node-redに[Kuromojiが入っている](https://github.com/kotasuizu/node-red-kuromoji/blob/master/README_JP.md)




#環境

 - サーバー環境はBluemixのNode-redボイラープレートなので略
 - データベースはBluemixのCloudant
 - MQはBluemixのMQ Lite


#ここから本題です

#クローラについて
しりとりではそのネタとなる単語が多数必要となりますが、辞書を整形したり手でデータ作ったりするのが嫌だったので、WEBサイトから単語取って来れたら楽じゃんよ、という発想になりました。
そこで、

1. 【URLゲッター】サイトのアドレスを入れたらhtmlとなっている記事の一覧を取得する
2. 【名詞ゲッター】記事からテキストを引っこ抜いてから、形態素解析して名詞のみ取得する
3. 【名詞セーバー】取得した値がデータベースに存在しなければ新規追加する

という流れで、データの取得を自動化しようと試みました。

##URLゲッター
###初期のロジック
####概要
まず、URLゲッターの処理概要です。

1. HTTP requestノードでアドレスを取得
2. HTMLノードで、a:linkで指定して、attributesとelementsも取得し、結果はmultiple messagesで返す
3. 結果のhrefについて、色々フィルターかけてゴミを排除
4. データベースに存在するか確認して、無ければ追加し、かつ再帰用のMQに放り込む

という流れで作成してみました。
しかし、やれどもやれども、ちっとも帰ってくるデータが変です。多数のa:linkが有るはずなのに数個しか値が帰ってこない＆場合によって結果が異なるのです。困った困った。


####問題点
 - 初回トリガー時は、１つのmsgが流れます。これにより取得されたHTMLデータをHTMLノードで処理してリンクを取得する際に、数十個のmsgが発生します。この数十個のmsgを生成する際は高速ですが、後続のDB検索処理がその速度に比べて遅すぎる為、1つの検索クエリーが処理し終わる前に数十個のmsgが次から次にDB検索ノードにたどり着いてしまい、msgが上書きされている模様です。（なので、最初のリンクと最後のリンクだけが残ったりする。但し、挙動は不安定です）
 - リンクを深掘りする為に新規のアドレスは再帰用のMQに取り込んでから取り出すつもりが、ここもまた取り出すスピードが早すぎて、どうやら上書き問題が発生している模様。（全体の文書数が細かく把握できていないので、どれくらい上書き問題により取りこぼしているかがわからない）
 - 多分、秒あたりのGET回数が多すぎて、サーバーに負荷がかかる（目立つ）。対象のサイト管理は自前だから著作権的には問題ないけど、レンタルサーバー屋さん的に怒られかねない？
 - DBへの重複検索回数が、文書数×文書の中に有るリンクの数になる。例えば、2000文書x30リンクだと6万回の検索！？無駄無駄無駄ァァアァ！！！！後半は殆ど重複となるはずなのに、それでも検索しなきゃならんのがダサい。


####対応方針
 - DB検索が遅いので、msgにDelayノードで1秒当り5～20個の制限を掛けたいと思いました。
 - 但し、今度はDelayノードで上書き問題が発生してしまうため、全く意味なし。
 - MQかましたら遅く出来るかな、、とやってみたけど、MQからの取り出しが早すぎてやっぱり意味なし。
 - そこで、一度context.globalに配列を用意して、payloadを含むmsgが来た場合は配列にPushする。payloadを含まない場合は、配列から一個データを取り出してmsg.nextitemに突っ込んで後続に渡す方式にする。そしたらその途中にDelayを入れる事が出来る。

このアプローチでやってみました。

####途中経過
という事で、まずは速度制御問題の解決をTryした結果が以下。

![Crawler_old.JPG](https://qiita-image-store.s3.amazonaws.com/0/116534/6a54514f-e1d9-ce34-7503-d2d16ba5445a.jpeg)

途中でぐるぐるしている所がcontext.globalの配列つかって取りこぼしなく、かつ、速度を自由に調整出来るようにした所です。
一部にはまだMQが残ってますね。ダサい。


####まだ残っている問題点
 - 再帰用のMQ部分は速度制限のつもりで導入していたものなので、ここも意味なし。
 - 文書数×リンク数の検索が無駄問題。速度は制御出来るようになったものの、数万回の無駄なDB検索とHTMLアクセスは恥ずかしくて耐えられない。一応、検索は5msg/s迄みたいなCloudantAPIプランの規制があり、そうなると何分待たされるんだよ、となる。


## 改善したロジック
 - すべてのMQ無くした
 - 途中のぐるぐるのcontext.globalで配列に突っ込んでいる箇所に、追加でアドレスをキーとした連想配列をcontext.global以下に作って重複排除した。これだとCPU食うけど、まぁ5msg/sに比べたら圧倒的な速さなので無視出来る。メモリも食うかもだけど、まあ数千～数万件程度のURL位だったら全然OKでしょう。
 - 途中にある分類器で、jpgやPNGは落として、htmlだけデータベースに保管する。

全体像はこんな感じ。あら、スッキリ。
![Crawler_new.JPG](https://qiita-image-store.s3.amazonaws.com/0/116534/8926ee94-db76-7a01-57e3-e99d14e0f93c.jpeg)

####ソース
```json:Crawler
[{"id":"22010f88.d3a2f","type":"function","z":"9ded7b0.a4c6b88","name":"set URL and method","func":"var url = \"http://hogehoge.jp/\";\nvar method = \"GET\";\n\nglobal.targetUrl = url;\nmsg.url = url;\nmsg.method = method;\n\n\nreturn msg;","outputs":1,"noerr":0,"x":440,"y":1300,"wires":[["96b9f915.511238"]]},{"id":"2625a29c.7dbffe","type":"inject","z":"9ded7b0.a4c6b88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":260,"y":1300,"wires":[["22010f88.d3a2f"]]},{"id":"96b9f915.511238","type":"function","z":"9ded7b0.a4c6b88","name":"prep urls, kvs, j","func":"\ncontext.global.urls = new Array();\ncontext.global.kvs = new Array();\ncontext.global.j = 0;\n\nreturn msg;","outputs":1,"noerr":0,"x":640,"y":1300,"wires":[["e66d78e6.22c838"]]},{"id":"e66d78e6.22c838","type":"http request","z":"9ded7b0.a4c6b88","name":"","method":"GET","ret":"txt","url":"","tls":"","x":250,"y":1400,"wires":[["783bebca.ff9294"]]},{"id":"783bebca.ff9294","type":"html","z":"9ded7b0.a4c6b88","name":"","tag":"a:link","ret":"attr","as":"multi","x":390,"y":1400,"wires":[["d318e357.4c20e"]]},{"id":"d318e357.4c20e","type":"function","z":"9ded7b0.a4c6b88","name":"for each","func":"var tmp2 = {\"payload\":\"\"};\n\nif (msg.payload !== undefined && msg.payload !== null){ \n    if( context.global.kvs[msg.payload.href]){\n        return [null,null];  \n    }else{\n        context.global.kvs[msg.payload.href] = 1;\n        context.global.urls.push(msg.payload);\n    }\n    msg.payload = \"\";\n    return [null,msg];\n}else{\n    var tmp = {\"payload\":\"\"};\n    tmp.payload = context.global.urls[ context.global.j -1 ];\n    msg.payload = \"\";\n    return [tmp,msg];\n}\n\n\n","outputs":"2","noerr":0,"x":540,"y":1400,"wires":[["5f1e59d1.9c9a58"],["82c70883.7b01e8"]]},{"id":"5f1e59d1.9c9a58","type":"function","z":"9ded7b0.a4c6b88","name":"is not null","func":"if( msg.payload != undefined && msg.payload != null)\n{\n    return msg;\n}","outputs":1,"noerr":0,"x":720,"y":1400,"wires":[["f812bc91.bc7c8"]]},{"id":"82c70883.7b01e8","type":"delay","z":"9ded7b0.a4c6b88","name":"5msg/s","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"5","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":480,"y":1460,"wires":[["459c77c1.c21288"]]},{"id":"459c77c1.c21288","type":"function","z":"9ded7b0.a4c6b88","name":"++","func":"if ( (context.global.j += 1) < context.global.urls.length ){\n  msg.payload = null;\n  return msg;  \n} \n\n","outputs":1,"noerr":0,"x":590,"y":1460,"wires":[["d318e357.4c20e"]]},{"id":"f812bc91.bc7c8","type":"switch","z":"9ded7b0.a4c6b88","name":"filter site","property":"payload.href","propertyType":"msg","rules":[{"t":"cont","v":"targetUrl","vt":"global"},{"t":"else"}],"checkall":"false","outputs":2,"x":280,"y":1640,"wires":[["4b56618.ea143a"],[]]},{"id":"4b56618.ea143a","type":"switch","z":"9ded7b0.a4c6b88","name":"分類器","property":"payload.href","propertyType":"msg","rules":[{"t":"regex","v":".*page.*","vt":"str","case":true},{"t":"regex","v":".html$","vt":"str","case":false},{"t":"cont","v":"event_report","vt":"str"},{"t":"regex","v":".jpg$","vt":"str","case":false},{"t":"regex","v":"png$","vt":"str","case":true},{"t":"else"}],"checkall":"false","outputs":6,"x":432,"y":1642,"wires":[["ef837702.f424e8"],["6d7f9a9c.6e1a24","ef837702.f424e8"],["ef837702.f424e8"],[],[],["ef837702.f424e8"]]},{"id":"6d7f9a9c.6e1a24","type":"function","z":"9ded7b0.a4c6b88","name":"set payload ","func":"var url = msg.payload.href;\nmsg.payload.url = url;\nmsg.payload.timestamp = Date.now();\nmsg.payload.crawlCount = 0;\nmsg.payload.crawlStatus = \"address_stored\"\n\nreturn msg;","outputs":1,"noerr":0,"x":630,"y":1660,"wires":[["32e8add0.b070a2"]]},{"id":"32e8add0.b070a2","type":"cloudant out","z":"9ded7b0.a4c6b88","name":"","cloudant":"","database":"http_crawler","service":"cloudantNoSQLDB","payonly":true,"operation":"insert","x":790,"y":1660,"wires":[]},{"id":"ef837702.f424e8","type":"function","z":"9ded7b0.a4c6b88","name":"set payload","func":"var url = msg.payload.href;\nvar method = \"GET\";\n\nmsg.url = url;\nmsg.method = method;\n\nreturn msg;","outputs":1,"noerr":0,"x":630,"y":1620,"wires":[["e66d78e6.22c838"]]}]
```

このお陰で、数時間かかっていたURLゲッター処理が数分で終わるようになりましたとさ。
:raised_hands:バンザーイ:raised_hands:

##名詞ゲッター
上記のURLゲッターで取ってきたHTMLアドレスをもう一度読み込んで、形態素解析かけてから名詞だけを取り出します。

![Tokenizer.JPG](https://qiita-image-store.s3.amazonaws.com/0/116534/39d96f79-7b8c-8ea9-9f9d-985f727ed97e.jpeg)

1. 手動によるトリガー、もしくはURLゲッターからの継続で指定されたアドレスのHTMLを取得する
2. そのHTMLをHTMLノードに突っ込んで<p>タグのテキストだけをこれまた懲りずにMultipulなmsgとして取り出す。
3. その結果をKuromojiで形態素解析します。入力テキストの各要素が配列一個ずつに入っています。
4. それをぐるぐる回っている所では、3で渡された配列を一個一個分解してから、context.global.dataという配列にPushします。取り出し方はURLゲッターとほぼ同じです。
5. あとは後続のURLセーバーの為のMQに突っ込みます。


**「あれれ、またMQ出てきたけど大丈夫なの？」**と思う方もいらっしゃるでしょう。その通り、良くないです:relaxed:。
URLゲッター、名詞ゲッター、名詞セーバーと一通り動くモノをつくってから、上記のURLゲッターの改善を行ったので、ここは未着手です。まぁ現状で3600くらい名詞あるから困ってないし。その内改善します。

```json:Tokenizer
[{"id":"75699701.f69538","type":"html","z":"9ded7b0.a4c6b88","name":"","tag":"p","ret":"text","as":"multi","x":890,"y":120,"wires":[["9136086e.ceeb88"]]},{"id":"9136086e.ceeb88","type":"Kuromoji Tokenizer","z":"9ded7b0.a4c6b88","name":"Tokenize","x":190,"y":197.00003051757812,"wires":[["ee52080d.3d7198"]]},{"id":"ee52080d.3d7198","type":"function","z":"9ded7b0.a4c6b88","name":"for each item(名詞)","func":"var arr;\n\nif (msg.payload !== undefined && msg.payload !== null){ \n    msg.payload.forEach(function(js){\n        if (js.pos == \"名詞\" && js.reading != null && js.surface_form.length > 1){\n            if (js.reading.length > 1)context.global.data.push(js);\n        } \n//        if (js.pos == \"人名\" && js.reading != null) context.global.data.push(js);\n//        if (js.pos == \"形容詞語幹\" && js.reading != null) context.global.data.push(js);\n    });\n}\n\n\nmsg.payload = null;\nmsg.nextitem = context.global.data[ context.global.i ];\n\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":197.00003051757812,"wires":[["54a9fde9.8d5084","e7492612.75b088"]]},{"id":"54a9fde9.8d5084","type":"function","z":"9ded7b0.a4c6b88","name":"++","func":"if ( (context.global.i += 1) < context.global.data.length ) return msg;\n\n","outputs":1,"noerr":0,"x":373,"y":273.0000305175781,"wires":[["ee52080d.3d7198"]]},{"id":"4250f1fc.04845","type":"function","z":"9ded7b0.a4c6b88","name":"prep data-array and i","func":"\nif( context.global.data === undefined ) context.global.data = new Array();\nif( context.global.i === undefined ) context.global.i = 0;\n\nreturn msg;","outputs":1,"noerr":0,"x":724,"y":120.0000114440918,"wires":[["75699701.f69538"]]},{"id":"633aad1b.8a2884","type":"http request","z":"9ded7b0.a4c6b88","name":"","method":"GET","ret":"txt","url":"","tls":"","x":534,"y":120.0000114440918,"wires":[["4250f1fc.04845"]]},{"id":"1a91f079.df673","type":"function","z":"9ded7b0.a4c6b88","name":"set url & method","func":"var url = \"http://hogehoge.jp/\";\nvar method = \"GET\";\n\n\nmsg.url = url;\nmsg.method = method;\n\n\nreturn msg;","outputs":1,"noerr":0,"x":344,"y":120.0000114440918,"wires":[["633aad1b.8a2884"]]},{"id":"cc50ba57.312838","type":"inject","z":"9ded7b0.a4c6b88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":184,"y":120.0000114440918,"wires":[["1a91f079.df673"]]},{"id":"b4a52232.35be3","type":"mqlight out","z":"9ded7b0.a4c6b88","name":"Put msg into MQ \"word_tokenize_list\"","service":"MQ Light","topic":"word_tokenize_list","x":800,"y":197.00003051757812,"wires":[]},{"id":"e7492612.75b088","type":"function","z":"9ded7b0.a4c6b88","name":"format","func":"\nif( msg.nextitem != undefined )\n{\n    msg.payload = {\n        \"pos\" : msg.nextitem.pos,\n        \"text\" : msg.nextitem.surface_form,\n        \"yomi\" : msg.nextitem.reading\n    }\n\n    return msg;\n}","outputs":1,"noerr":0,"x":580,"y":197.00003051757812,"wires":[["b4a52232.35be3"]]},{"id":"5da709f4.8cc6b8","type":"link in","z":"9ded7b0.a4c6b88","name":"from validated URL list","links":["488d809f.bff0b"],"x":379,"y":60.0000114440918,"wires":[["633aad1b.8a2884"]]}]
```

##名詞セーバー
見たまんまですね。全然ダメダメなんですが、まぁある程度データ取得できたので良しとしてます。その内改善。***Done is better than nothing***ですね。（誰だっけな。）

![Saver.JPG](https://qiita-image-store.s3.amazonaws.com/0/116534/3d71880d-01a2-4fed-553f-f6957207790f.jpeg)

```json:Saver
[{"id":"45a7de82.e1534","type":"function","z":"9ded7b0.a4c6b88","name":"set search keys","func":"msg._payload = msg.payload;\nmsg.payload = { \n    \"query\": \"yomi:\" + msg.payload.yomi,\n    \"limit\": 1\n}\nreturn msg;","outputs":1,"noerr":0,"x":560,"y":460,"wires":[["ebba1865.23f738"]]},{"id":"ebba1865.23f738","type":"cloudant in","z":"9ded7b0.a4c6b88","name":"find value","cloudant":"","database":"shiritori_dev","service":"cloudantNoSQLDB","search":"_idx_","design":"newDesignDoc","index":"yomiSearch","x":720,"y":460,"wires":[["8371c3d7.14fc7"]]},{"id":"8371c3d7.14fc7","type":"switch","z":"9ded7b0.a4c6b88","name":"if data not exist","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"","vt":"str"},{"t":"else"}],"checkall":"true","outputs":2,"x":320,"y":580,"wires":[["7277a5e1.02c8cc"],[]]},{"id":"e5cb395b.90c008","type":"cloudant out","z":"9ded7b0.a4c6b88","name":"insert value","cloudant":"","database":"shiritori_dev","service":"cloudantNoSQLDB","payonly":true,"operation":"insert","x":710,"y":580,"wires":[]},{"id":"7277a5e1.02c8cc","type":"function","z":"9ded7b0.a4c6b88","name":"restore _payload","func":"msg.payload = msg._payload;\nreturn msg;","outputs":1,"noerr":0,"x":530,"y":580,"wires":[["e5cb395b.90c008"]]},{"id":"cc2ace44.3329d","type":"mqlight in","z":"9ded7b0.a4c6b88","name":"Get msg from MQ","service":"MQ Light","topic":"word_tokenize_list","share":"","x":190,"y":460,"wires":[["49256a12.e04dc4"]]},{"id":"49256a12.e04dc4","type":"delay","z":"9ded7b0.a4c6b88","name":"","pauseType":"rate","timeout":"100","timeoutUnits":"milliseconds","rate":"10","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":380,"y":460,"wires":[["45a7de82.e1534"]]}]
```



#しりとりロジック

##構成
まずLineサーバーからのメッセージのハンドリング。
![LineMsgHandler.JPG](https://qiita-image-store.s3.amazonaws.com/0/116534/ac29371e-c6a7-9f90-941f-29070638f02b.jpeg)
[参考：Bluemix上のNode-redでLine botのValidationを行うTips](http://qiita.com/kouichirou/items/d0a973433d52bf0cb828)


結構昔に作ったので、ぐるぐるの所が大量メッセージの際にちゃんと動くか不安です。
ただ、どうやって大量アクセスのテストできるんだろう。ユーザー増やせばいいのか。。


そして、しりとりの処理は「メッセージ受信イベント」の中に書いてあります。

大きな枠としては以下のようになっています。
1. セッション管理。userIDやGroupID等を使って、アプリケーションとしてのセッションを管理します。
2. コマンド判別。セッションが無い場合、入力された文字列によってコマンドを受け付けます。例えば「しりとり」でしりとり開始する、みたいな。
3. しりとりロジック。しりとりで必要な判断を行います。
4. メッセージ送信。LINEサーバーのメッセージを送り返します。


###データ構造
しりとりでは、以下のデータを保存したり操作しています。

 - ターン（今どっちが回答すべきなのか）
 - LW：前回の単語
 - LFC：前回の単語の頭文字
 - LEC：前回の単語の尻文字（最後ってことね）
 - CW：今回の単語
 - CFC：今回の単語の頭文字
 - CEC：今回の単語の尻文字

###流れ

１．セッションがあれば、その中のデータをLW、LFC、LECにストア。
２．ターン情報をユーザーにセット。
３．新規単語について、CWにストア。
４．CWをTOkenizeしてCFC、CECを取得。ここから以下判定を順次実施する

| 判定項目 |　ユーザーで不可だった場合 | システムで不可だった場合 |
|:-:|:-:|:-:|
| CFC、CECの取得可？ | （不可）再入力依頼  | （不可）返答データ再検索 |
|前回の情報無し？| （無し） 尻文字判定に進む |（無し）尻文字判定に進む |
| LEC == CFC ？（頭文字判定）|  （異なる）前回の続きでない。再入力依頼 | （異なる）返答データ再検索  |
| CEC != "ん"（尻文字判定）|  （ん！！）システム勝利宣言 | （ん！！）返答データ再検索  |
|新規単語？|重複。再入力依頼|返答データ再検索|


５．上記をすべてパスした場合、ユーザーターンであれば、CW、CFC、CECをLW、LFC、LECにコピーして、ターンをシステムにセット。システムのターンであれば、今回のCW、CFC、CECをセッションに保存し、ユーザーに単語を応答して終了。
６．（システムの場合）単語の検索を実施。
７．単語の検索で、候補が１つも残っていない場合はシステムの負けとして応答し、セッションを削除して終了。
８．単語の検索結果について、読み情報が入っていない場合は返答データ再検索。入っていれば、CWにテキスト情報をCWにセットして４．の実施。

ざっくりこんな感じです。
![Shiritori.JPG](https://qiita-image-store.s3.amazonaws.com/0/116534/e6101336-2e34-52f4-f17b-2bb9cde90e7a.jpeg)
（小さくて文字潰れてますが、日が暮れそうなのでこの位で。。。）


###忘れてた
上記の１．の処理の前に、「やめる」という文字の場合はセッションけして終了する処理と、セッションに保存されているタイムスタンプが90秒以上前だとタイムアウトする処理を入れています。

##細かいTips解説

###DBの検索結果が想定外
Cloudantに履歴データ入れる際に、LINEのUserID＋テキストでIndex作ったのですが、その際にテキストは日本語だからとJapaneseとして作成したら、曖昧な検索になってしまって全然違うデータが帰ってきた。
IndexをStanderdで再作成したら概ね解決したが、たまに変なデータが帰ってくる時もあるので、上記の流れ８．の箇所で正しくデータが取れたかを確認している。

###形態素解析が理解出来ないカタカナだけ文字
使っている形態素解析（Kuromoji）の性能なのか、形態素解析なんてそんなもんなのかわからないんですが、カタカナだけのテキストで辞書になさそうな言葉を入力すると、ちゃんと欲しいデータが取れない。
なので、そういう場合はユーザーに再入力させる事にした。
この点については、Mecabあたりを使ったり、ユーザー辞書追加で解決するんだろうか？

###ー（棒線）
例えば、「カレー」という場合は、通常「レ」とするじゃないですか。
なので、機械的に最後の文字を取ってこようとするとエラーになるので４．の処理で後ろから棒線の連続は無視するようにしている。（下記`post_tokenizer`参照）

###記号
ユーザーが記号を入力してきても、どうしたら良いかわかんないですよね。
なので、それらも文字として扱わない様にしています。（下記`post_tokenizer`参照）

###小さい奴ら（ぁ、ぃ、ぅ、ぇ、ぉ、ゃ、ゅ、ょ）
「根拠」なんかは、「こんきょ」になるんですが、人間様は「ょ」を「よ」として当たり前のように処理するんですが機械様はそんな事出来ないので、大きくしてあげてます。（下記`post_tokenizer`参照）
この対応については、post_tokenizer以外にもシステムが返答用の単語を返す際の頭文字を用意する際にも実施しています。頭が「ょ」で始まる名詞は普通無いですからね。。ょょょ。


```js:post_tokenizer

var curFirstWordChunk = msg.payload[0].reading;
var curFirstWordChunk;
var curLastWordChunk;
var i = msg.payload.length - 1;
var j;
var curFirstChar;
var curEndChar;

if (curFirstWordChunk == null || curFirstWordChunk == undefined){
    var char = msg.payload[0].surface_form;

    if (char == "ァ") char = "ア";
    if (char == "ィ") char = "イ";
    if (char == "ゥ") char = "ウ";
    if (char == "ェ") char = "エ";
    if (char == "ォ") char = "オ";
    if (char == "ッ") char = "ツ";
    if (char == "ャ") char = "ヤ";
    if (char == "ュ") char = "ユ";
    if (char == "ョ") char = "ヨ";

    curFirstWordChunk = char;
}

//curFirstWordChunk = msg.payload[0].reading;

while (true){
    if (msg.payload[i].reading != null && msg.payload[i].reading != undefined  && msg.payload[i].pos != "記号"){
        break;    
    } else {
        if ( i === 0  ){
            // Wrong case. only 記号等
            msg.payload = msg.payload2;
            return [msg , null];
        }
        i--;
    }
}
curFirstChar = curFirstWordChunk.charAt(0);

curLastWordChunk = msg.payload[i].reading;
j = curLastWordChunk.length - 1;
while ( true ){
    curEndChar = curLastWordChunk.charAt( j );
    if (curEndChar != "ー"){
        break;
    }else{
        if ( j === 0  ){
            // Wrong case. only 記号等
            msg.payload = msg.payload2;
            return [msg , null];
        }
        j--;
    }
}

var char = curEndChar;

    if (char == "ァ") char = "ア";
    if (char == "ィ") char = "イ";
    if (char == "ゥ") char = "ウ";
    if (char == "ェ") char = "エ";
    if (char == "ォ") char = "オ";
    if (char == "ッ") char = "ツ";
    if (char == "ャ") char = "ヤ";
    if (char == "ュ") char = "ユ";
    if (char == "ョ") char = "ヨ";

curEndChar = char;

msg.payload = msg.payload2;
//msg._shiritoriCurWord = msg.payload.message.text;
msg._shiritoriCurFirstChar = curFirstChar;
msg._shiritoriCurEndChar = curEndChar;


return [null , msg];

```

###セッションをユニークにするためのIDは何を使うか？
LINEは、ユーザー、グループ、トークルームと３つの種類の会話があるので、それぞれでユニークIDの変数が異なります。なので以下の処理で、uniqidという変数に値を突っ込んでそれを使うようにしました。

```js
if (msg.payload !== null  && msg.payload !== undefined){
    if (msg.payload.source.type == "user"){
        msg.payload.uniqid = msg.payload.source.userId;
    }else if (msg.payload.source.type == "room"){
        msg.payload.uniqid = msg.payload.source.roomId;
    }else if (msg.payload.source.type == "group"){
        msg.payload.uniqid = msg.payload.source.groupId;
    }
}
```

##QRコード再掲です。
Shiritoriボット君です。倒してみて下さい。「しりとり」でしりとり開始します。
![Shiritori](http://qr-official.line.me/L/3jbpw2SXV1.png)
<a href="https://line.me/R/ti/p/%40eqe0984y"><img height="36" border="0" alt="友だち追加" src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png"></a>

#所感・やり残し

 - 人間様はすごいな。たかがしりとりですが、意外と大変でした。
 - Mecabの導入したらどう変わるか見てみたい。
 - スケールアウトすると考えたら何が課題となるか考えたい。
 - 普通にクローラってどの位のアクセスさせるもんなんだろ。並列化やってみたい。
 - そもそもmsg上書き問題って、ホントにあるのか。気のせいだったりしないよね。
 - チャットボットってUIのコトを殆ど考えなくてイイから楽ちん。フロント側はやれなくはないけど無駄に時間かかるからな。。
 - 任意のサイトを指定して、その中で使われている単語だけでやってみたら面白いかな。陰謀論のサイトとか、あとは、アダ。。(ry
 - システムが返した結果について、フィードバック出来るような仕組もあったらイイかな。違うだろツッコミが来たら、後で人力チェックするとか、優先度下げるとか。確かLineのメッセージ形式でボタンつけれた筈だから、それで対応可能だと思われる。



#以上です。
お読み頂きありがとうございました。
楽しかったです。
何かあればコメント頂けたら幸いです。