【python】メンドクサイを自動にさせてみた３【SlackBot編】

#この記事について
最近iOSアプリで20円ばかり儲けた凄腕プログラマー（自称）がサラリーマンしている仕事の一部がめんどくさかったり、誰かの人的ミスのせいでなんか怒られたりと嫌になりそうなことを、プログラムを作ってサクッと解決★

今回は送迎の有無をpythonを使って判別し、slackに放り投げて通知を送ることで、見落としミスがなくなるようにしました。

背景には、今時宿泊者名簿をイチイチ紙に出力して、今日の送迎の有無を確認しているのですが、その出力するシステムがツギハギだらけのもので、予期せぬ動作をすることがあります。
今回は、送迎あり・なしのラジオボタンがあるのですが、ありのボタンを押しても、その下にある備考欄が空欄だと、紙に出力されないというわけわかんない不具合がありました。
何言ってるのかわからないかと思いますが、私にもわかりません。

#何をした？
１．pythonを使います。
２．seleniumを使って予約のコントロールパネルから情報を取得します。
３．slackに送迎の有無と送迎する人の名前を出力します。
４．タスクマネージャーで自動的に実行します。

なお、私の記事では毎回完全初心者向けに記事を書いています。
しつこい説明があるかもしれませんが、大目に見てください。

では行きましょう。

#コード全体
<details><summary>pythonファイル</summary><div>

```myslackbot.py 
#coding: UTF-8
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from datetime import datetime, date, timedelta

import slackweb
import time
import json

reserveDic = { 0: "施設１", 
             1: "施設２", 
            2: "施設３", 
            3: "施設４", 
            4: "施設５",
            5: "施設６" 
}

stationDic = {0: "なし" ,
            1: "A駅"
}

class person:
    def __init__(self):
        self.name = '' #名前
        self.reserveId = 0 #０．施設１　１.施設２　２.施設３　３.施設４　４.施設５　５.施設６
        self.station = 0 #０．なし　１．A駅


def main():

    #コンパネに接続して
    dataArray = conpane()
    #取得した配列を一つにつなげる
    postText = arrayToText(dataArray)
    #人数あればslackに名前と人数の投稿　なければ無しと投稿
    postSlack(postText)


def conpane():
    #コントロールパネルに接続
    options = webdriver.ChromeOptions()
    #---headlessで動かすために必要なオプション---
    options.add_argument("--headless")
    driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)
    driver.get("コントロールパネルのURL")

    #ログイン
    #elem_search_word = driver.find_element_by_id("uid")
    elem_search_word = driver.find_element(by=By.ID, value="uid")
    elem_search_word.send_keys("ログインID")
    #elem_search_word = driver.find_element_by_id("passwd")
    elem_search_word = driver.find_element(by=By.ID, value="passwd")
    elem_search_word.send_keys("ログインパスワード")
    #elem_search_btn = driver.find_element_by_css_selector(".mws-button.green.mws-login-button")
    elem_search_btn = driver.find_element(by=By.CSS_SELECTOR, value=".mws-button.green.mws-login-button")
    elem_search_btn.click()
    time.sleep(3)
    #予約管理マネージャーページに移行
    driver.get("予約管理マネージャーページ")
    time.sleep(3)

    #メンバーを格納
    customerArray = []
    #seleniumのラジオボタンid
    radioNumber = ["radio_1", "radio_2", "radio_3", "radio_4", "radio_5"]
    #各種類で必要なループ回数
    needLoop = [25,17,25,17,17,25]
    #while文のカウント用
    x = 0
    
    #whileループ内で関数にもっていくようにする
    #ラジオボタン、セルの数、ループの数とかもってく
    while x <= 5:
        #getCustomer引数＝[ドライバー, 棟タイプID, seleniumラジオボタンid, 必要ループ数] 
        customerElem = getCustomer(driver, x, radioNumber[x], needLoop[x])
        customerArray.append(customerElem)
        x += 1
    
    return customerArray


def getCustomer(driver, x, radioNumber, needLoop):

    #ラジオボタンの選択
    #elem_search = driver.find_element_by_id(radioNumber)
    elem_search = driver.find_element(by=By.ID, value=radioNumber)
    driver.execute_script("arguments[0].click();", elem_search) #ラジオボタンを選択するためのJs
    time.sleep(2)

    customer = person()
    preArray = []
    #テーブル範囲の選択
    #elem_search_tr = driver.find_elements_by_xpath( "//div[@class='ui-widget-content ui-selectee']" )
    elem_search_tr = driver.find_elements(by=By.XPATH, value="//div[@class='ui-widget-content ui-selectee']")

    for i in range(0, int(needLoop), 8):
        customer = person()

        #コントロールパネルの顧客情報のボタンを押す
        #driver.find_element_by_link_text(u"顧客情報").click()
        driver.find_element(by=By.LINK_TEXT, value=u"顧客情報").click()
        time.sleep(2)
        
        #elem_color = elem_search_tr[i].value_of_css_property('background-color')
        elem_color = elem_search_tr[i].value_of_css_property('background-color')
        #backgroundが赤(rgba(215, 66, 137, 1）)のとき
        if elem_color == "rgba(215, 66, 137, 1)":
            elem_search_tr[i].click()
            time.sleep(2)
            #名前
            
            #customer.name = driver.find_element_by_id('client_furigana').get_attribute('value')
            customer.name = driver.find_element(by=By.ID, value="client_furigana").get_attribute('value')
            customer.reserveId = x

            #コントロールパネルの予約施設一覧のボタンを押す
            #driver.find_element_by_link_text(u"予約施設一覧").click()
            driver.find_element(by=By.LINK_TEXT, value=u"予約施設一覧").click()
            time.sleep(2)
            #jsonData = driver.find_element_by_xpath('//*[@id="reservelist"]/tr/td[7]/a').get_attribute('data-json')
            jsonData = driver.find_element(by=By.XPATH, value='//*[@id="reservelist"]/tr/td[7]/a').get_attribute('data-json')
            customer.station = int(jsonData[10])

            preArray.append(customer)
        else:
            continue
    
    return preArray
        

def arrayToText(dataArray):
    text = ''
    for texts in dataArray:
        for i in texts:
            if i.station == 1:
                text = text + i.name + " " + reserveDic[i.reserveId] + " " + stationDic[i.station] + "\n"
            else:
                continue
    
    if text == '':
        text = "A駅の迎えなし"
    
    return text


def postSlack(postText):
    slack = slackweb.Slack(url="Slackのdevelop画面でもらえるURL")
    slack.notify(text=postText)


if __name__ == '__main__':
    main()
```
</div></details>

