【Python】WebスクレイピングでSlackとLINEへAtCoderのリマインダー機能を作ってみる

##はじめに
先日業務でソートをする際にシンプルな実装ができず、アルゴリズムへの苦手があったため流行りの競技プログラミングを始めてみることにしました。
私は灰色コーダーでAB(たまにC)が解けるレベルなのでレートを上げるために解くスピードが重要になってくるわけですが、
1ヶ月ほど参加してみて、開始時間に遅刻してしまうことが多々ありました。土曜日の夜は忘れてしまいがちです。。。
その結果直近2回のコンテストで終了間際に参加忘れに気づいてしまい、パフォーマンスが低く出てしまいました:cry:
![スクリーンショット 0003-03-13 2.43.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648295/ef74e7e0-ce68-14aa-012f-eb847d01430c.png)

##制作したいもの
時間通りに参加するというのが茶コーダーになる最短であると考えました。そこで、AtCoderの参加忘れ防止リマインダーをLINEとSlackに送る機能を作成することにしました。

**必要機能**
1. 土曜日の20:50に通知をする。(コンテスト開始が基本21:00のため)
2. 直近のコンテストの開始時間、タイトルをLINEとSlackに通知する。
3. AtCoder Beginner Contest(ABC)のみ通知する。(ARCは2回参加したが0完だったので...:sweat:)

**使用技術**
1. Python3.8.2(環境はVS Codeを使用)
2. webスクレイピング(Requests、Beautiful Soup)
3. LINE Notify
4. slack webhook
5. cron(自動実行)

##Webスクレイピング
https://atcoder.jp/contests/
こちらのコンテスト一覧画面です。
![スクリーンショット 0003-03-13 14.19.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648295/ec5e5dc1-4147-f932-4c09-6a4d0d31ef0f.png)
詳しく見てみると欲しい情報(コンテストの開始時間とタイトル)は```<div id="contest-table-upcoming">```内のaタグにあることがわかります。
![スクリーンショット 0003-03-13 14.23.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648295/b3fcea03-bd3a-a55e-6568-38f64e4afcf5.png)
スクレイピングしていきましょう。

```python
import requests
from bs4 import BeautifulSoup
# スクレイピング対象の URL にリクエストを送り HTML を取得する
url = 'https://atcoder.jp/contests'
response = requests.get(url)
# レスポンスの HTML から BeautifulSoup オブジェクトを作る
soup = BeautifulSoup(response.content, "html.parser")
# 予定されたコンテストの開始時間とタイトルを全て取得
elems = soup.select("#contest-table-upcoming a")
```
このままでは```print(elems)```するとこうなるわけです。

```
[<a href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20210313T2100&amp;p1=248" target="blank"><time class="fixtime fixtime-full">2021-03-13 21:00:00+0900</time></a>, <a href="/contests/abc195">Panasonic Programming Contest (AtCoder Beginner Contest 195)</a>, <a href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20210314T2100&amp;p1=248" target="blank"><time class="fixtime fixtime-full">2021-03-14 21:00:00+0900</time></a>, <a href="/contests/arc114">AtCoder Regular Contest 114</a>, <a href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20210320T2100&amp;p1=248" target="blank"><time class="fixtime fixtime-full">2021-03-20 21:00:00+0900</time></a>, <a href="/contests/abc196">AtCoder Beginner Contest 196</a>, <a href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20210321T2100&amp;p1=248" target="blank"><time class="fixtime fixtime-full">2021-03-21 21:00:00+0900</time></a>, <a href="/contests/arc115">AtCoder Regular Contest 115</a>, <a href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20210327T2100&amp;p1=248" target="blank"><time class="fixtime fixtime-full">2021-03-27 21:00:00+0900</time></a>, <a href="/contests/abc197">AtCoder Beginner Contest 197（Sponsored by Panasonic）</a>, <a href="http://www.timeanddate.com/worldclock/fixedtime.html?iso=20210328T2100&amp;p1=248" target="blank"><time class="fixtime fixtime-full">2021-03-28 21:00:00+0900</time></a>, <a href="/contests/arc116">AtCoder Regular Contest 116</a>]
```
見にくいですよね。直近のデータかつテキストにタグの文字列だけに変換します。
また予定の日付もタイムゾーンや年が必要ないため削除します。

```python
import datetime
# 直近のデータ取得
time_string = elems[0].get_text()
# 取得したString型時刻をDate型に変換
time_date = datetime.datetime.strptime(time_string[:-5], '%Y-%m-%d %H:%M:%S')
contest_date = time_date.strftime('%m月%d日%H:%Mから')
contest_text = elems[1].get_text()
```
```print(contest_date, '\n', contest_text)```してみます。

```
03月13日21:00から 
 Panasonic Programming Contest (AtCoder Beginner Contest 195)
```
AtCoder Beginner Contestは長いので省略します。また、ABC以外は受けないので通知をしない処理をします。

```python
# コンテスト名省略 & ABC以外は通知しない
if 'AtCoder Beginner Contest' in contest_text :
    contest_text = contest_text.replace('AtCoder Beginner Contest', 'ABC')
else:
    exit()
```

