田舎者の私がMessagingAPIを使って地下鉄を攻略するまでの過程

#はじめに
ちょっとした好奇心からLINEのMessagingAPIに触りたくなり, 簡単なLINEボットを作りました. 
駅名を入力すると, 駅の出口とそこから行ける場所の一覧が返ってきます. 
詳細は下の動画をご参照ください.

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">linebot作ってみました。楽しい。<a href="https://twitter.com/hashtag/MessagingAPI?src=hash&amp;ref_src=twsrc%5Etfw">#MessagingAPI</a> <a href="https://twitter.com/hashtag/%E5%B9%B4%E6%9C%AB%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F?src=hash&amp;ref_src=twsrc%5Etfw">#年末作ってみた</a> <a href="https://t.co/bbEZNjRGR4">pic.twitter.com/bbEZNjRGR4</a></p>&mdash; やおもて (@susuka_yaomote) <a href="https://twitter.com/susuka_yaomote/status/1211785944933601280?ref_src=twsrc%5Etfw">December 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#なぜ作ろうと思ったのか
地下街ダンジョンを無駄にウロウロしないようにしたかったから. 

以下, 過去の失態です. 
・田舎から名古屋へ出てきた当初, 名古屋駅で迷子になる. 
　このとき, 迷駅の恐ろしさを知る. 
・田舎から東京へ旅行したとき, 地下と地上で同じ道を歩いていることに気づく. 
　このとき, 東京の「え？そんな駅も攻略できないの？ﾌﾟﾌﾟｯ」感を肌で感じる. 
・田舎から生まれ故郷大阪へ帰省したとき, 梅田駅の大混雑地帯を無駄に三往復する. 
　このとき, おばちゃーんのたいあたりが痛いことを改めて実感する. 

#環境
```
python 3.6.1
Flask 1.1.1
urllib3 1.25.7
bs4 0.0.1
selenium 3.141.0
line-bot-sdk 1.15.0
```
pythonの環境構築・スクレイピングのお話は他の記事へお譲りします. 
また, linebotの始め方は先人の経験を参考にさせていただきました. ありがとうございます. 
https://qiita.com/kro/items/67f7510b36945eb9689b

