【Python】飲み会の開催場所をwebスクレイピングで決めてみた

# あらすじ

自分「飲み会やろうよ。俺が幹事やるわ」

友達A・B・C「いいよ」

自分「場所決めようか。皆の最寄り駅教えてくれ」

A「〇〇駅だよ」

B「××駅から来るわ」

C「△△駅」

自分「了解。では集合用のアプリを使って、自分の最寄り駅も含めた
　　　四つの駅から一番集まりやすい駅を検索しよう。
　　　結果は … うーん、□□駅か。近くに飲み屋ほとんどないじゃん。
　　　少し離れるけど、飲み屋が一杯ある新橋駅で開催だとどうかな。

　　　えーと、それぞれの駅から新橋駅までの時間を調べないとな。
　　　まず〇〇駅から新橋までは … 30分か。
　　　次に××駅は … 20分。
　　　次に△△駅は … 」



**めんどくさい！！**

というわけで、初体験のwebスクレイピングで解決を目指すことに。

# こんな感じのものを作りたい
・GUI上に、以下を配置
　- 出発駅の入力枠を複数
　- 集合駅を1つだけ
　- 検索開始ボタン
・複数の出発駅と単一の到着駅をそれぞれ入力して検索開始ボタンを
　押すと、それぞれの出発駅から到着駅までの所要時間を出発駅ごとに
　表示
・存在しない駅を入力したり、駅名を入力しないまま検索開始ボタンを
　押すと、エラーメッセージを表示


# 環境
- PC：windows10
- 言語    ：python 3.6.3
- IDE     ：pycharm
- GUI     ：Tkinter
- ブラウザ操作：Selenium

pythonを選んだのは、今後、仕事で使う予定があるため。
開発環境以下は、pythonを初めて使うためとりあえず
定番(っぽい)のを選択した。

また、情報を抽出するサイトは、普段使用している駅探を選択。