##LINEへ通知を送る
https://notify-bot.line.me/ja/

ここからLINE Notify API Document > アカウント作成、マイページ > トークンを発行する > トークン名、トークルームを設定（トークン名はAtCoderにしました） > 発行する

```python
def send_line_notify(notification_message):
    # LINEに通知する
    line_notify_token = '上記で取得したトークン'
    line_notify_api = 'https://notify-api.line.me/api/notify'
    headers = {'Authorization': f'Bearer {line_notify_token}'}
    data = {'message': '\n' + notification_message}
    requests.post(line_notify_api, headers = headers, data = data)
if __name__ == "__main__":
    send_line_notiify('LiNEに通知する')
```
![LINE_capture_637303828.559058.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648295/213279f5-68e3-a090-03f8-40963083ae26.jpeg)
実行すると正常に通知されました:v:

##Slackへ通知を送る
Slackチャンネルを作成します。AtCoderというチャンネルを作りました。

https://slack.com/services/new/incoming-webhook

ここからチャンネルを選択し、Add Incoming WebHooks integrationをクリックします。Web hook URLを保存したら終わりです。

```python
import slackweb
def send_slack_notify(notification_message):
    # slackに通知する
    SLACK_URL = '上記で保存したWeb hook URL'
    slack = slackweb.Slack(SLACK_URL)
    slack.notify(text = notification_message)
if __name__ == "__main__":
    send_slack_notify('Slackに通知する')
```
![スクリーンショット 0003-03-13 14.10.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648295/1ff10fd6-a6c2-68c5-93e6-f68b8bc0639a.png)
実行すると同様に通知されました:v:

##実際のコードまとめる
```python
import requests
from bs4 import BeautifulSoup
import slackweb
import datetime

def send_line_notify(notification_message):
    # LINEに通知する
    line_notify_token = '取得したトークン'
    line_notify_api = 'https://notify-api.line.me/api/notify'
    headers = {'Authorization': f'Bearer {line_notify_token}'}
    data = {'message': '\n' + notification_message}
    requests.post(line_notify_api, headers = headers, data = data)

def send_slack_notify(notification_message):
    # slackに通知する
    SLACK_URL = '取得したURL'
    slack = slackweb.Slack(SLACK_URL)
    slack.notify(text = notification_message)

# スクレイピング対象の URL にリクエストを送り HTML を取得する
url = 'https://atcoder.jp/contests'
response = requests.get(url)
# レスポンスの HTML から BeautifulSoup オブジェクトを作る
soup = BeautifulSoup(response.content, "html.parser")
# 予定されたコンテストの開始時間とタイトルを全て取得
elems = soup.select("#contest-table-upcoming a")

# 直近のデータ取得
time_string = elems[0].get_text()
# 取得したString型時刻をDate型に変換
time_date = datetime.datetime.strptime(time_string[:-5], '%Y-%m-%d %H:%M:%S')
contest_date = time_date.strftime('%m月%d日%H:%Mから')
contest_text = elems[1].get_text()

# コンテスト名省略 & ABC以外は通知しない
if 'AtCoder Beginner Contest' in contest_text :
    contest_text = contest_text.replace('AtCoder Beginner Contest', 'ABC')
else:
    exit()

# 通知処理
send_line_notify(contest_date + '\n' + contest_text)
send_slack_notify(contest_date + '\n' + contest_text)

```

##Cron(自動実行)
毎回自分で実行していてはなんの解決にもなっていません。そこでpython自動実行を行っていきます。
毎週土曜日20:50に実行するようmacのcommandに打ち込んでいきます。

```
export EDITOR=vim
echo $PATH
crontab -e
PATH=
# (分)　(時)　(日)　(月)　(曜日) 　(実行するコマンド)
*50 20 * * 6 python /User/~PATHの記述~/atcoder.py
```
![LINE_capture_637329047.276970.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648295/19a2162a-8e2c-ffa9-3fd1-a9bf6ca26d4c.jpeg) 
![スクリーンショット 0003-03-13 20.51.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648295/6f0f8671-8934-51a3-5429-3f53d5362175.png)
時間通り通知がきました:sunglasses:

##最後に
実際にAtCoder Beginner Contestに遅れずに参加できました。
![スクリーンショット 0003-03-13 22.52.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648295/60bedf5c-2422-ce08-53fd-df95c81f8d32.png)
いつもと違い最初からフルで時間をかけれたのでパフォーマンスが過去最高でした！！！
しかし実際PCを開いていないと通知が来ないため、実用化にはデプロイもしないといけません。また機会があればそちらの記事も書こうと思います。

##参考文献
[Beautiful Soup のfind_all( ) と select( ) の使い方の違い](https://gammasoft.jp/blog/difference-find-and-select-in-beautiful-soup-of-python/
)
[【仕事の自動化】Pythonでの自動化プログラムを、さらに効率化させる「スケジュール実行」のやり方【初心者でもわかる】](https://kino-code.com/python_autorun/)