LoginSignup
4
1

More than 1 year has passed since last update.

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

Posted at

0 おさらい

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

1 CSVファイルの編集

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

アクセスできたらdataフォルダ内のactiondata.csvを開いてください。
01.png
今回はVscodeの拡張機能1であるEdit CSVを使用して編集していきます。この拡張機能にはcsvファイルを編集するのに便利な機能が含まれています。
開くと下のような画面になります。
03.png
拡張機能がインストールされていれば右上に"Edit CSV"というボタンが現れます。このボタンを押す前にactiondata.csvに一つで構いませんので「,(カンマ)」を入力して下さい。 拡張機能の仕様なのかはわかりませんが、空のcsvファイルをこの機能で編集しようとするとうまくいきません。
image.png

カンマが入力できたら、保存して右上のEdit CSVをクリックします。すると、下の写真のように一行二列のセルが表示されます。
04.png
この状態でファイルを操作していきます。上の「Add row」では行、「Add column」では列をそれぞれ追加することができます。今回は以下のようにcsvを構成しました。
05.png
左から質問内容、その次に応答、最後にコマンド番号です。質問内容はユーザーが投げかけてくる質問(受信メッセージ)です。応答はLINEbotが返信するメッセージを指します。コマンド番号については質問に対して何かしらのコマンドを実行するための識別子です。例えば「今のTwitterのトレンドは?」という質問をした際に、pythonのTwitter APIプログラムを実行する場合など別のプログラム、プラットフォームや外部センサーなどを操作する際に使用するものです(詳細は3章の応用)。
では、ここからは自由に質問とそれに対する応答を記入していきます。Add rowでいくつか行を追加して質問内容、応答、コマンド番号をそれぞれ記入していきます。この際に同じ行の質問内容、応答、コマンド番号が対応するようにしてください。例えば、下図のように質問内容に「おはよう」と書き込んだら、同じ行の応答にはそれに対する応答を書きます。ここでは「おはようございます。」と書きました。さらに合わせてコマンドを操作したい場合はコマンド番号に適切な番号を入力します。今回は全て0で構いません。
image.png
最終的には下図のようにしました。
06.png
書き込みが終わったら上にある「Apply changes to file and save」を押して元のcsvファイルに適用し保存します。ファイルが保存できたかどうかの確認は、上のファイルタブ(CSV edit actiondata.csvと書いてあるところ)で確認できます。左上にある*マークが消えている場合は保存できています。保存できたら元のcsvファイルに戻ります。そして中身が入っていれば大丈夫です。
image.png
08.png
これでcsvファイルは編集完了です。会話をさらに広げたい場合は、プログラムを変えずにこのcsvファイルを追記するだけです!

2 コーディング

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

2.1 準備

プログラムを書き換える前に必要なpythonライブラリをインストールしましょう!
今回インストールするライブラリはpandasです。pandasライブラリはデータ分析作業を支援してくれる機能が含まれており、配列などをシリーズやデータフレームとして扱うことができます。今回は先ほど作成したcsvファイルを取り込み、pandasライブラリで2次元配列に変換して扱うために使います2
インストール方法は以下の通りです:

pip install pandas

または

pip3 install pandas

これでOKです。その他にも時間を扱うプログラムであればdatetime、乱数を扱いたい場合はrandomなどプログラムの使用用途に合わせてライブラリをインストールしてください。インストール方法は上と同じです。

pip install インストールしたいライブラリ名

または

pip3 install インストールしたいライブラリ名

2.2 プログラム変更結果

では、実際に書いていきましょう!前回のプログラムより主に変える部分はこのselectwords関数です。

selectwords
#変更前(前回)
def selectwords(commandtext):  # 対応する言葉を選択
    if commandtext == "おはよう":
        reply = "おはようございます!"
    elif commandtext == "こんにちは":
        reply = "こんにちは"
    elif commandtext == "こんばんは":
        reply = "こんばんは"
    else:
        reply = "分からない単語です。"

    return reply

変更後のプログラムを先に見せます、変更後は下のようになります。*プログラム冒頭に先ほどインストールしたpandasライブラリをインポートすることを忘れないでください。

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ファイルを開き、その中身を2次元配列として読み込みます3

2.3.2 ループ動作

次にfor文ですが、forは簡単に言うとactionDataの中身を上から順に見ていく動作をしています。
ここで少しpandasライブラリのデータフレームについてお話します。通常、pandas.DataFramを単純にfor文に入れて実行すると列名のみが得られます。しかし、今回必要なのは1行ずつの全ての列情報です。そこで、pandasライブラリの機能である、DataFrame.iterrows()メソッド4を使っていきます。このメソッドによりデータを1行ずつ取り出すことができます。forループの動きと変数の扱いは下図のようになります。
image.png
forには2つの変数ivalueがあります。iはラベル(index)を示しており、行数情報を持っています。実際には、この行数はデータフレーム(csv)とは異なり、この場合2行目が0となります。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を用いたものです。for else構文ではforの中に必ずbreakが存在します。プログラムを少し勉強された方で、breakが実行されると今度はforを抜けてelseの中身が実行されるのではないかと思われるかもしれませんが、それは間違いです。この場合には、elseが実行されるのはforの中の条件が一回も合致しなかったときです。もっと言うと、ループが回り切った時に実行されるということです。
ではelseブロックの中身を見ていきましょう。elseブロックには

