LINE Messaging API＋Ngrok＋Python　で実験的にLINEボットを開発してみよう　Part3会話盛りだくさん編

# 0　おさらい
[Part2](https://qiita.com/Antareskkudo/items/4d70c6cc0f8dfd18e82b)では[Part1](https://qiita.com/Antareskkudo/items/bedd28326f57a5d80092)で作成したプログラムを実行する手順についてお話しし、簡単な会話をするプログラムを作成しました。また、会話をさらに拡大するためのアプローチ方法についても探りました。
今回はさらに会話のレパートリーを増やしより自然な会話に近づけていきます。また、外部プログラムを使った例を紹介し応用についても説明していきます。

# 1　CSVファイルの編集
今回はPart1で作成したディレクトリ構成を元に説明していきます。まだ作成されていない方はPart1を参照して作成してみてください。
最初はメッセージとそれに返信する返信メッセージのパターンを、表形式のファイルに作成します。では前回同様にRaspberry Piを操作できるようにVscodeを開いてリモートアクセスしましょう。

アクセスできたらdataフォルダ内のactiondata.csvを開いてください。
![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/bd356d87-ffdb-1941-52b7-07e08432ae87.png)
今回はVscodeの拡張機能[^1]である**Edit CSV**を使用して編集していきます。この拡張機能にはcsvファイルを編集するのに便利な機能が含まれています。
開くと下のような画面になります。 
![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/4a1f965d-e73a-adf1-bf3d-541a34229ae3.png)
拡張機能がインストールされていれば右上に"Edit CSV"というボタンが現れます。<ins>このボタンを押す前にactiondata.csvに一つで構いませんので「,（カンマ）」を入力して下さい。</ins> 拡張機能の仕様なのかはわかりませんが、空のcsvファイルをこの機能で編集しようとするとうまくいきません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/648b9a13-29e0-a26e-868e-b5c9005e9f03.png)

カンマが入力できたら、保存して右上のEdit CSVをクリックします。すると、下の写真のように一行二列のセルが表示されます。
![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/a5643c7b-52e3-64ed-6dfb-6bc2d4c5d792.png)
この状態でファイルを操作していきます。上の「Add row」では行、「Add column」では列をそれぞれ追加することができます。今回は以下のようにcsvを構成しました。
![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/4d0f734c-79f3-e7b1-b007-674f562c0a93.png)
左から質問内容、その次に応答、最後にコマンド番号です。質問内容はユーザーが投げかけてくる質問（受信メッセージ）です。応答はLINEbotが返信するメッセージを指します。コマンド番号については質問に対して何かしらのコマンドを実行するための識別子です。例えば「今のTwitterのトレンドは？」という質問をした際に、pythonのTwitter APIプログラムを実行する場合など別のプログラム、プラットフォームや外部センサーなどを操作する際に使用するものです（詳細は３章の応用）。
では、ここからは自由に質問とそれに対する応答を記入していきます。Add rowでいくつか行を追加して質問内容、応答、コマンド番号をそれぞれ記入していきます。この際に**同じ行の質問内容、応答、コマンド番号が対応する**ようにしてください。例えば、下図のように質問内容に「おはよう」と書き込んだら、同じ行の応答にはそれに対する応答を書きます。ここでは「おはようございます。」と書きました。さらに合わせてコマンドを操作したい場合はコマンド番号に適切な番号を入力します。今回は全て０で構いません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/5a08ef2b-6dd1-82e7-bb39-2ad3d6201d4a.png)
最終的には下図のようにしました。
![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/8355cfe7-aa0c-13e5-9fb7-4612816365af.png)
書き込みが終わったら上にある「Apply changes to file and save」を押して元のcsvファイルに適用し保存します。<ins>ファイルが保存できたかどうかの確認は、上のファイルタブ（CSV edit actiondata.csvと書いてあるところ）で確認できます。左上にある*マークが消えている場合は保存できています。</ins>保存できたら元のcsvファイルに戻ります。そして中身が入っていれば大丈夫です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/f82757b4-9de3-236b-8b76-8f9d472fcc5c.png)
![08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/b25952af-5213-82a4-78dd-dd0289a6fbc3.png)
これでcsvファイルは編集完了です。会話をさらに広げたい場合は、プログラムを変えずにこのcsvファイルを追記するだけです！

# 2　コーディング
では前回のプログラムを、編集したcsvファイルが使用されるように変更します。