# 実際に作ったもの
初期状態はこんな感じ
![1.png](https://qiita-image-store.s3.amazonaws.com/0/271833/c05113e0-0b18-76c0-76d5-0b801eba53e1.png)

出発駅・集合駅をそれぞれ入力して「検索開始」を押すと…
![2.png](https://qiita-image-store.s3.amazonaws.com/0/271833/10f6e116-c542-ea8c-3bc9-7fc130c4da75.png)

それぞれの駅から、集合駅まで到着するのに必要な時間を算出する
![3.png](https://qiita-image-store.s3.amazonaws.com/0/271833/8eca52ea-4257-06a6-0358-bd91f427401c.png)

存在しない駅名を入力して検索開始を押すと…
![4.png](https://qiita-image-store.s3.amazonaws.com/0/271833/381429d9-95f8-8a63-3eb0-1ba1281fbe51.png)

エラーメッセージが出てくる
![5.png](https://qiita-image-store.s3.amazonaws.com/0/271833/8279c8d7-1b2f-57ac-cc25-98463992d24e.png)


# ソースコード
```python
import sys
import tkinter
import time
from tkinter import messagebox

from selenium import webdriver
from selenium.webdriver.common.by import By

# 定数
CHANGE_MODE = 0
DELETE_MODE = 1
FUNC_NG = 1
FUNC_OK = 0

# 画面の表示
root = tkinter.Tk()
root.title(u"集合する駅を検索")
root.geometry("400x400")

# ボタン押下時に実施する処理
def verify_station_to_gather(event):

    # 出発駅/入力駅が空欄の場合は関数を停止
    if strtsta1.get() and goalsta.get():
        pass # 何もしない
    else:
        messagebox.showerror("エラー内容", "出発駅と集合駅を入力して下さい。")
        return # 関数の処理を終了

    #「所要時間」を初期化
    change_readonly_text(arvtime1, 0, DELETE_MODE)
    change_readonly_text(arvtime2, 0, DELETE_MODE)
    change_readonly_text(arvtime3, 0, DELETE_MODE)
    change_readonly_text(arvtime4, 0, DELETE_MODE)
    change_readonly_text(arvtime5, 0, DELETE_MODE)

    # 操作するブラウザを開く
    driver = webdriver.Chrome("C:/Users/souma/Desktop/Selenium/chromedriver")
    time.sleep(10)

    # 操作するページを開く
    driver.get("https://ekitan.com/")
    time.sleep(20)

    #「出発駅」から「到着駅」までの所要時間を算出
    if input_need_time(driver, strtsta1, goalsta, arvtime1, 1):
        return
    if strtsta2.get():
        if input_need_time(driver, strtsta2, goalsta, arvtime2, 2):
            return
    if strtsta3.get():
        if input_need_time(driver, strtsta3, goalsta, arvtime3, 3):
            return
    if strtsta4.get():
        if input_need_time(driver, strtsta4, goalsta, arvtime4, 4):
            return
    if strtsta5.get():
        if input_need_time(driver, strtsta5, goalsta, arvtime5, 5):
            return

    # ブラウザを閉じる
    driver.close()

def input_need_time(window, start, goal, time, stanum):

    # 駅探のテキストボックスに出発駅と到着駅を入力する
    window.find_element_by_id("sfname").send_keys(start.get())
    window.find_element_by_id("stname").send_keys(goal.get())

    # 駅探の検索ボタンをクリック
    window.find_element_by_xpath("//*[@id=\"searchbtn\"]/table/tbody/tr/td[2]/input").click()

    # 出発駅/到着駅として入力された駅が存在しない時
    if window.find_elements_by_class_name("alert"):

        alertsta = window.find_element_by_class_name("alert").text
        window.close() # ブラウザを閉じる

        if start.get() in alertsta: # 出発駅が存在しない場合

            messagebox.showerror("エラー内容", "出発駅" + str(stanum) + "「" + start.get() + "」は存在しません。")

        else: # 到着駅が存在しない場合

            messagebox.showerror("エラー内容", "到着駅「" + goal.get() + "」は存在しません。")

        return FUNC_NG # 関数を終了

    # 出発駅・到着駅の候補が複数存在する場合、それぞれ格納 ※ 候補なしの場合は空の配列を格納
    strtlist = window.find_elements_by_xpath("//*[@id=\"SFNameList\"]/option[1]")
    goallist = window.find_elements_by_xpath("//*[@id=\"STNameList\"]/option[1]")

    # 出発駅・到着駅のどちらかorどちらも候補が複数存在する場合、「検索」を再度クリックてして先に進む
    if strtlist and goallist:
        window.find_element_by_xpath("//*[@id=\"searchbtn\"]/table/tbody/tr/td[2]/input").click()
    elif strtlist:
        window.find_element_by_xpath("//*[@id=\"searchbtn\"]/table/tbody/tr/td[2]/input").click()
    elif goallist:
        window.find_element_by_xpath("//*[@id=\"searchbtn\"]/table/tbody/tr/td[2]/input").click()

    # 到着までにかかる時間を取得
    value1 = window.find_element_by_class_name("even")  # classでの指定
    # value2 = value1.find_elements(By.TAG_NAME, "td")
    value2 = value1.find_element_by_class_name("time")

    # 到着までにかかる時間をGUIに入力
    change_readonly_text(time, value2, CHANGE_MODE)

    # 一旦トップ画面に戻る
    window.find_element_by_xpath("//*[@id=\"logo\"]/a/img").click()

    return FUNC_OK

# 普段書き換え禁止のエントリーを書き換え
def change_readonly_text(obj, word, mode):

    obj.configure(state="normal") # 一時的に書き換え可能にする

    if mode == CHANGE_MODE: # 書き換える場合
        obj.insert(tkinter.END, word.text)

    elif mode == DELETE_MODE: # 削除する場合
        obj.delete(0, tkinter.END)

    obj.configure(state="readonly") # 書き換え禁止に戻す


# ボタンの設定
bttn = tkinter.Button(text = u'検索開始')
bttn.bind("<Button-1>", verify_station_to_gather) #（Button-2でホイールクリック、3で右クリック）
bttn.place(x=160, y=20)
# bttn.pack()

# ラベル
strtname1 = tkinter.Label(text=u'出発駅１')
strtname1.place(x=60,   y=60)
strtname2 = tkinter.Label(text=u'出発駅２')
strtname2.place(x=60,  y=120)
strtname3 = tkinter.Label(text=u'出発駅３')
strtname3.place(x=60,  y=180)
strtname4 = tkinter.Label(text=u'出発駅４')
strtname4.place(x=60,  y=240)
strtname5 = tkinter.Label(text=u'出発駅５')
strtname5.place(x=60,  y=300)
time1 = tkinter.Label(text=u'所要時間')
time1.place(   x=145,   y=60)
time2 = tkinter.Label(text=u'所要時間')
time2.place(   x=145,  y=120)
time3 = tkinter.Label(text=u'所要時間')
time3.place(   x=145,  y=180)
time4 = tkinter.Label(text=u'所要時間')
time4.place(   x=145,  y=240)
time5 = tkinter.Label(text=u'所要時間')
time5.place(   x=145,  y=300)
goalname  = tkinter.Label(text=u'集合駅')
goalname.place(x=260,   y=60)

# エントリー
strtsta1 = tkinter.Entry(width = 12)
strtsta1.place(x=60,  y=80)
strtsta2 = tkinter.Entry(width = 12)
strtsta2.place(x=60, y=140)
strtsta3 = tkinter.Entry(width = 12)
strtsta3.place(x=60, y=200)
strtsta4 = tkinter.Entry(width = 12)
strtsta4.place(x=60, y=260)
strtsta5 = tkinter.Entry(width = 12)
strtsta5.place(x=60, y=320)
arvtime1 = tkinter.Entry(width = 9, state = "readonly")
arvtime1.place(x=145,  y=80)
arvtime2 = tkinter.Entry(width = 9, state = "readonly")
arvtime2.place(x=145, y=140)
arvtime3 = tkinter.Entry(width = 9, state = "readonly")
arvtime3.place(x=145, y=200)
arvtime4 = tkinter.Entry(width = 9, state = "readonly")
arvtime4.place(x=145, y=260)
arvtime5 = tkinter.Entry(width = 9, state = "readonly")
arvtime5.place(x=145, y=320)
goalsta  = tkinter.Entry(width = 12)
goalsta.place( x=260,  y=80)

root.mainloop()
```

# やってみて分かった課題
・とにかく遅い。これに尽きる。PCのスペックが微妙とか同時に色んなブラウザを
　立ち上げているなど理由もあるが、こんなに遅いとは思わなかった。
　どうもfind_elements（elemetではなくelements）が遅いっぽい。
　
・ソースに記載の「一旦トップ画面に戻る」処理も遅さに拍車をかけている。
　本当ならいちいちトップに戻らず、そのまま新しい出発駅を入力して再度
　検索したいが、これは駅探の仕様なのか更新されない。
　※ 例えば、出発駅１：渋谷、出発駅２：新宿、到着駅：新橋で検索した
　　 場合、最初に渋谷→新橋までの時間が表示された後、トップ画面に戻らずに
　　 出発駅を新宿に更新して再度検索しても、渋谷→新橋までの時間がまた
　　 表示されてしまう。

・GUI上に配置するオブジェクトを宣言する時、time1、time2 … みたいに
　宣言していくのはかなり無駄に感じる。本当ならtime(1)、time(2)の
　ように配列で宣言できればいいが。代替手段を模索中。

・time.sleepを、呼び出した関数内で実行するとエラーが出るため、
　関数内でsleepを使用できなかった。

# 初めてpythonやwebスクレイピングをやって直面したトラブルと対処方法
pipが実行できない
→ pip.exeへのパスを環境に変数リストに登録する

「driver = webdriver.Chrome(chrome driver へのパス)」でエラーが出る
→ chrome driver へのパス中の「\」を「/」に直す

クリックしたい要素のxpathに「"」が複数含まれていると、全体が文字列として認識されない
→「"」の前に「\」を置いてエスケープする

if文の真の処理に何も書かないとエラーが出る
→ passと書く

# 所感
何か新しい技術を学ぶ時、入門サイトや初心者用の書籍を読むのもいいが、
何かを作る過程で色々身に付いた方が、自分にとっては効率が良いと感じた。