環境
OS:Windows10
エディタ:VSCode
言語:Python3.8.5
使用したモジュール[理解度:10段階]
標準モジュール
sys
[5]- コード実行時の引数を受け取るのに使用した。
pathlib
--パスをオブジェクトとして扱える標準モジュール[8]- パスをいじるのに使った。
re
--正規表現を扱う標準モジュール[5]- htmlからurl抜き取る際に正規表現が便利だった。
webbrowser
--urlをウェブブラウザで開ける標準モジュール[8]- 最後にwebページを確認したいためchromeを起動するのに使った。
json
--JSONとPythonオブジェクトを繋ぐ標準モジュール[10]- jsonには慣れているし、モジュールの復習として使った。
tempfile
--安全に一時ファイル/ディレクトリを作成できる標準モジュール[7]- テストデータでストレージを圧迫したくなかったため用いた。
subprocess
--サブプロセスを管理できる標準モジュール[5]- 提出コードをサブプロセスとして起動して、提出コードを一切書き換えないでテストできるようにした。
外部モジュール
requests
--HTTP通信を簡潔に書ける外部モジュール[3]- 公式ドキュメントでも推奨されている。(from:https://docs.python.org/ja/3/library/urllib.request.html#module-urllib.request , accessed on 2020/09/10)
BeutifulSoup4
--htmlを解析するのに便利な外部モジュール[2]- tagや属性をもとにhtmlの情報を抜き取ってこれる。
自動テストツール
次の2プロセスを実行する。
- webページからテストデータを取得する
- テストデータをもとにソースコードを実行する
下準備
ツールはターミナルから実行するとして、いちいちターミナルに書き込みたくないので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]
このinputs
とcorrects
を使ってソースコードの正否を調べれば良さそうだ。
テストデータをもとにソースコードを実行する
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つのプロセスからなる。
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 }
ソースコードを提出する
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なんてことざらにあります。
競プロに飽きたら簡単なニューラルネットワークを構築してみようと考えています。