## 2.1　準備
プログラムを書き換える前に必要なpythonライブラリをインストールしましょう！
今回インストールするライブラリは**pandas**です。pandasライブラリはデータ分析作業を支援してくれる機能が含まれており、配列などをシリーズやデータフレームとして扱うことができます。今回は先ほど作成したcsvファイルを取り込み、pandasライブラリで２次元配列に変換して扱うために使います[^2]。
インストール方法は以下の通りです：
```
pip install pandas
```
または
```
pip3 install pandas
```
これでOKです。その他にも時間を扱うプログラムであれば<code>datetime</code>、乱数を扱いたい場合は<code>random</code>などプログラムの使用用途に合わせてライブラリをインストールしてください。インストール方法は上と同じです。
```
pip install インストールしたいライブラリ名
```
または
```
pip3 install インストールしたいライブラリ名
```

## 2.2　プログラム変更結果
では、実際に書いていきましょう！前回のプログラムより主に変える部分はこのselectwords関数です。
```python:selectwords
#変更前（前回）
def selectwords(commandtext):  # 対応する言葉を選択
    if commandtext == "おはよう":
        reply = "おはようございます！"
    elif commandtext == "こんにちは":
        reply = "こんにちは"
    elif commandtext == "こんばんは":
        reply = "こんばんは"
    else:
        reply = "分からない単語です。"

    return reply
```
変更後のプログラムを先に見せます、変更後は下のようになります。**＊プログラム冒頭に先ほどインストールしたpandasライブラリをインポートすることを忘れないでください。**
```python:selectwords
import pandas as pd
"""
中略
"""
#変更後のselectwords関数
def selectwords(commandtext):
    actiondatafile = "LINEbot/data/actiondata.csv"
    actionData = pd.read_csv(actiondatafile)
    for i,value in actionData.iterrows():
        if commandtext == value[0]:
            reply = value[1]
            break
    else:
        reply = "分からない単語です。"
    return reply
```
## 2.3　プログラム解説
プログラムの解説をしていきます。少し見づらくなりますが細分化して説明します。
### 2.3.1　csv読み込み＆データフレーム化
まず``actiondatafile``では先ほど作成したcsvファイルの場所を示しています。part1のディレクトリ構成からそのまま作成している方は上のパスで使えます。
続いて`actionData = pd.read_csv(actiondatafile)`ではcsvファイルを開き、その中身を２次元配列として読み込みます[^3]。
### 2.3.2　ループ動作
次にfor文ですが、forは簡単に言うと`actionData`の中身を上から順に見ていく動作をしています。
ここで少しpandasライブラリのデータフレームについてお話します。通常、pandas.DataFramを単純にfor文に入れて実行すると列名のみが得られます。しかし、**今回必要なのは１行ずつの全ての列情報です**。そこで、pandasライブラリの機能である、**``DataFrame.iterrows()``メソッド**[^4]を使っていきます。このメソッドによりデータを１行ずつ取り出すことができます。forループの動きと変数の扱いは下図のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/9745f392-14cf-7d06-d6f0-d4c63f722471.png)
forには２つの変数``i``と``value``があります。iはラベル（index）を示しており、行数情報を持っています。実際には、この行数はデータフレーム（csv）とは異なり、この場合２行目が０となります。iは整数型（int）です。valueについては今いるループで得られた列情報すべてが含まれています。valueはシリーズ型で、添え字で取得したい列を選択できます。例えば、`value[0]`であれば一番左の列[^5]である「質問内容」が得られます。同様に`value[1]`であれば応答というようになります。valueの添え字でどのようなデータを扱っているかを正確に把握しましょう。
### 2.3.3　ループの中身
では、forの中身の解説をしていきます。``if commandtext == value[0]:``では引数commandtext、とvalue[0]を比較して同じであればifの中身を実行するというものです。前回、前々回と説明したようにcommandtext変数はユーザーから送られてくる**テキストメッセージ**です。ifの中身は``reply = value[1]``です。reply変数も前回同様にbotが送信するメッセージそのものです。
つまり、**botが受信したメッセージとcsvファイルの質問内容とを比較して同じであれば、その同じ行の応答列の情報をbotが返信するという事です**。そして、replyに変数を代入し終えたらこのループは用済みとなり、ループ外に出る動作``break``が実行されます[^6]。
### 2.3.4　for else構文
このプログラムではforの後にelseが来ます。これは**for else構文**[^7]を用いたものです。<ins>for else構文ではforの中に必ず``break``が存在します。</ins>プログラムを少し勉強された方で、breakが実行されると今度はforを抜けてelseの中身が実行されるのではないかと思われるかもしれませんが、それは間違いです。この場合には、**elseが実行されるのはforの中の条件が一回も合致しなかったときです**。もっと言うと、**ループが回り切った時に実行されるということです。**
ではelseブロックの中身を見ていきましょう。elseブロックには
```python
reply = "分からない単語です。"
```
とあります。これは前回のプログラムにもあった、**作成した条件分岐(if)が当てはまらなかったときに実行されるプログラム**でした。今回も意味合いは同じで、**csvファイルに記載のない質問内容であった時に実行されるプログラム**です。

