Atcoder自動テスト・提出ツールを自作したメモ

環境

OS:Windows10
エディタ:VSCode
言語:Python3.8.5

使用したモジュール[理解度:10段階]

標準モジュール
  • sys[5]
    • コード実行時の引数を受け取るのに使用した。
  • pathlib--パスをオブジェクトとして扱える標準モジュール[8]
    • パスをいじるのに使った。
  • re--正規表現を扱う標準モジュール[5]
    • htmlからurl抜き取る際に正規表現が便利だった。
  • webbrowser--urlをウェブブラウザで開ける標準モジュール[8]
    • 最後にwebページを確認したいためchromeを起動するのに使った。
  • json--JSONPythonオブジェクトを繋ぐ標準モジュール[10]
    • jsonには慣れているし、モジュールの復習として使った。
  • tempfile--安全に一時ファイル/ディレクトリを作成できる標準モジュール[7]
    • テストデータでストレージを圧迫したくなかったため用いた。
  • subprocess--サブプロセスを管理できる標準モジュール[5]
    • 提出コードをサブプロセスとして起動して、提出コードを一切書き換えないでテストできるようにした。
外部モジュール

自動テストツール

次の2プロセスを実行する。

  1. webページからテストデータを取得する
  2. テストデータをもとにソースコードを実行する

下準備

ツールはターミナルから実行するとして、いちいちターミナルに書き込みたくないのでVSCodeのショートカットを設定する。
ctrl+Shift+Pでコマンドパレットを開いて「基本設定:キーボードショートカットを開く(JSON)」を選ぶ。パレットに「shortcut」等打つと出てくる。
JSONファイルが開くので、

    },
    {
        "key": "f6",
        "command": "workbench.action.terminal.sendSequence",
        "args": {
            "text": "python .\\atcoder_tester.py ${file} \n"
        }
    }

のように任意のショートカットキーとコマンドを追加する。
コマンド"workbench.action.terminal.sendSequence"で"args"の"text"をターミナルに送ることができる。
カレントファイル${file} を引数としてツールのソースを改行"\n"で実行する。
ここで、${file}VSCodeでサポートされている変数で、https://code.visualstudio.com/docs/editor/variables-reference (accessed on 2020/09/10)で確認することができる。

webページからテストデータを取得する

私はファイル名と問題が1対1で対応付いているため、そこからアクセスするURLを作成した。
基本的には"https://atcoder.jp/contests/abc047/tasks/abc047_a"のような形式だが、稀に"https://atcoder.jp/contests/abc063/tasks/arc075_a"のように末尾が異なることがあるため、https://atcoder.jp/contests/abc063/tasksからURLを取得するように工夫した。

ここは省略して、得たURLをprob_urlとして進める。

サーバからレスポンスを受け取る

レスポンスにはhtmlといった欲しい情報が含まれている。
requests.get(url)メソッドはレスポンスをオブジェクトとして返す関数である。
詳しくは知らないのだがhttpにはget, post, put, deleteなどのメソッドがあり、それを実行しているようだ。

import requests
response = requests.get(prob_url)

なにかしら通信の問題が起きたときにエラーを吐いてプログラムを終了するには、

response.raise_for_status()

を追加しておく。どのエラーコードで停止するかは知らない。

レスポンスからhtmlを抽出する
import bs4
soup = bs4.BeautifulSoup(response.content, 'html.parser')

response.contentでhtmlを取得して、標準ライブラリのhtml.parerでsoupオブジェクトに変換する。パーサは他にもあるが動けばよいということで標準のものを使った。

soupから欲しいものを取り出す

tagとかnameとかattrとか色々抽出メソッドがある。実際、prob_urlを得るのには正規表現を使って抽出した。Atcoderのページソースを見ると、サンプル入出力はpreタグが付いているのでそれで抽出。

