otsunekoの日常

Codeforces(+AtCoder)のレート更新をLINEに通知してくれるツールを開発した話

Notify-cf-ratechange

最近競プロの勉強ばっかだけど、たまにはPythonを使った開発もやってみたいなということで競プロに関係するツールをおもむろに作ってみました。

具体的には、Codeforcesのレートが最新のものに更新されたらLINEに通知してくれるツールをHeroku+LINE Notifyのサービスを利用して実現しました。Codeforcesのレート反映の遅さはあまりにも有名ですよね。既に有志の方がこんなツールを作ってくれていて僕も何回か使っていたのですが、このツールだと毎回自分のユーザ名と現レートを入力しないといけない上、PCのデスクトップにしか通知されないために見落としがち、という難点がありました(それでも今か今かと更新ボタンを押し続ける苦行よりは遥かに良かったですよ!)。

そんなわけで本記事では、作成したツールを簡単に紹介したいと思います。不特定多数の方に広く使ってもらえるようなWebアプリだと尚良かったんですが、今の所完全に自分専用です。Githubリンクはこちらになります。もしも同じ問題を抱えた方が似たツールを作りたいと思った時のご参考になれば幸いです。

2021/6/14追記:なんとAtCoderでも非公式APIでレート変遷のJSON取得できるんですね(僕のアカウントの例)。じゃあこっちもレート更新通知しちゃうかと、本記事と同じ手順でherokuにデプロイしました。Githubリンクはこちらです。ただHerokuは無料プランだと10分周期までしかクロール間隔を短くできないので、すぐにレート更新されるAtCoderでどれだけ意味があるのかについては専門家の間でも意見が分かれるところです。

アイデア

  • CodeforcesはAPIで任意のユーザのレート変遷(user.rating)が取得できるので、pythonのrequestsメソッドでこれを取得する
  • 予め現在までの自分のレート変遷をDBに登録しておき、以降はAPI叩いてuser.ratingを定期的に確認。取得したレート変遷の情報から最新のコンテストIDをキーにDB検索して、もしも該当のコンテスト情報が無ければレートが更新されたと判断し、DBに追加登録及びLINE通知を行う
  • 以上の処理をpythonで書いてHerokuにデプロイ(DBはPostgreSQLを使用)、LINEへの通知はLINE Notifyを使用する

開発環境

  • Windows 10(VSCode + PostgreSQL 13)
  • Github
  • Heroku (Python 3.9.5 + PostgreSQL 13)
  • LINE Notify

下準備

  • Herokuのアカウント登録、Heroku CLIインストール(参考)
  • Herokuで今回作成するアプリの登録、“Heroku Postgres”及び”Heroku Scheduler” Add-onの登録、Githubのリポジトリと連携して自動デプロイ設定
  • Herokuにクレカ情報登録(クレカ情報がないと、“Heroku Scheduler”でスクリプトの定期実行ができない…クレカ登録さえすれば無料で使える)
  • LINE Notifyの利用登録、APIキー取得
  • ローカルで動かせるPostgreSQL環境準備(ローカルテストと、heroku pg:psqlコマンド使用のため)

細かい手順

1. HerokuのPostgreSQL DBに現在までの自分のレート変遷を予め登録する

まず以下のスクリプトで、こどふぉのAPIを叩いて取得したレート変遷の情報をCSVファイルに出力します(USER_NAMEやCSVファイル名は適宜変更ください。以後同様)。

create-cf-ratechange-csv.py
import csv
import requests
from requests.api import request

#Get Codeforces User Info
USER_NAME = "otsuneko"
response = requests.get("https://codeforces.com/api/user.rating?handle=" + USER_NAME)
jsonData = response.json()

#Write Codeforces rate info. to CSV file
with open('codeforces.csv','w', newline='') as f:
    writer = csv.writer(f)
    for contests in jsonData["result"]:
        l = []
        for item in contests.items():
            l.append(item[1])
        writer.writerow(l)

実行したらこんな感じでCSVファイルが出力されます。

codeforces_rate_history_csv