プログラムの操作としてはこれで終わりです。後は変数``reply``に入った返信メッセージを返り値として返してあげれば、適切な応答がユーザーに送信されます。ここまでのプログラム全体を下に示します。
```python:app.py
import config   #環境変数のインポート
from flask import Flask, request, abort
import pandas as pd

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage
)
app = Flask(__name__)

line_bot_api = LineBotApi(config.LINE_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(config.LINE_CHANNEL_SECRET)

@app.route("/callback", methods=['POST'])
def callback():

    signature = request.headers['X-Line-Signature']

    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        print("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return 'OK'

@handler.add(MessageEvent, message=TextMessage)#メッセージが送られた際の操作
def handle_message(event):
    if event.reply_token == "00000000000000000000000000000000":
        return
    req  = request.json["events"][0]
    userMessage = req["message"]["text"]


    replymessage = selectwords(userMessage)

    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=replymessage)
    )

#変更後のselectwords関数
def selectwords(commandtext):
    actiondatafile = "LINEbot/data/actiondata.csv"
    actionData = pd.read_csv(actiondatafile)
    for i,value in actionData.iterrows():
        if commandtext == value[0]:
            reply = value[1]
            break
    else:
        reply = "分からない単語です。"
    return reply

if __name__ == "__main__":#最後に置かないと関数エラーが出るので注意！
    app.run(host="localhost", port=8080) # ポート番号を8080に指定
```

# 3　応用
では最後に少し応用について説明していきます。
csvファイルには質問内容や応答の他にコマンド番号がありました。このコマンド番号を操作していきます。
例えば、筆者が今操作しているRaspberry PiはIoT要素を取り込んでいます。Raspberry Pi上には部屋の照明を操作できるような外部マイコンが取り付けられています。そして、その実行プログラムを``denki.py``としています[^8]。同じ環境でなくても外部プログラムの操作の説明も含んでいますので、参考になるところを読んでいただけたら幸いです。

今回はこのpythonプログラムを、作成しているLINEbotで動かす例を紹介します。
denki.pyの中身は割愛しますが<ins>照明をオンにできる関数がon()で、照明をオフにできる関数がoff()</ins>となっています。
**ディレクトリ構成にはapp.pyと同じ階層にdenki.pyを追加しているので注意してください！**
>pi/
 └ LINEbot/
 │    └ config.py（環境変数、アクセストークンなど）
 │    └app.py（実際に操作する内容）
 │    └**denki.py（新たに追加した照明をオンオフするプログラム）**
 │    └data/
 │    │ └ actiondata.csv（操作内容や会話パターンなど）

## 3.1　csv変更
初めにcsvの中身を変更しましょう。いつものようにEdit csvボタンで編集画面に行きます。Add rowボタンで２行分追加します。そしたら追加した行に上から「照明オン」、「照明オフ」と入力します。次に、真ん中の応答列には動作を知らせるメッセージ「照明がオンになりました」と「照明がオフになりました」を入力します。最後に、追加した行のコマンド番号列に上から「1」、「2」と入力し適用・保存をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/d54f89bc-d275-03f3-5234-6634399cc84e.png)

## 3.2　プログラム変更&解説
ではプログラムを変更していきます。今回も例によって``selectwords``関数に追記します。加えて、今回は外部プログラムも使用するのでその外部プログラムのインポートもします。
for文ではifの中にさらに条件分岐を増やします。変更後のプログラムを下に示します。
```python:selectwords
import denki #外部プログラム
"""
中略
"""
#selectwords関数
def selectwords(commandtext):
    actiondatafile = "LINEbot/data/actiondata.csv"
    actionData = pd.read_csv(actiondatafile)
    for i,value in actionData.iterrows():
        if commandtext == value[0]:
            reply = value[1]
            ope = value[2]
            if ope == 1:
                denki.on()
            elif ope == 2:
                denki.off()
            break
    else:
        reply = "分からない単語です。"
    return reply
```
for文の中に新しく``ope``という変数を追加しています。これはコマンド番号情報を入れるためのものです。