<details><summary>batファイル</summary><div>

```callSlackBot.bat
cd C:\AutoTask\slack_bot
call C:\Users\ユーザー名\anaconda3\Scripts\activate.bat
call activate slackbot
python myslackbot.py
exit
```

</div></details>

はい。
決してきれいなコードではない自覚はありますので、動けばいい精神でお願いします。
アドバイスはありがたく吸収させていただいますので、ご指摘いただければ幸いでございます。

##解説
```myslackbot.py 
#coding: UTF-8
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from datetime import datetime, date, timedelta

import slackweb
import time
import json

reserveDic = { 0: "施設１", 
             1: "施設２", 
            2: "施設３", 
            3: "施設４", 
            4: "施設５",
            5: "施設６" 
}

stationDic = {0: "なし" ,
            1: "A駅"
}

class person:
    def __init__(self):
        self.name = '' #名前
        self.reserveId = 0 #０．施設１　１.施設２　２.施設３　３.施設４　４.施設５　５.施設６
        self.station = 0 #０．なし　１．A駅
```
dictionaryを使って数字に該当する施設名を登録しています。
これがないと、変数をイチイチswitchとかifとかで条件分けして名前判定することになります。
なくてもいいけどあるとすっきり書けるといったところです。
また、クラスでpersonは【名前】と【予約施設】と【迎えの有無】を持つことにします。


```myslackbot.py 
def main():
    #コンパネに接続して
    dataArray = conpane()
    #取得した配列を一つにつなげる
    postText = arrayToText(dataArray)
    #人数あればslackに名前と人数の投稿　なければ無しと投稿
    postSlack(postText)
```
メインは指示するためだけの関数です。
conpane関数、arrayToText関数、postSlack関数の三つを順番に呼び出します。

