とぱーず茶漬のブログ

@topazgravity がtwitterの延長で書いているぞ。実質長文ツイート。

Paladinsの簡易レポートをDiscordに毎日送信するbotを作った

作ったもの

毎日Paladinsの戦績の簡易レポートをDiscordに送信する「Paladins Diary」。
f:id:topazgravity:20200123211750p:plain
 

仕組み

f:id:topazgravity:20200123220508p:plain
戦績を詳細に見ることができるPaladinsGuruという非公式のWebサイトから情報を取得してDiscordに送る。
そういう処理をPythonで書いて定期実行している。

f:id:topazgravity:20200123222120p:plain
それぞれの処理を実現させるための登場人物はAWS LambdaとSelenium
SeleniumでWebスクレイピングして情報を取得し、
集計してDiscordに投げるスクリプトAWS Lambdaにアップロードして定期実行している。 


f:id:topazgravity:20200123224829p:plain
動かしているスクリプトの動きはこんな感じ。
txtファイルには以下3つを書いた。

  1. メンバー各々のPaladinsGuruのURL
  2. DiscordのWebHook URL
  3. 送信文のテンプレート

この辺は状況に応じて変わると思ったのでソースコードに触らなくても編集できるように外出ししておいた。実際、文面のテンプレートとか納得いかずにコロコロ変えたりした。

環境構築やLambdaでの定期実行、Webスクレイピング周りの実装については以下の記事を大いに参考にさせていただいた。
qiita.com

開発環境

AWS Cloud9を利用。
ブラウザで動くIDE。裏でEC2のインスタンスが動いている。
簡単に言うと、Linuxサーバーを借りてブラウザ経由で開発できる。
AWS初心者マンだけど、これはめちゃめちゃ便利だと思った。
どんなデバイスから開いても同一の環境が使えるので、病院の待ち時間にiPadでバグ一つ解決できたりした。
参考にさせていただいた記事に書いてあったので真似してCloud9で開発した。

テキストファイルを読み取る

f:id:topazgravity:20200124000658p:plain
読み取りPythonファイルとテキストファイルはこういう位置関係。

import os

def get_text():

    #pythonファイルと同じ階層に置いたconfigフォルダのパスを格納
    path = os.path.dirname(__file__) + "/config/"

    #configフォルダ内にある送信メッセージのテンプレートを読み込み
    with open(path + "message_template.txt",encoding="utf_8") as f:
        message = f.read()

    #WebHook URLを読み込み
    with open(path + "webhook_url.txt",encoding="utf_8") as f:
        webhook_url = f.read()

    #名前とPaladinsGuruのURLを取得
    with open(path + "user_list.txt",encoding="utf_8") as f:
        user_list = f.readlines() #ここをreadlinesにすると1行ずつlistに格納できる

    return message,webhook_url,user_list

たったこれだけ。超簡単。
参考にさせていただいた記事:Pythonでファイルの読み込み、書き込み(作成・追記) | note.nkmk.me

ブラウザを開いて戦績を読み取る

正直これはあんま理解せずに見様見真似で作ったので、ヤバいコードになっている自信ある。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import time
import os

date=[]
elo=[]
sr=[]
champ=[]


def webscrape(url):
     # webdriver settings
    options = webdriver.ChromeOptions()
    options.binary_location = os.path.dirname(__file__) +"/bin/headless-chromium"
    options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--single-process")

    driver = webdriver.Chrome(
    #chromedriverのファイルパスを指定
        executable_path=os.path.dirname(__file__) +"/bin/chromedriver", 
        chrome_options=options
    )

    driver.get(url)
    driver.implicitly_wait(60) # 要素が出現するまでの待機時間の上限
    targetElement=driver.find_element_by_xpath('//*[@id="cw"]/section/section/div[2]/div/div[2]/div[2]/div[1]/div[1]')

    for i in range(2,26):
        date_check = driver.find_element_by_xpath('//*[@id="cw"]/section/section/div[2]/div/div[2]/div['+str(i)+']/div[1]/div[1]').text
        
        if 'HOURS' in date_check or 'MINUTES' in date_check:
            date.append(date_check)
            elo.append(driver.find_element_by_xpath('//*[@id="cw"]/section/section/div[2]/div/div[2]/div['+ str(i) +']/div[1]/div[2]/div[2]').text)
            sr.append(driver.find_element_by_xpath('//*[@id="cw"]/section/section/div[2]/div/div[2]/div['+ str(i) +']/div[1]/div[2]/div[3]').text)
            champ.append(driver.find_element_by_xpath('//*[@id="cw"]/section/section/div[2]/div/div[2]/div['+ str(i) +']/div[2]/div[2]/div[2]/div[1]').text)
        else:
            break
   
    return date,elo,sr,champ

//*[@id="cw"]/section/section/div[2]/div/div[2]/div['+str(i)+']/div[1]/div[1]みたいな部分はxpathといって、ブラウザの開発者ツールを使って取得できる。
HTMLの構造に従って法則性があるので、何箇所かxpathを比較して、変数にできる部分を見つけ無理やりループ処理に落とし込んだ。
また、マッチ時間にHOURSかMINUTESという文字が含まれていれば24時間以内のマッチということになるので、条件式に組み込んで24時間以内の情報を読み込むようにした。

ページの構造が変わればxpathも変わるし、文言が変われば条件式が使い物にならなくなる。
ページの仕様への依存度が高く、頻繁に更新されるサイトをスクレイピングしたいならコードの改修がその都度発生する。はっきり言ってだるい。
定期的に実行するスクリプトにWebスクレイピングは適していない。今回の学び。

参考にさせていただいた記事:
AWS Lambdaで動的サイトのwebスクレイピングをしてtwitterに投稿するbotを作った - Qiita
Seleniumで待機処理するお話 - Qiita
Selenium webdriverよく使う操作メソッドまとめ - Qiita

Discordに投げる

WebHookという仕組みを使って簡単にメッセージを送信できる。
とても簡単に扱えるので、一方的に送るだけのbotを作るときには便利。

import requests

#message_sendには送信する文字列を入れておく
#webhook_urlにはWebHook URLを入れておく

main_content = {
"content": message_send
}
requests.post(webhook_url,main_content)

参考にさせていただいた記事:discordのwebhookをpythonのrequestsのpostで使う | jibundex

ちなみにWebHook URLはサーバ管理画面から取得できる。
f:id:topazgravity:20200123235648p:plain

Lambdaに載せて定期実行する

大いに参考にさせていただいた記事の内容を丸々実行。
qiita.com
Paladinsの戦績がPaladinsGuru上で見えるようになるまで時間差があったので、身内のプレイが落ち着く朝方に送信して「昨日の分のレポート」とすることにした。
実行スケジュールを定義するCron式については謎の仕様に苦戦。
「cron(00 19 * * ? *)」とすれば毎日UTC19時(日本時間朝4時)に動くようにできた。

結果

こうなる。
f:id:topazgravity:20200123211750p:plain
ELO、SRは偉大なるPaladinsGuru様を眺めていて欲しいと思った情報だったので入れた。
管理者の方いわくELOは今後廃止する予定らしい。そうなると色々と対応が必要になる。

最後に

自分はクソ雑魚なのでWebスクレイピングすんの初めてだしAWSまともに触ったこと無いし、何ならPythonのループ処理の文法すらググりながら開発してたけど、とりあえず動くものができたので満足。
1日25戦までしか集計できないしバグもあるし、諸々もっとうまいやり方はいくらでもあるんだろうけど、現状これが僕のベストです。頑張りました。
今後も精進していきます。