上から順に見ていきましょう。
まずはforで送られてきたメッセージとcsvファイルにある質問内容が合致する情報を探します。発見できたら最初のifに入り、replyに返答が代入されます。ここまでは先ほどと同じです。
そして新しく追加された``ope = value[2]``で今のループ、つまり現在見ている行のコマンド番号列情報が格納されます。このopeが１（照明オン）の時は、外部プログラムdenki.pyの``on()``関数が実行されます。つまり部屋の照明がオンになります。そして「照明がオンになりました」とユーザーに送信されます。
一方でopeが０（照明オフ）の時は、外部プログラムdenki.pyの``off()``関数が実行され、部屋の照明がオフになります。オンと同様にこちらもユーザーに「照明がオフになりました」と送信されます。
opeが0（照明オン・オフ以外）の時は、外部プログラムは何も実行されずただreply変数が返されるだけとなります。

## 3.3　プログラム全文＆実行結果
csvファイル適用＋応用を施したプログラム全文を下に示します。参考にしてください。
```python:app.py
import config   #環境変数のインポート
from flask import Flask, request, abort
import pandas as pd
import denki #外部プログラム

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage
)
app = Flask(__name__)

line_bot_api = LineBotApi(config.LINE_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(config.LINE_CHANNEL_SECRET)

@app.route("/callback", methods=['POST'])
def callback():

    signature = request.headers['X-Line-Signature']

    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        print("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return 'OK'

@handler.add(MessageEvent, message=TextMessage)#メッセージが送られた際の操作
def handle_message(event):
    if event.reply_token == "00000000000000000000000000000000":
        return
    req  = request.json["events"][0]
    userMessage = req["message"]["text"]


    replymessage = selectwords(userMessage)

    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=replymessage)
    )

#変更&応用後のselectwords関数
def selectwords(commandtext):
    actiondatafile = "LINEbot/data/actiondata.csv"
    actionData = pd.read_csv(actiondatafile)
    for i,value in actionData.iterrows():
        if commandtext == value[0]:
            reply = value[1]
            ope = value[2]
            if ope == 1:
                denki.on()
            elif ope == 2:
                denki.off()
            break
    else:
        reply = "分からない単語です。"
    return reply

if __name__ == "__main__":#最後に置かないと関数エラーが出るので注意！
    app.run(host="localhost", port=8080) # ポート番号を8080に指定
```
また、下に実行例を示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/fc127660-dd2d-7596-f6b2-547ba113d3f6.png)
きちんとコマンドが実行されていることが分かります。またその他の会話も問題なく動作していますね！

**登録していない言葉を新たに登録する場合は、先ほども説明したようにcsvファイルに追記します。この時、通常の会話（コマンドを使用しないもの）であればプログラムを停止して修正する必要はありません。**

# 4　まとめ
以上でLINEbotに関する説明はいったん終わります。いかがだったでしょうか？Part1～Part3まで３部作で書きましたが、内容としてはそこまで多くはなかったと思います（個人差有）。自分もまだまだ勉強不足なところがありますが、それでも何かしら伝えられたらと思いながら書きました。私と同じくプログラミングを勉強、楽しんでいる方の参考になっていれば幸いです。

botの会話についてはpart2に比べ、part3の方が**会話に近い**ものとはなりましたが所詮はまだ一問一答形式で、かつ登録したものと完全一致でなければ成り立ちません。会話とはそれまでの会話の履歴（脈絡）を考慮していることや、少し言葉遣いが異なっていても成り立つものです。今後は「**会話の中から単語を抽出**」して、**Word2Vec**のような「**単語をベクトル化**」する技術を用いて、より自然な会話に近づけられるように個人で開発を進めていきます。

LINEbotについてはさらに応用を利かせることができます。説明したものはメッセージだけのやり取りだけですが、画像やその他のファイル形式のやり取り、またグループチャットでの動作もできます。グループチャットでは、グループメンバーが投稿した画像をbotに保存させたり、会話を検索させたりなんかも可能です。そのような応用の原理は、基本的に今回説明したものと同じです。興味がある方はやってみることをお勧めします。もしこの記事の反響が良かったり、応用に関する質問が多い場合には超応用編を書くかもしれません。