```myslackbot.py 
def conpane():
    #コントロールパネルに接続
    options = webdriver.ChromeOptions()
    #---headlessで動かすために必要なオプション---
    options.add_argument("--headless")
    driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)
    driver.get("コントロールパネルのURL")

    #ログイン
    #elem_search_word = driver.find_element_by_id("uid")
    elem_search_word = driver.find_element(by=By.ID, value="uid")
    elem_search_word.send_keys("ログインID")
    #elem_search_word = driver.find_element_by_id("passwd")
    elem_search_word = driver.find_element(by=By.ID, value="passwd")
    elem_search_word.send_keys("ログインパスワード")
    #elem_search_btn = driver.find_element_by_css_selector(".mws-button.green.mws-login-button")
    elem_search_btn = driver.find_element(by=By.CSS_SELECTOR, value=".mws-button.green.mws-login-button")
    elem_search_btn.click()
    time.sleep(3)
    #予約管理マネージャーページに移行
    driver.get("予約管理マネージャーページ")
    time.sleep(3)

    #メンバーを格納
    customerArray = []
    #seleniumのラジオボタンid
    radioNumber = ["radio_1", "radio_2", "radio_3", "radio_4", "radio_5"]
    #各種類で必要なループ回数
    needLoop = [25,17,25,17,17,25]
    #while文のカウント用
    x = 0
    
    #whileループ内で関数にもっていくようにする
    #ラジオボタン、セルの数、ループの数とかもってく
    while x <= 5:
        #getCustomer引数＝[ドライバー, 棟タイプID, seleniumラジオボタンid, 必要ループ数] 
        customerElem = getCustomer(driver, x, radioNumber[x], needLoop[x])
        customerArray.append(customerElem)
        x += 1
    
    return customerArray

def getCustomer(driver, x, radioNumber, needLoop):

    #ラジオボタンの選択
    #elem_search = driver.find_element_by_id(radioNumber)
    elem_search = driver.find_element(by=By.ID, value=radioNumber)
    driver.execute_script("arguments[0].click();", elem_search) #ラジオボタンを選択するためのJs
    time.sleep(2)

    customer = person()
    preArray = []
    #テーブル範囲の選択
    #elem_search_tr = driver.find_elements_by_xpath( "//div[@class='ui-widget-content ui-selectee']" )
    elem_search_tr = driver.find_elements(by=By.XPATH, value="//div[@class='ui-widget-content ui-selectee']")

    for i in range(0, int(needLoop), 8):
        customer = person()

        #コントロールパネルの顧客情報のボタンを押す
        #driver.find_element_by_link_text(u"顧客情報").click()
        driver.find_element(by=By.LINK_TEXT, value=u"顧客情報").click()
        time.sleep(2)
        
        #elem_color = elem_search_tr[i].value_of_css_property('background-color')
        elem_color = elem_search_tr[i].value_of_css_property('background-color')
        #backgroundが赤(rgba(215, 66, 137, 1）)のとき
        if elem_color == "rgba(215, 66, 137, 1)":
            elem_search_tr[i].click()
            time.sleep(2)
            #名前
            
            #customer.name = driver.find_element_by_id('client_furigana').get_attribute('value')
            customer.name = driver.find_element(by=By.ID, value="client_furigana").get_attribute('value')
            customer.reserveId = x

            #コントロールパネルの予約施設一覧のボタンを押す
            #driver.find_element_by_link_text(u"予約施設一覧").click()
            driver.find_element(by=By.LINK_TEXT, value=u"予約施設一覧").click()
            time.sleep(2)
            #jsonData = driver.find_element_by_xpath('//*[@id="reservelist"]/tr/td[7]/a').get_attribute('data-json')
            jsonData = driver.find_element(by=By.XPATH, value='//*[@id="reservelist"]/tr/td[7]/a').get_attribute('data-json')
            customer.station = int(jsonData[10])

            preArray.append(customer)
        else:
            continue
    
    return preArray
```
conpane関数です。
１．seleniumのoptionでヘッドレスを設定します。
ヘッドレスにしないと、seleniumがモニター画面上を占領してピコピコ動きますので、せっかくの自動化が意味半減です。
２．driverはあらかじめダウンロードしてローカルに置き、動かす方法と、毎回必要な時にだけダウンロードして使う方法がありますが、今回は後者です。
こうすることで、パソコンが複数台あったとしても問題なくなります。実行時間はほとんど変わりません。
３．ログイン画面のユーザIDとパスワードを入力する欄を探し出し、ログインボタンを押させます。
４．getCustomer関数内で使う変数を設定し、引数として持っていきます。
５．getCustomer関数内では予約の有無を背景の色で判別しています。
```
jsonData = driver.find_element(by=By.XPATH, value='//*[@id="reservelist"]/tr/td[7]/a').get_attribute('data-json')
            customer.station = int(jsonData[10])
```
でjsonデータも引っ張ってこれます。string型で取得できるので、必要な文字をjsonData[10]でピンポイントに取得しています。
これは数字１文字と確定しているため、int型に変換してcustomerに持たせています。
６．getCustomer関数からは配列が返って来るので、それをさらに配列に入れます。（２次元配列）