駅情報は[goo路線](https://transit.goo.ne.jp/ "goo路線")から取得します. 

#実装
2019/12/31　例外処理が不十分です. 今後修正します. 
###全体
```python:main.py
from flask import Flask, request, abort
from linebot import (LineBotApi, WebhookHandler)
from linebot.exceptions import (InvalidSignatureError)
from linebot.models import (FollowEvent, MessageEvent, TextMessage, TextSendMessage,)
import os

from bs4 import BeautifulSoup                               # web scraping用
from selenium import webdriver                              # 動的ページに対するscraping用
from selenium.webdriver.chrome.options import Options       # webdriverの設定用

app = Flask(__name__)

# 環境変数取得
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]
# 各クライアントライブラリのインスタンス作成
line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)


# 著名検証とhandleに定義されている関数呼び出し
@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

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

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'


# 友達追加時、最初のメッセージ
@handler.add(FollowEvent)
def handle_follow(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text='駅の何番出口にどんなお店があるかをお知らせするアプリです\n\n〇使い方\n駅名のみ入力してください\n（例：渋谷駅）\n※「駅」までご入力下さい')
    )


# メッセージ
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    # 変数
    stationInfo = {}    # {'駅名-路線':'ページurl'}

    # 入力テキストチェック
    result = list(event.message.text)
    # 入力値OKの場合
    if result[-1] == '駅' and result.count('駅') == 1:
        driver.get(f"https://transit.goo.ne.jp/station/train/confirm.php?st_name={event.message.text}&input=検索")        # 駅名検索ページアクセス
        html = driver.page_source.encode('utf-8')       # HTMLを文字コードをUTF-8に変換してから取得します。
        soup = BeautifulSoup(html, "html.parser")       # htmlをBeautifulSoupで扱う

        # 駅名-路線名と駅ページのurlをリストstationInfoへ格納
        stationName_tag = soup.select('ul.stationname > li > a')
        for sn_tag in stationName_tag:
            stationName = sn_tag.string
            stationInfo[stationName] = sn_tag.get('href')

        # 出口案内情報を取得
        for stationName in stationInfo:
            # 変数初期化
            text = ""
            reUrl = ""
            reUrlCnt = 0
            feedpageFlag = False
            feedpageNum = 0
            feedCnt = 0

            # urlの作り直し
            exitUrl = stationInfo[stationName].split('/')
            for exitUrlOne in exitUrl:
                if reUrlCnt == 4:
                    reUrlCnt += 1
                    continue
                else:
                    reUrlCnt += 1
                    reUrl = reUrl + exitUrlOne + '/'
            driver.get(f"https://transit.goo.ne.jp{reUrl}exit.html")        # 出口案内ページアクセス
            html = driver.page_source.encode('utf-8')       # HTMLを文字コードをUTF-8に変換してから取得します。
            soup = BeautifulSoup(html, "html.parser")       # htmlをBeautifulSoupで扱う

            # 複数ページにまたがるかどうか本処理前にチェック
            feedpage = soup.find(class_='feedpage')
            if feedpage == None:
                feedpageFlag = False
            else:
                feedpage = feedpage.find_all('a')
                feedpageNum = len(feedpage) - 2     #feedpageの数（1ページ目と次への項目を除く）
                feedpageFlag = True

            # 1ページ目は必ず実行 複数ページにまたがる場合は繰り返し
            while True:
                # 変数初期化
                exitCnt = 0

                # 出口と施設をリストexitInfoへ格納
                exit_tag = soup.find_all(id='facility')
                facility_tag = soup.find_all(class_='exit')
                for et in exit_tag:
                    exitName = et.string
                    facility_total = facility_tag[exitCnt].find_all('li')
                    text = text + exitName + '\n----\n'
                    for facility_one in facility_total:
                        facility = facility_one.string
                        text = text + facility + '\n'
                    text = text + '----\n'
                    exitCnt += 1
                if feedpageFlag == False:
                    break
                else:
                    if feedCnt >= feedpageNum:
                        break
                    else:
                        driver.get(f"https://transit.goo.ne.jp{reUrl}{2+feedCnt}/exit.html")        # 出口案内ページアクセス
                        html = driver.page_source.encode('utf-8')       # HTMLを文字コードをUTF-8に変換してから取得します。
                        soup = BeautifulSoup(html, "html.parser")       # htmlをBeautifulSoupで扱う
                        feedCnt += 1
            break

        driver.quit()
        line_bot_api.reply_message(event.reply_token, TextSendMessage(text=text))

    # 入力値NGの場合
    else:
        line_bot_api.reply_message(event.reply_token,
            TextSendMessage(text="駅名を確認してください（名前の最後に「駅」がついているか等）\n例:渋谷駅")
        )

if __name__ == "__main__":
    # スクレイピング準備
    options = Options()             # ブラウザオプション格納
    options.set_headless(True)      # Headlessモードを有効にする（コメントアウトするとブラウザが実際に立ち上がる）
    driver = webdriver.Chrome(chrome_options=options, executable_path='/app/.chromedriver/bin/chromedriver')        # ブラウザを起動

    port = int(os.getenv("PORT", 5000))
    app.run(host="0.0.0.0", port=port)
```
###メッセージ部分
クライアントから受け取ったメッセージ（例：渋谷駅）をlist()にて
文字ごとのリストとし（例：result = ['渋', '野', '駅']）, 
「○○駅」の型式通りかどうか判断します. 
問題なければ[goo路線の駅情報](https://transit.goo.ne.jp/station/train/confirm.php?st_name=%E6%B8%8B%E8%B0%B7%E9%A7%85&input=%E6%A4%9C%E7%B4%A2 "goo路線")さんから駅名と出口情報を取得します. 
情報を整理し, 連結させた情報をtextとして送信します. 

```python
# 出口案内情報を取得
        for stationName in stationInfo:
        ...
            break
```
上記for部分において1度目の処理でbreakしているのは, 
[goo路線の駅情報](https://transit.goo.ne.jp/station/train/confirm.php?st_name=%E6%B8%8B%E8%B0%B7%E9%A7%85&input=%E6%A4%9C%E7%B4%A2 "goo路線")のどの路線であっても出口情報が同じで, 
一つ目の路線だけを取得するためです. 
いわば応急処置です. ここは処理の仕方を見直そう......

#感想
MessagingAPI, 楽しい.

イベントを受けてからの処理を実装するだけで目に見えて動くものが出来るので楽しかったです. 
ただ、認証まわりがよく分からずじまいでした. 
MessagingAPIのドキュメントをもう少し読む必要がありそうです.
コードがかなり汚いのも課題です. 
というか, 駅名入力してから返答までめちゃくちゃ時間かかってるのもどうなんだこれ.....
画像やマップもやり取りできるようなので, もう少し触りたいと思います.