【LINE BOT】GAS×レーベンシュタイン距離を使っていい感じに返信してくれるBOTを作った

#あいさつ
Qiita初投稿です。
GoogleAppsScript×レーベンシュタイン距離を使っていい感じに返信してくれる(AIとかではない)LINEBOTを作ったので作り方を公開します。


#必要な環境
1.Googleアカウント(たぶんみんな持ってる)

2.LINE developersアカウント(10分あれば作れます)

3.PC(Windows/Macどちらでも)

4.気合い

###作業時間
2時間
**※大枠の実装1時間 + 返信用のデータ登録1時間くらい**
この記事で紹介するものと全く同じものを作るだけなら、30分もあれば作成可能だと思います。


#いい感じに返信してくれるBOTってなに？？　−−ロジックを思いつくまで
諸事情で、友人とのラインのログをテキストに落としていろいろ作業しているときに思いつきました。

**「2年分のLINEの発言のログをデータとして格納してやって、その手札を使えばユーザーの発言に対していい感じの返答を返すことができるbotが作れるのでは？？」**

って感じです。

###さらに詳しく
友人をA、僕をBとします。

| Aの発言                 | Bの返答　　　　　　 | 
|:-----------------------|:----------|
| ラーメン食べたい          |わかる       |
| でんわしよ               |いいよ       | 
| 男の人は一生中二病だと思う  |それはある    | 

こんな感じのなんでもない会話を記録しておいて、LINEBOTに向かって投げられた発言(strとします)と、格納されているAをすべて比べて、strと最も近いAを特定します。

特定したAに紐づくBを、LINEBOTから返してやればいい感じに返事が帰って来たふうに見えるのでは、という感じです。

上記の例では、3会話しかありませんが、実際は2年分の会話のすべてを格納します**(テキストで2万行くらいでした)**
→下記コードでは一部抜粋して格納しています

####LINEBOTに投げられた発言と最も近いAの特定方法
これはだいぶ迷いましたが、~~機械学習とかAIを実装できる気はしなかったので、~~レーベンシュタイン距離という考え方を使うことにしました。