また、応用の例で取り扱ったIoTを活用したものも他に複数あります。植物の水やりを遠隔でやったり、ちょっと凝った工夫をして勤怠管理と健康管理を同時にやらせたりなんかもできちゃいます。

最後に、記事の投稿間隔がとても長くなってしまいました。記事の投稿を待っていらしゃる方には大変申し訳ございませんでした。また、説明にもかなり雑な部分やむらが出てしまったなと反省しております。何か質問等あればお気軽に聞いてください！今回でLINEbotに関する記事はいったん終了です。<ins>今後はAndroidアプリ関係の記事を投稿する予定です（いつ投稿するかは未定です）。</ins>

# 参考資料（引用を含む）
## サイト
* [【入門用】PythonによるLINEbot作り方](https://www.tomotaku.com/line-repeat-bot) < とても参考にさせていただきました
* [Messaging APIを始めよう](https://developers.line.biz/ja/docs/messaging-api/getting-started)
* [LINE Developers](https://developers.line.biz/ja/services/messaging-api/)
* [while,forループのelse](https://python.civic-apps.com/else-loop/) 
* [PandasのIndexの理解と使い方まとめ](https://deepage.net/features/pandas-index.html) 
* [pandas.DataFrameのforループ処理（イテレーション）](https://note.nkmk.me/python-pandas-dataframe-for-iteration/) 
* [pandasでcsv/tsvファイル読み込み（read_csv, read_table）](https://note.nkmk.me/python-pandas-read-csv-tsv/) 
## 書籍
* コーディングを支える技術-成り立ちから学ぶプログラミング作法

## これまでの記事
* [Part1](https://qiita.com/Antareskkudo/items/bedd28326f57a5d80092)
* [Part2](https://qiita.com/Antareskkudo/items/4d70c6cc0f8dfd18e82b)




[^1]: 拡張機能のインストール方法について
拡張機能とはデフォルトのvscodeでは対応していない機能を、後から自分好みで入れることができるものです。尚、拡張機能を自作して公開することも可能です。拡張機能のインストール方法は左バーにある、四角のブロックの組み合わせのようなアイコンをクリックします。
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1100334/43adcad0-6457-4a32-fca9-f8d07f1b280c.png)
次に上にある”Marketplaceで拡張機能を検索する”をクリックして所望の拡張機能を検索して該当するものをクリック、インストールするのボタンを押してインストール完了です。インストール後は再読み込みが必要なこともあります。
[^2]: pandasライブラリについて
pandasライブラリはNumPyライブラリと依存性を持つライブラリです。numpyをインストールしていない場合は併せてインストールしましょう。
インストール方法は<code>pip install numpy</code>です。
環境によってはnumpyのバージョン違いによりpandasライブラリが動作しない場合があります。
[^3]: この際に`actionData`の型は`class 'pandas.core.frame.DataFrame'`となります。つまりcsvの中身をデータフレームとして扱うことができるのです。
[^4]: itterrowsメソッド以外にも同じ機能を持つ``DataFrame.itertuples()``メソッドがあります。機能は同じですが、itertuplesの方が高速です。また、今回は１行ずつの情報でしたが場合によっては**１列ずつ**の情報が欲しい場合もあると思います。その際には``DataFrame.iteritems()``メソッドを使います。
[^5]: この場合の一番左の列は質問内容ですが、これを聞いて違和感を覚える方もいらっしゃると思います。実際にはこの場合の一番左の列は１～７の数字であるインデックスです。しかしデフォルトのforループのデータフレームの操作では１行目と１列目は見られずに２行目、２列目から始まります。オプションで`index=0`などを設定してあげれば最初の行、列もforで操作できるようになりますがデータフレームを破壊する恐れがあります。
[^6]: もしこの時``break``がないとreply変数が上書きされてしまいます。一度自身の環境下でbreakなしで実行してみるとわかりやすいです。詳細は注釈7でも説明しています。
[^7]: この構文はpython以外にはあまり見られない機能の一つです。forの他にもwhile+else構文もあります。機能は全ループ処理ですべての条件が合わなかったときに、elseの中身を実行するというものです。筆者もpythonの他にC++やJava、kotlinなどを使いますがこの機能はpythonでしか見られなかったと思います。
[^8]: 今回はdenki.pyのプログラムは本題とはずれてしまうので説明は省きましたが、「Raspberry Pi IoT 照明切り替え」などと検索していただければ方法はたくさん出てきます。筆者も機会があれば新しく記事にするかもしれないです。