そしたら作成したCSVを試しにローカルのPostgreSQL DBに流し込んでみます。 説明は割愛しますが、DBには予めテーブルやカラムを作成しておきます。今回はcontestsというテーブルを作成し、取得したuser.rating情報のうち以下をカラムとして登録しました。主キーはcontestIdです。

contestId, contestName, rank, ratingUpdateTimeSeconds, oldRating, newRating

ちなみに、PostgreSQLのテーブル名、カラム名は原則snake case(英小文字の単語を_(アンダーバー)でつなぐ)推奨みたいです。僕は最初こどふぉ同様に英大文字を使ったカラム名にしていたのですが、pythonからSQL文投げる時にカラム名が英小文字に変換されたせいでエラーが出て苦しみました(参考)。

話を戻して、psqlコマンドを使ってデータを流し込みたいDBにアクセスした状態で以下を実行します。

copy_csv_data_to_DB
copy [テーブル名] from '~~/codeforces.csv(絶対パス)' with encoding 'sjis' csv;

結果をpgAdmin等で確認すると、うまくいっていれば指定したテーブルに情報が登録されているかと思います。 自分でまともにDBへデータを流し込む体験が初めてだったので、僕はここで早くも感動しました(早い)。

postgresql_table

ここまでで下準備として作成しておくべきDBは完成なので、Herokuへのインポート用にローカルDBのdumpファイルを出力します。 コマンドは以下のような感じです。

output_DB_dump
pg_dump --no-acl --no-owner -h localhost -U [PostgreSQLのユーザ名] [作成したDB名] > codeforces.dump

ではコンソールから以下のHeroku CLIコマンドを使って、ローカルで作成したDBのdumpファイルをHeroku DBにインポートしていきましょう。 以下のコマンドのDATABASE_URLは、Heroku上のPostgreSQLアクセス用のURLとして、既にHeroku側で環境変数として登録されているかと思いますので、そのまま使えばいいかと思います(環境変数の内容は、Herokuアプリのページで、「Setting」タブ->「Config Vars」で確認できるはず)。

import_DB_dump_to_Heroku_DB
heroku pg:psql --app [Herokuに登録したアプリ名] DATABASE_URL < codeforces.dump

問題無く実行できれば、CREATE TABLEとかCOPY (レコード数)とかALTER TABLEといったログが表示されてインポートできるかと思います。ちゃんとインポートできてるかHeroku DBのテーブルを直接確認したい方は、こちらを参考に確認してみてください。

以上で現在までのレート変遷をHeroku DBに登録完了です!

2. レート更新をチェックして更新があればDBに登録&LINEに通知する機能の開発

今回作ったコードは以下になります。これが、Heroku上で定期実行されるわけですね。

基本的な流れは、(1)こどふぉのレート変遷(user.rating)を取得、(2)レートに更新があればDBに追加登録&LINEに通知、というシンプルなものです。 補足として、LINE NotifyのAPIキー等、直接コードに書きたくない情報とかはHerokuの環境変数に登録して、os.environで取得してます。また、PythonからPostgreSQLを扱うライブラリには一番有名そうなpsycopg2を採用しました。

main.py
import os
import psycopg2
import requests
from requests.api import request

# Connect to Heroku PostgreSQL DB
def connect():
    con = psycopg2.connect(os.environ['DATABASE_URL'])
    return con

# Search a record from connected DB
def search_DB(con, sql):
    with con.cursor() as cur:
        cur.execute(sql)
        rows = cur.fetchall()
    return rows

# Insert a record to DB table
def insert_DB(con, sql, data):
    with con.cursor() as cur:
        cur.execute(sql, data)    
    con.commit()

# Send a notification to LINE
def line_notify(message):
    line_notify_token = os.environ['LINE_NOTIFY_API_KEY']
    line_notify_api = 'https://notify-api.line.me/api/notify'
    payload = {'message': message}
    headers = {'Authorization': 'Bearer ' + line_notify_token}
    requests.post(line_notify_api, data=payload, headers=headers)


# 1. Get Codeforces rate change history
user_name = os.environ['CODEFORCES_USER_NAME']
response = requests.get("https://codeforces.com/api/user.rating?handle=" + user_name)
json_data = response.json()