>レーベンシュタイン距離は、二つの文字列がどの程度異なっているかを示す距離の一種である。編集距離とも呼ばれる。具体的には、1文字の挿入・削除・置換によって、一方の文字列をもう一方の文字列に変形するのに必要な手順の最小回数として定義される。　[レーベンシュタイン距離](https://ja.wikipedia.org/wiki/%E3%83%AC%E3%83%BC%E3%83%99%E3%83%B3%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%A4%E3%83%B3%E8%B7%9D%E9%9B%A2)

要は、「文字を何回書き換え(挿入・削除・置換)れば、目的の文字列になるか」って感じです。

#実装
BOTを作るための諸々の手続き、どこにコーディングすんの？　みたいなのは~~サボり~~省略します。
[【LINE Botの作り方】Messaging API × GAS(Google Apps Script)でおうむ返しボットを作成する｜TAKEIHO](https://www.takeiho.com/messaging-api-gas)

**こちらの方のブログを参考にしました。非常にわかりやすくておすすめです。**

リンクを参考にGoogleAppsScriptを書くぞ！　って部分まで環境を整えてからこの下を読むといいかもしれないです。


```javascript:コード.gs
// xxx部分をChannel Access Token => ISSUE で発行された文字列に置き換える
var channel_access_token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 

function doPost(e) {
  var posted_json = JSON.parse(e.postData.contents);
  var events = posted_json.events;
   //送られたLINEメッセージを取得
  var json = JSON.parse(e.postData.contents);
  var user_message = json.events[0].message.text;  

  //送られて来た種類
  var receive_message_type = json.events[0].message.type;
  events.forEach(function(event) {
   //発言A　ArrayMessage[i]とArrayResponse[i]は対になるように登録してね
    var ArrayMessage = ['明日が本番だから','酔ってると八方美人やめれる','焼きあごラーメン','秋の四角形みたいなやつが同時に見えた','星めっちゃ見た','なにたべるかまじでまよう','とりあえずこたつ買って電車乗っとこ','ちょっと盛った','夏終わる','しおり作りたい','くそかわいい','東京湿度高すぎ','帰ってる','後でまた連絡するね','まじ寝坊しないか不安','7時集合','美味しすぎた','たのしい','とおくね？','1分くらい待って','でんわしよ','いま家？','どへんたいだな','気が向いたらもってくる','でんぱくそわるい','こんどいこ','秋の四角形みたいなやつが同時に見えた'];

   //発言B  発言Aと発言Bは好きな会話を入れてね。ここにあるのは筆者と筆者の友人の会話の一部だよ
    var ArrayResponse = ['気合い入れないと','常に酔ってれば勝てる','たべたい','秋の四辺形わかるの強すぎ','羨ましすぎ','ラーメンが肉か定めよう','そして夜行バス','盛りすぎ','夏らしいことやってないわ','旅行の時しおり作る派だわ','かわいい','まじで暑い','おつ','まってる','モニコするわ','早い','うらやま','いいなあ','遠いわ','まつわ','おけまる水産','家なう','ばれた','#持ってこなそう','無線LAN繋がらなさそう','いこ','秋の四辺形わかるの強すぎ'];

    　//レーベンシュタイン距離 
    //ユーザーが入力したテキストと、ArrayMessageのレーベンシュタイン距離を比較。最も小さいものの返答を出力
    var minlevenshtein  = levenshtein(user_message, ArrayMessage[0]);
    var min_i = 0;

    for(var i=1;i<ArrayMessage.length;i++){
      if (levenshtein(user_message, ArrayMessage[i]) < minlevenshtein){
        minlevenshtein = levenshtein(user_message, ArrayMessage[i]); //最小のレーベンシュタイン距離更新
        min_i = i;
      }
    }
     var postData = {
      "replyToken" :event.replyToken,
      "messages" : [
        {
          "type" : "text",
          "text" :  ArrayResponse[min_i]
                         }
      ]
    };
    var options = {
      "method" : "post",
      "headers" : {
        "Content-Type" : "application/json",
        "Authorization" : "Bearer " + channel_access_token
      },
      "payload" : JSON.stringify(postData)
    };
    var reply = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
  });
};

//レーベンシュタイン距離
function levenshtein (s1, s2) {
    // http://kevin.vanzonneveld.net
    // +            original by: Carlos R. L. Rodrigues (http://www.jsfromhell.com)
    // +            bugfixed by: Onno Marsman
    // +             revised by: Andrea Giammarchi (http://webreflection.blogspot.com)
    // + reimplemented by: Brett Zamir (http://brett-zamir.me)
    // + reimplemented by: Alexander M Beedie
    // *                example 1: levenshtein('Kevin van Zonneveld', 'Kevin van Sommeveld');
    // *                returns 1: 3

    if (s1 == s2) {
        return 0;
    }

    var s1_len = s1.length;
    var s2_len = s2.length;
    if (s1_len === 0) {
      return s2_len;
    }

    if (s2_len === 0) {
        return s1_len;
    }

    // BEGIN STATIC
    var split = false;
    try{
        split=!('0')[0];
    } catch (e){
        split=true; // Earlier IE may not support access by string index
    }

    // END STATIC
    if (split){
        s1 = s1.split('');
        s2 = s2.split('');
    }

    var v0 = new Array(s1_len+1);
    var v1 = new Array(s1_len+1);
    var s1_idx=0, s2_idx=0, cost=0;

    for (s1_idx=0; s1_idx<s1_len+1; s1_idx++) {
       v0[s1_idx] = s1_idx;
    }

    var char_s1='', char_s2='';
    for (s2_idx=1; s2_idx<=s2_len; s2_idx++) {
        v1[0] = s2_idx;
        char_s2 = s2[s2_idx - 1];

        for (s1_idx=0; s1_idx<s1_len;s1_idx++) {
            char_s1 = s1[s1_idx];
            cost = (char_s1 == char_s2) ? 0 : 1;
            var m_min = v0[s1_idx+1] + 1;
            var b = v1[s1_idx] + 1;
            var c = v0[s1_idx] + cost;

            if (b < m_min) {
                m_min = b; }
            if (c < m_min) {
               m_min = c; }
            v1[s1_idx+1] = m_min;
        }

        var v_tmp = v0;
        v0 = v1;
        v1 = v_tmp;
    }

    return v0[s1_len];

}
```

**上記のコードのうち、2行目のトークンを書き換えてください。最低限それで動きます。**

ソース上の方にある鬼みたいな配列は、発言Aと発言Bです。最終的には6000アイテムくらい入れる予定なので、配列の要素は外付けで持つように拡張する予定です(このままベタで書いてもソースが汚いだけで普通に動きます)

なお、スタンプと画像が送られて来た際の処理はこのコードでは未実装です。~~(テキスト以外が送られた処理は非常に面倒っぽかったので)~~

#LINEBOTと会話しよう
<img width="431" alt="20180820214931.png" src="https://qiita-image-store.s3.amazonaws.com/0/306205/99e1b304-2af6-1b5a-bbeb-3dc8de08bc3a.png">
格納しているデータが少ないこともあり、怪しい箇所もありますがまあまあいい感じに返信してくれてます(返信しやすいようなメッセージを送ったのもありますが)

ちなみに、アイコンがキズナアイちゃんなのは**「AI返信したかった(技術が足りなくてできなかった)」**という悲しい事情からです。 

##まとめ
機械学習もAIも使わずにレーベンシュタイン距離でいい感じの返答ができるLINEBOTを作ることができました。

LINEでの発言をLINEBOTに使うって、なんかエモさがあって楽しかったです。

**最後に、今回作成したアカウントを載せておくのでよかったら友達登録してね！！**

![20180820215316.jpg](https://qiita-image-store.s3.amazonaws.com/0/306205/a0745b4b-76fd-4c11-3e2f-5224ce359a2a.jpeg)
![20180820214935.png](https://qiita-image-store.s3.amazonaws.com/0/306205/16a5b34c-b757-3e8a-e6ed-fb2f67f2f511.png)
→このLINEBOTでは上記機能に加えて、画像を送ると文字起こししてくれる機能を追加してます！！