examples = [tag.text for tag in soup.find_all('pre')]
データを加工
# 日本語と英語で各データ2回含まれるので半分に
examples = examples[:len(examples)//2] 
# example[0]は入力形式なのでカット
inputs = examples[1::2]
corrects = examples[2::2]

このinputscorrectsを使ってソースコードの正否を調べれば良さそうだ。

テストデータをもとにソースコードを実行する

import tempfile
import subprocess as sbp
# inputsの全てについてcode.pathを実行して結果をcorrectsと比較
for i, (_input, correct) in enumerate(zip(inputs, corrects)):
    # 一時ファイルobjを作成して、そこにinptを書き込む
    with tempfile.TemporaryFile(mode='w') as f: 
        f.write(_input)
        f.seek(0)
        try:
            # subprocessモジュールのドキュメント参照
            result = sbp.run(['python', code.path], stdin=f, encoding='UTF-8', stdout=sbp.PIPE, check=True).stdout
        # ソースコードにミスがあってプロセスが完遂できないエラーを検知
        except sbp.CalledProcessError: 
            raise Exception('The code has an error')
    # 出力
    print(f'Example{i+1}:', result.strip() == correct.strip()) 

code.pathとは提出予定のソースコードのパスである。パス自体はターミナルでツールを起動する際に引数として与えている。
(書いていない部分でCodeクラスを定義していて、そのインスタンスpathプロパティである)
subprocessモジュールのドキュメント:
https://docs.python.org/ja/3/library/subprocess.html (accessed on 2020/09/10)
簡単に説明すると、第一引数に渡された文字列コンテナを半スペースで繋げたコマンドが実行されて、その際の標準入力をファイルオブジェクトで指定したりできるわけである。

以上が自動テストツールの概形と必要なライブラリ・メソッドだ。

自動提出ツール

VSCodeのショートカットの作り方は同様。
自動提出ツールは次の3つのプロセスからなる。

  1. Atcoderにログインする
  2. ログイン状態のままサーバにpostリクエストを行う
  3. webブラウザを立ち上げて提出の成否、及び提出コードの正誤を目視で確認する

Atcoderにログインする

cookieを記憶してログイン状態の接続を保つためにrequests.session()でセッションオブジェクトを確保する。ログインページのURLLOGIN_URLも用意する。

session = requests.session()
LOGIN_URL = 'https://atcoder.jp/login'
csrf_tokenを取得する

セキュリティ用のワンタイムトークン。LOGIN_URLにアクセスして得たhtmlから抜き出す。

#サンプルメソッド
def get_csrf_token(self, url):
    response = self.session.get(url)
    response.raise_for_status()
    soup = BeautifulSoup(response.content, "html.parser")
    csrf_token = soup.find(attrs={'name':'csrf_token'}).get('value')
    return csrf_token

csrf_token, username, password を一つの辞書型オブジェクトlogin_infoにまとめる。
username, passwordは別ファイルから読み込んでくる。

login_info = {
    # あるクラス内に作成したためselfが付いている
     'csrf_token': self.get_csrf_token(LOGIN_URL), 
     'username': username,
     'password': password
}
LOGIN_URLにpostリクエストを送る

login_infoを引数dataに格納して、session.post()を使ってLOGIN_URLにpostリクエストを送る。

session.post(LOGIN_URL, data=login_info)

ログインする前にget(問題文URL)で得たレスポンスobjとログイン後のget(問題文URL)で得たレスポンスobjはログインが成功していた場合異なるため==で評価することができる。
成功すればsessionはログイン状態を保持するので、それを使って同じように提出用URLにpostリクエストを送る。

ソースコードを提出する

post()で送るデータを確認する

chromeの開発者ツール(f12)を開いた状態で実際に何か一つファイルを提出すると、postリクエストの送り先と送った情報がわかる。submitの中身を見ると

General
Request URL: https://atcoder.jp/contests/{contest_name}/submit

From Data
data.TaskScreenName:~~
data.LanguageId:~~
sourceCode:~~
csrf_token:~~

と書かれているので、これら4つのデータをhtmlから取得したり、ファイルから読み込んだりすれば良い。

#サンプルメソッド
def submit_code(self):
    submit_url = f'https://atcoder.jp/contests/{self.code.contest_name}/submit'
        
    submit_info = {
        'csrf_token': self.get_csrf_token(submit_url),
        'data.TaskScreenName': self.code.task_screen_name,
        'data.LanguageId': 4006, # Python 多言語対応予定
        'sourceCode': self.code.source_code
    }
    self.session.post(submit_url, data=submit_info).raise_for_status()
    return None

 (私はAtcoderWebsiteというクラスを作成し、その中にこれらのメソッドを埋め込んだ)

実行結果確認

 最後にwebブラウザを開いて、提出されて正解したかを確認する。
(目視する必要のない場合は飛ばしてよい)
webブラウザを開くにはwebbrowserモジュールを使い、webbrowser.open(url)だけで良い。
デフォルトのブラウザでurlが開かれる。
詳細はドキュメント参照。
https://docs.python.org/ja/3/library/webbrowser.html (accessed on 2020/09/10)

import webbrowser
url = f'https://atcoder.jp/contests/{contest_name}/submissions/me'
webbrowser.open(url, new=2)

 

感想とか

競プロ始めてまだ1か月半弱ですが、ツール作り始めたら楽しくて徹夜で作っちゃいました。アルゴリズムを勉強した後は計算量削っていくのが楽しくてまだまだ飽きそうにないです。
競プロは初めはCで馴染みのあったC++で初めて半月くらいやってたのですが、今は専らPythonの標準モジュールの勉強ばかりしてます。Pythonの外部ライブラリの多さまで考えると、再びC++を勉強することは可能でしょうか?
ポインタ/イテレータや基本的なアルゴリズムの実装は一通りC++で勉強したのでC/C++が無駄だったとは感じませんが、Java、js、あわよくばTypeScriptやC#もやってみたいので時間が足りません!
当分の目標はPython標準モジュール及びNumpyの8割カバーです。Python、ドキュメント読めば読むほどコードが簡潔になっていくのが素晴らしい。初期にPythonで解いた問題のコードが今は1/2なんてことざらにあります。
競プロに飽きたら簡単なニューラルネットワークを構築してみようと考えています。