latest_contest_info = json_data["result"][-1]
contest_id = latest_contest_info["contestId"]
contest_name = latest_contest_info["contestName"]
rank = latest_contest_info["rank"]
rating_update_time_seconds = latest_contest_info["ratingUpdateTimeSeconds"]
old_rating = latest_contest_info["oldRating"]
new_rating = latest_contest_info["newRating"]

# 2. Check if latest contestId exists in DB. If not, insert the record and send the notification with LINE Notify
con = connect()
sql_search_latest_contest_info = "SELECT * FROM contests WHERE contest_id=" + str(contest_id)
res = search_DB(con,sql_search_latest_contest_info)

# If there is an update
if not res:
    # Insert the latest contest info. into DB
    insert_info = (contest_id, contest_name, rank, rating_update_time_seconds, old_rating, new_rating)
    sql_insert_latest_contest_info = "INSERT INTO contests VALUES(%s, %s, %s, %s, %s, %s)"
    insert_DB(con, sql_insert_latest_contest_info, insert_info)

    # send the Notification to LINE
    message = contest_name + "\n" + "Rate:" + str(old_rating) + "->" + str(new_rating)
    line_notify(message)

3. Herokuにデプロイする

ここまでで開発は終わってるので、後はHerokuにコード一式をデプロイしてちゃんと動くかを確認するだけです。まずはHerokuと連携させるGithubリポジトリのルートディレクトリに以下のファイルを追加していきます。

  • requirements.txt

必要なpythonのパッケージをHerokuに教えてあげるために必要です。以下コマンドで生成できます。ちなみに今回のツールだと、psycopg2とrequestsに関する情報だけあれば動きます。

create_requirements.txt
pip freeze > requirements.txt
  • runtime.txt

Herokuで動かすpythonのバージョンを指定するために必要です。以下コマンドで生成できますが、ぶっちゃけHerokuでサポートされてるバージョン一覧を直接見に行って指定するのが確実だと思います(サポートバージョンじゃないとデプロイエラーするので)。

create_runtime.txt
python --version > runtime.txt
  • procfile

Heroku上のWebアプリをどのコマンドで実行するかを記述します。今回だとmain.pyというソースコードを1つ動かすだけなので、中身を以下のようにすればオッケーです。ちなみに、僕はこのProcfileがUTF-8で保存されていなかったからか、デプロイ時に”push failed: can not parse Procfile”のエラーを吐かれました。文字コードには気をつけましょう(戒め)。

Procfile
web: python main.py

上記3ファイルが作成できたらmain.pyと併せてGithubにpushし、Herokuにデプロイしていきましょう(GithubとHerokuの連携等は、ここ参考です)!無事デプロイが成功したら、忘れずに”Heroku Scheduler” Add-onの設定から”python main.py”のjobを10分おきに実行するよう設定しておきましょう(参考)。

全く関係ないですけど今ってgithubのデフォルトブランチがmasterからmainに変わっているんですね。知らなくて焦りました。

4. こどふぉのコンテストを受けてみる

いよいよ実地試験です。デプロイしたちょうどその日にCodeforces Round #721 (Div. 2)が開催予定だったので、これ幸いとばかりに受けてみました。結果は2完でした(大量のWAとともに)。

さぁレート更新の通知は無事届くのか?ワクワクしながら眠りにつき翌朝スマホを確認すると…

LINEの通知1件!これは…?

Notify-cf-ratechange

うおおおおおおおおおおおお(歓喜)

まとめ

作り終えてみての感想ですが、今ってアプリ開発の敷居が本当に低くなっていますね。無料で使えるホスティングサイト等の便利なWebサービスが大量にありますし、Qiitaのようなノウハウ共有サイトもありますし。このツールも1日で作成できてびっくりしました。今後も思いついたらいろいろ試してみたいと思います。

今後やりたいこと

  • レートの増減に応じてコメント変えてみる(これは簡単ですね)
    2021/6/14追記:完了しました。Githubにもpush済です :)
  • FlaskやDjangoといったフレームワーク使ってWebアプリ開発してみる

参考にしたページ

以上です。