この２次元配列を持ってmain関数に戻ります。

```myslackbot.py 
def arrayToText(dataArray):
    text = ''
    for texts in dataArray:
        for i in texts:
            if i.station == 1:
                text = text + i.name + " " + reserveDic[i.reserveId] + " " + stationDic[i.station] + "\n"
            else:
                continue
    
    if text == '':
        text = "A駅の迎えなし"
    
    return text
```
二次元配列を長い一つの文字列に変換してslackに投げたいので処理を行います。
一つ目のforで中の配列を取り出し、二つ目のforで各個人情報を取り出します。
この中がdictionaryを使ったりclassを使ったりすることですっきり書けます。
\nをつけることで、次の段に記入できるようにしておきます。
何も配列がなかった場合、空白""で出力されてしまうので、ifで丁寧な言葉に変換してあげます。
このtextを持ってmain関数に戻ります。

```myslackbot.py 
def postSlack(postText):
    slack = slackweb.Slack(url="Slackのdevelop画面でもらえるURL")
    slack.notify(text=postText)
```
slackに投げるためのプログラムはたった３行です。
今まで処理してきたtextを投げるだけです。

slackのdevelop画面でもらえるURLというのはここでは割愛します。
こちらですごくわかりやすく解説されてましたので、リンクを張らせていただきます。
[Shota Nakagami様 Python3でslackに投稿する](https://qiita.com/shtnkgm/items/4f0e4dcbb9eb52fdf316)

```callSlackBot.bat
cd C:\AutoTask\slack_bot
call C:\Users\ユーザー名\anaconda3\Scripts\activate.bat
call activate slackbot
python myslackbot.py
exit
```
これは.batファイルです。
いつもｈpythonをexe実行ファイルにするpyinstallerというモジュールを使って.pyから.exeにしてタスクマネージャーに登録していたのですが、実行するとすぐに落ちる事態になりました。
解決しようとあれこれ苦戦したのですが、それに時間を使うくらいなら別の方法でアプローチしたほうが早いと思い、batファイルからpythonを実行することにしました。
また、開発環境がanacondaを使っているため、一度anacondaをactivateし、さらに仮想開発環境slackbotをactvateしています。

あとはタスクマネージャーからタスクを作成し、好きな時間にdatファイルを実行するようにして完了です。
ここではタスクマネージャーの作成方法は割愛させていただきます。

お疲れ様でした。

#最後に
コピペしてみた方ならわかるかもしれませんが、各所にfind_elementがコメントアウトされていると思います。
これはseleniumのバージョンアップによって書き方が変わったため、前バージョンと新バージョンで見比べるために両方残しておきました。
新バージョンで書き直さないと、コマンドプロンプトが警告でわちゃわちゃしてしまうため、今からコードを書く人は新バージョンで書きましょう。

この記事が誰かの役に立てば、うれしいなぁ。

#おまけ
プログラムを実行すると、コマンドプロンプトが表示され、ちょっと鬱陶しいです。
そんな時は
以下のbatファイルを新規作成し、先に作ったcallSlackBot.batファイルを呼び出すと、最初からコマンドプロンプトが最小化された状態で実行できます。
下のバーでタスクが動いている事を確認しながら、画面の邪魔にならないので非常に有効です。

```callbat.bat
@echo off
 
start /min %~dp0call_slackbot.bat %*
```