reply = "分からない単語です。"

とあります。これは前回のプログラムにもあった、作成した条件分岐(if)が当てはまらなかったときに実行されるプログラムでした。今回も意味合いは同じで、csvファイルに記載のない質問内容であった時に実行されるプログラムです。

プログラムの操作としてはこれで終わりです。後は変数replyに入った返信メッセージを返り値として返してあげれば、適切な応答がユーザーに送信されます。ここまでのプログラム全体を下に示します。

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の中身は割愛しますが照明をオンにできる関数がon()で、照明をオフにできる関数がoff()となっています。
ディレクトリ構成にはapp.pyと同じ階層にdenki.pyを追加しているので注意してください!

pi/
└ LINEbot/
│ └ config.py(環境変数、アクセストークンなど)
│ └app.py(実際に操作する内容)
│ └denki.py(新たに追加した照明をオンオフするプログラム)
│ └data/
│ │ └ actiondata.csv(操作内容や会話パターンなど)

3.1 csv変更

初めにcsvの中身を変更しましょう。いつものようにEdit csvボタンで編集画面に行きます。Add rowボタンで2行分追加します。そしたら追加した行に上から「照明オン」、「照明オフ」と入力します。次に、真ん中の応答列には動作を知らせるメッセージ「照明がオンになりました」と「照明がオフになりました」を入力します。最後に、追加した行のコマンド番号列に上から「1」、「2」と入力し適用・保存をします。
image.png

3.2 プログラム変更&解説

ではプログラムを変更していきます。今回も例によってselectwords関数に追記します。加えて、今回は外部プログラムも使用するのでその外部プログラムのインポートもします。
for文ではifの中にさらに条件分岐を増やします。変更後のプログラムを下に示します。

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が1(照明オン)の時は、外部プログラムdenki.pyのon()関数が実行されます。つまり部屋の照明がオンになります。そして「照明がオンになりました」とユーザーに送信されます。
一方でopeが0(照明オフ)の時は、外部プログラムdenki.pyのoff()関数が実行され、部屋の照明がオフになります。オンと同様にこちらもユーザーに「照明がオフになりました」と送信されます。
opeが0(照明オン・オフ以外)の時は、外部プログラムは何も実行されずただreply変数が返されるだけとなります。

3.3 プログラム全文&実行結果

csvファイル適用+応用を施したプログラム全文を下に示します。参考にしてください。

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
きちんとコマンドが実行されていることが分かります。またその他の会話も問題なく動作していますね!

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

4 まとめ

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

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

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

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

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

参考資料(引用を含む)

サイト

書籍

  • コーディングを支える技術-成り立ちから学ぶプログラミング作法

これまでの記事

  1. 拡張機能のインストール方法について
    拡張機能とはデフォルトのvscodeでは対応していない機能を、後から自分好みで入れることができるものです。尚、拡張機能を自作して公開することも可能です。拡張機能のインストール方法は左バーにある、四角のブロックの組み合わせのようなアイコンをクリックします。
    02.png
    次に上にある”Marketplaceで拡張機能を検索する”をクリックして所望の拡張機能を検索して該当するものをクリック、インストールするのボタンを押してインストール完了です。インストール後は再読み込みが必要なこともあります。

  2. pandasライブラリについて
    pandasライブラリはNumPyライブラリと依存性を持つライブラリです。numpyをインストールしていない場合は併せてインストールしましょう。
    インストール方法はpip install numpyです。
    環境によってはnumpyのバージョン違いによりpandasライブラリが動作しない場合があります。

  3. この際にactionDataの型はclass 'pandas.core.frame.DataFrame'となります。つまりcsvの中身をデータフレームとして扱うことができるのです。

  4. itterrowsメソッド以外にも同じ機能を持つDataFrame.itertuples()メソッドがあります。機能は同じですが、itertuplesの方が高速です。また、今回は1行ずつの情報でしたが場合によっては1列ずつの情報が欲しい場合もあると思います。その際にはDataFrame.iteritems()メソッドを使います。

  5. この場合の一番左の列は質問内容ですが、これを聞いて違和感を覚える方もいらっしゃると思います。実際にはこの場合の一番左の列は1~7の数字であるインデックスです。しかしデフォルトのforループのデータフレームの操作では1行目と1列目は見られずに2行目、2列目から始まります。オプションで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 照明切り替え」などと検索していただければ方法はたくさん出てきます。筆者も機会があれば新しく記事にするかもしれないです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1