AtCoderのコンテスト情報を毎週通知するSlackBotを作ってみた

AtCoderのコンテスト情報をAtCoderのコンテスト情報を通知するためのSlackbotを作成しました。

# (1)BotのAPIトークンの取得とslackbotのインストール
Slackbotを作成する前にAPIトークンの取得およびslackbotのインストールが必要です。
この二つについては[PythonでSlackBot開発①「APIキーの取得と簡単な返答」](https://www.virtual-surfer.com/entry/2018/04/04/190000)に書かれていることをそのまま実行しました。
書かれている通りに実行を行い、Botとの会話で適当な文字列を送ると以下のようにDEFALUT_REPLYで設定した文言が返ってきます。
<img width="359" alt="スクリーンショット 2019-11-13 16.13.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331393/3f5f115f-306c-62ac-f1b0-c88b8a2b7389.png">

#(2)Webスクレイピング
Slackbotが動くことを確認したのでスクレイピングをしてAtCoderのコンテスト情報を集めていきます。以下がそのコードになります。

```python:scrape.py

from urllib import request
from bs4 import BeautifulSoup
import re
import datetime


#時刻とURLの組を返す
def scrape_active():
    '''
    今開催しているコンテストの情報を返す関数
    '''
    re_contests=[]
    #そのURLの情報を受け取る
    url="https://atcoder.jp"
    html=request.urlopen(url)
    #パーサでページの情報を解析する
    soup=BeautifulSoup(html, "html.parser")
    #contest-table-activeのidをもつdivタグの中に今開催しているコンテストの情報が入っている
    contests1=soup.find("div",id="contest-table-active")
    #コンテストがなかった場合(soupにNoneが入ってしまうので)
    if contests1 is None:
        return re_contests
    contests2=contests1.find("tbody")
    contests3=contests2.find_all("tr")
    #re_contestsにコンテストのurlと終了の日時を格納する
    for c in contests3:
        re_contests_sub=[]
        #コンテストページのurlをurl2に格納
        d=c.find("a",href=re.compile("contests"))
        url2=url+d.get("href")
        html2=request.urlopen(url2)
        soup2=BeautifulSoup(html2, "html.parser")
        #classはPythonでは予約語なのでclass_
        sftime=soup2.find_all("time",class_="fixtime-full")
        #終了の時間のみをre_contests_subに格納
        re_contests_sub.append(sftime[1]+" 終了")
        #コンテストページのurlもre_contests_subに格納
        re_contests_sub.append(url2)
        re_contests.append(re_contests_sub)
    return re_contests


def scrape_upcoming():
    '''
    一週間以内に開催されるコンテストの情報を返す関数
    '''
    re_contests=[]
    url="https://atcoder.jp"
    html = request.urlopen(url)
    soup = BeautifulSoup(html, "html.parser")
    #contest-table-upcomingのidをもつdivタグの中に一週間以内に開催されるコンテストの情報が入っている
    contests1=soup.find("div",id="contest-table-upcoming")
    #コンテストがなかった場合(soupにNoneが入ってしまうので)
    if contests1 is None:
        return re_contests
    contests2=contests1.find("tbody")
    contests3=contests2.find_all("tr")
    #今日の日時を取得(月曜日)
    w=datetime.datetime.today()
    #コンテストのurlと開始の日時をre_contestsに格納
    for c in contests3:
        re_contests_sub=[]
        d1=c.find("time")
        #分まで入ってるところだけをスライスして渡す
        #strtotime関数によりstringからdatetimeオブジェクトに帰る
        t=strtotime(d1.text[:16])
        #その週の日曜までにないコンテストは格納しなくて良い
        if (t-w).days>=7:
            break
        #formatを統一するためにtimetostr関数を使う
        #コンテストの開始の日時を格納する
        re_contests_sub.append(timetostr(t)+" 開始")
        d2=c.find("a",href=re.compile("contests"))
        #コンテストページのurlもre_contests_subに格納
        re_contests_sub.append(url+d2.get("href"))
        re_contests.append(re_contests_sub)
    return re_contests

def strtotime(date_sub):
    '''
    datetimeオブジェクトにして返す
    '''
    return datetime.datetime.strptime(date_sub,'%Y-%m-%d %H:%M')

def timetostr(date_sub):
    '''
    datetimeオブジェクトをstrオブジェクトにして返す
    '''
    W=["月","火","水","木","金","土","日"]
    return ('%d-%d-%d(%s) %d:%s'%(
        date_sub.year,date_sub.month,date_sub.day,W[date_sub.weekday()],date_sub.hour,str(date_sub.minute).ljust(2,"0")
    ))
```

コメントアウトで何をやっているのかは書いていますが、関数の使い方などでわからないことがあれば、参考にしたページを見るようにしてください。

###参考にしたページ
[Beautiful Soup での スクレイピング基礎まとめ [初学者向け]](https://qiita.com/U-MA/items/896c49d46585e32ff7b1)
[BeautifulSoup+Requestsの基本](https://narito.ninja/blog/detail/70/)
[Python日付型](https://qiita.com/motoki1990/items/8275dbe02d5fd5fa6d2d)
[datetime](https://docs.python.org/ja/3/library/datetime.html)


#(3)Slackへの投稿
集めたコンテスト情報をSlackへ投稿していきます。

 ```python:run.py
from slackbot.bot import Bot
from slacker import Slacker
import slackbot_settings
import scrape
import datetime

def make_message(channel,slack,s,message):
    '''
    slack.chat.post_messagを用いてメッセージを送る。
    '''
    for i in s:
        message=message+"\n"+i[0]+"\n"+i[1]+"\n"
    #pos_messageでslackに投稿ができる
    #channelには投稿したいチャンネル
    #messageには投稿したいメッセージ
    #as_userはTrueにしておくことでurlが展開されて投稿される
    slack.chat.post_message(channel, message, as_user=True)

def info(channel,slack):
    #先にスクレイピングをしてコンテスト情報を格納しておく
    s1=scrape.scrape_active()
    s2=scrape.scrape_upcoming()
    #コンテスト情報がない(長さが0)の場合はコンテストが存在しないというメッセージを送る
    if len(s1)!=0:
        make_message(channel,slack,s1,"*[開催中のコンテスト一覧]*")
    else:
        slack.chat.post_message(channel,"*開催中のコンテストはありません*",as_user=True)
    if len(s2)!=0:
        make_message(channel,slack,s2,"*[今週のコンテスト一覧]*")
    else:
        slack.chat.post_message(channel,"*今週のコンテストはありません*",as_user=True)

def main():
    #Botを動かす前にそのチャンネルにBotアプリケーションを追加することを忘れずに
    channel="競プロ"
    #API tokenはslackbot_settings.pyに保存しておく
    slack = Slacker(slackbot_settings.API_TOKEN)
    #月曜であることの確認
    if datetime.datetime.today().weekday()==0:
        info(channel,slack)
    bot = Bot()
    bot.run()

if __name__ == "__main__":
    main()
```

(2)と同様、コメントアウトを参照してください。(詳しく説明すると長くなって読みにくいので)

###参考にしたページ
[chat.postMessage](https://api.slack.com/methods/chat.postMessage)
[SlackのIncoming Webhooksで投稿したURLが展開されなくて困った話](https://qiita.com/foooomio/items/16e4da98f8835daa7b6d)
[Slack APIでbotを作る2: メッセージ投稿編](https://www.dkrk-blog.net/slack/slack_api02)

#(4)Herokuへのデプロイ
(3)まででローカルでは`python run.py`を打つことで動かすことができるはずです。次はHerokuへデプロイします。(Herokuは無料で使えるサーバーみたいなもの)
Heroku自体はGitのリモートリポジトリと考えれば扱いは難しくないので、Gitを勉強しておけば使い方はわかるはずです。(ProgateのGitの章がまとまってておすすめです。)
また、Gitを使ってHerokuにファイルをPushしていくのですが、参考の記事に書かれている通り、Procfile、requirements.txt、runtime.txt、の三つのファイルを作成してからPushしましょう。(作成しないと色々エラー吐かれまくって虚無になります。)

###参考にしたページ
[SlackbotをPythonで作成する](https://qiita.com/illumination-k/items/964abc713e7d6a8b0a1d)
[pythonで作ったSlackBotを常駐化するまでの備忘録](https://qiita.com/usomar＿u/items/6eed064690cdb7988e54)

#(5)環境変数の設定
(1)で参考にしている記事の通りにやっていればslackbot_settignsというファイルができていてAPI_TOKENとDEFAULT_REPLYの二つの変数が定義されているはずです。
しかし、このままだと、ソースコードが流出した時にトークンの値が得られてしまい、Botが乗っ取られてしまうので、ソースコード内にはトークンの値を書かずに環境変数とする必要があります。
ローカルでは`export 環境変数名=値`というコマンドを打てば良いですが、herokuの環境変数を設定をする場合は、`heroku config:set 環境変数名=値`とすることで環境変数を設定できます。また、Herokuの該当のアプリケーションのSettingsのConfig Varsにおいて環境変数の設定をすることができます。
これにより環境変数は設定でき、以下のようにslackbot_settignsを書き換えれば環境変数の設定は完了です。

```python:slackbot_settings.py
import os

#環境変数の取得
API_TOKEN=os.environ["API_TOKEN"]
DEFAULT_REPLY=os.environ["DEFAULT_REPLY"]
```
os.environの使い方については[Pythonで環境変数を取得・追加・上書き・削除（os.environ）](https://note.nkmk.me/python-os-environ-getenv/)を参考にしてください。

###参考にしたページ
[【Python】PythonプログラムをHerokuにデプロイする方法](https://qiita.com/1-row/items/80f89c8ada2e61f04446)
→Herokuの使い方も載ってます。

#(6)スケジューラの登録
最後にスケジューラを登録したら自動化が全て完了です。
まず、スケジューラをアドオンとして追加するのですが、クレジットカードの登録が必要です。アカウントのページから登録を済ますようにしましょう。
登録したら`$ heroku addons:create scheduler:standard`でスケジューラをアドオンに追加でき、`heroku addons:open scheduler`でスケジューラへの登録のページを開くことができます。
開いたページにある"Add job"を押すと、動かす時間の間隔と実行して欲しいコマンドを登録することで定期的に実行することができます。(ちなみに、スケジューラに登録せずに動かすと、約六時間おきに実行されます。)
また、自分の場合は週に一回実行させたかったので、毎日実行させてプログラムの方で週一日のみ目的の処理をするように変えました。(また、時間がUTC表記なので日本時間に直すにはUTC時間に九時間足す必要があることにも注意してください。)

###参考にしたページ
[PythonでSlackBot開発⑧「Heroku Schedulerでプログラム定期実行」](https://www.virtual-surfer.com/entry/2018/04/12/190000)