AtCoderの提出コードを取得し、GitHubにプッシュする
AtCoderの提出コードを取得し、GitHubにプッシュする
AtCoderという競技プログラミングサイトで自分が提出したコードを保存したくなりました。
スクレイピングの練習も兼ねて自動で取得できるプログラムを作成したので記録に残します。
勝手に使用してもらって大丈夫ですが、ライブラリやChromeDriverのインストールの環境を作るのがちょっと大変かもしれません。
目的
- AtCoderで自分が提出したACコードを取得し、GitHubにプッシュする
仕様
- 提出した問題のうち、最新のACコードを取得する
- コンテストごとにフォルダを分けて、問題ごとにファイルを作成する
- リポジトリごとGitHubにプッシュする
言語
- Python (3.7.0)
ライブラリ
-
スクレイピング
- Selenium (3.141.0)
-
Git
- GitPython (3.1.14)
-
フォーマット
- clang-format (11.1.0)
ブラウザ
- Chrome Driver (89.0.4389.23.0)
API
- AtCoder Problems API
AtCoder Problems APIを用いたユーザの提出データのjsonの取得
自分が提出したコードの情報の取得にはAtCoder Problemsが提供してくれているAPIを感謝しながら使用します。
コード
import requests
import json
userID = "tishii24" # 自分のAtCoderのユーザーIDを設定する
unix_second = 0
api_path = "https://kenkoooo.com/atcoder/atcoder-api/v3/user/submissions?user=" + userID + "&from_second=" + str(unix_second)
# APIを用いた提出データの取得
def getSubmissionData(userID):
api_url = api_path + userID
response = requests.get(api_url)
jsonData = response.json()
return jsonData
submissions = getSubmissionData(userID)
最新のAC提出のデータのみに絞る
自分が提出したコードのうち、各問題において一番新しいACコードを取得したいので、以下のように古い順からDictに追加することにします。こうすることで、各問題において一番新しい提出が入るようになります。
最後にコンテストごとにディレクトリを作るためにコンテストごとにまとめて返します。
コード
# 各問題において最も新しいAC提出のみを取得する
# 各コンテストごとにまとめて返す
def collectNewestAcceptedSubmissions(submissions):
sortedData = sorted(submissions, key=lambda x: x['id']) # IDで昇順ソートすると古い順になる
submits = {} # 各問題ごとに最新の提出に更新する
for data in sortedData:
if data["result"] != "AC": # ACだった提出だけ対象
continue
submits[data["problem_id"]] = data
result = {} # コンテストごとにまとめる
for sub in submits.values():
if not sub["contest_id"] in result:
result[sub["contest_id"]] = []
result[sub["contest_id"]].append(sub)
return result
newestSubmits = collectNewestAcceptedSubmissions(submissions)
ディレクトリの作成
各コンテスト用にディレクトリを作ります。
os.makedirs()
にexist_ok=True
を指定することですでにディレクトリが作られていても大丈夫になります。
コード
import os
root = "submissions/"
for contestName in newestSubmits:
path = root + contestName
os.makedirs(path, exist_ok=True)
提出コードの取得
提出コードの取得をseleniumを使って行います。同時にファイルの作成と書き込み、フォーマットも行います。すでにACコードを取得した問題であれば取得しません。これには該当するファイルがあるかどうかで判定しています。また、AtCoderにアクセス負荷をかけるのはいいことじゃないので、取得は3秒ごとに行っています。
各問題ごとにファイルを作成し、そこに内容を書き込みます。
自分は基本的にC++とPythonでしか提出しないので、その二つの拡張子しか対応していません。
またC++の提出にはclang-formatを使ってフォーマットをかけます。
追記:
kumarstack55さんに不要な空白を取り除く処理を追加していただきました。ありがとうございます。
コード
import re
import html
import chromedriver_binary
from selenium import webdriver
from time import sleep
import subprocess
driver = webdriver.Chrome()
# 追加したファイルの数を増やす
add_cnt = 0
for submissions in newestSubmits.values():
for sub in submissions:
# 問題番号の取得
problem_num = sub["problem_id"][-1]
# 古い問題の場合には数字になっているので、アルファベットに戻す
if problem_num.isdigit():
problem_num = chr(int(problem_num)+ord('a')-1)
# 作成するファイルへのパス
path = root + sub["contest_id"] + "/" + problem_num
# 拡張子の設定(C++, Pythonのみ)
if "C++" in sub["language"]:
path += ".cpp"
elif "Python" in sub["language"]:
path += ".py"
# 既に提出コードがある場合は取得せず、次の問題の提出を探す
if os.path.isfile(path): continue
# 提出ページへアクセス
sub_url = "https://atcoder.jp/contests/" + sub["contest_id"] + "/submissions/" + str(sub["id"])
driver.get(sub_url)
# 提出コードの取得
code = driver.find_element_by_id("submission-code")
# code.text は提出時に含めていない空白が期待に反して含まれてしまう
# 空白はシンタックスハイライティングによるものであるように見える
# innerHTML から不要なタグなどを消し、空白が意図通りのテキストを得る
inner_html = code.get_attribute('innerHTML')
list_items = re.findall(r'<li[^>]*>.*?</li>', inner_html)
lines = []
for li in list_items:
line1 = re.sub(r'<[^>]+>', '', li)
line2 = re.sub(r' ', '', line1)
line3 = html.unescape(line2)
lines.append(line3 + "\n")
code_text = ''.join(lines)
# 書き込み
with open(path, 'w') as f:
f.write(code_text)
# C++の場合にはclang-formatを使ってフォーマットする
if "C++" in sub["language"]:
subprocess.call(["clang-format", "-i", "-style=file", path])
# 追加したファイルの数を増やす
add_cnt += 1
# アクセス負荷軽減のために時間をおく(3秒)
sleep(3)
driver.quit()
.clang-formatの中身
ちなみに自分のclang-formatの中身は以下のようにしています。
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 0
GitHubにプッシュする
最後にコミットし、GitHubにプッシュします。
コード
if add_cnt == 0:
# 何も追加していなければGitにアクセスしない
print("No added submissions, end process")
else:
# GitHubにプッシュ
import git
import datetime
dt_now = datetime.datetime.now()
repo_url = "https://github.com/tishii2479/atcoder.git"
repo = git.Repo()
repo.git.add("submissions/*")
repo.git.commit("submissions/*", message="add submission: " + dt_now.strftime('%Y/%m/%d %H:%M:%S'))
repo.git.push("origin", "main")
print(f"Finished process, added {add_cnt} files")
使い方
- リポジトリとするフォルダを作ってそこに移動します。
-
git init
をしてリポジトリを作ります、リモートにプッシュする場合はリモートリポジトリも作り、紐付けます。 -
root
にfetch_submission.py
というファイルを作り、コードを貼り付けます。 - terminalで
$ python3 fetch_submission.py
でOK。 - フォルダ内にsubmissionというフォルダが作られ、提出コードがそこに入ります。
最後に
完全に自分用に作ってしまったので、誰にでも導入しやすいものではないかもしれないです。
疑問点、指摘点あればください。
自分のレポジトリ
AtCoderアカウント
コード全体
#!/usr/bin/env python
# coding: utf-8
# In[3]:
import requests
import json
# In[4]:
userID = "tishii24" # 自分のAtCoderのユーザーIDを設定する
unix_second = 0
api_path = "https://kenkoooo.com/atcoder/atcoder-api/v3/user/submissions?user=" + userID + "&from_second=" + str(unix_second)
# In[5]:
# APIを用いた提出データの取得
def getSubmissionData(userID):
api_url = api_path + userID
response = requests.get(api_url)
jsonData = response.json()
return jsonData
# In[6]:
submissions = getSubmissionData(userID)
submissions[:2]
# In[7]:
# 各問題において最も新しいAC提出のみを取得する
# 各コンテストごとにまとめて返す
def collectNewestAcceptedSubmissions(submissions):
sortedData = sorted(submissions, key=lambda x: x['id']) # IDで昇順ソートすると古い順になる
submits = {} # 各問題ごとに最新の提出に更新する
for data in sortedData:
if data["result"] != "AC": # ACだった提出だけ対象
continue
submits[data["problem_id"]] = data
result = {} # コンテストごとにまとめる
for sub in submits.values():
if not sub["contest_id"] in result:
result[sub["contest_id"]] = []
result[sub["contest_id"]].append(sub)
return result
# In[8]:
newestSubmits = collectNewestAcceptedSubmissions(submissions)
newestSubmits["abc168"][0]
# In[9]:
import os
# In[10]:
root = "submissions/"
for contestName in newestSubmits:
path = root + contestName
os.makedirs(path, exist_ok=True)
# In[11]:
import re
import html
import chromedriver_binary
from selenium import webdriver
from time import sleep
import subprocess
# In[14]:
driver = webdriver.Chrome()
# 追加したファイルの数を増やす
add_cnt = 0
for submissions in newestSubmits.values():
for sub in submissions:
# 問題番号の取得
problem_num = sub["problem_id"][-1]
# 古い問題の場合には数字になっているので、アルファベットに戻す
if problem_num.isdigit():
problem_num = chr(int(problem_num)+ord('a')-1)
# 作成するファイルへのパス
path = root + sub["contest_id"] + "/" + problem_num
# 拡張子の設定(C++, Pythonのみ)
if "C++" in sub["language"]:
path += ".cpp"
elif "Python" in sub["language"]:
path += ".py"
# 既に提出コードがある場合は取得せず、次の問題の提出を探す
if os.path.isfile(path): continue
# 提出ページへアクセス
sub_url = "https://atcoder.jp/contests/" + sub["contest_id"] + "/submissions/" + str(sub["id"])
driver.get(sub_url)
# 提出コードの取得
code = driver.find_element_by_id("submission-code")
# code.text は提出時に含めていない空白が期待に反して含まれてしまう
# 空白はシンタックスハイライティングによるものであるように見える
# innerHTML から不要なタグなどを消し、空白が意図通りのテキストを得る
inner_html = code.get_attribute('innerHTML')
list_items = re.findall(r'<li[^>]*>.*?</li>', inner_html)
lines = []
for li in list_items:
line1 = re.sub(r'<[^>]+>', '', li)
line2 = re.sub(r' ', '', line1)
line3 = html.unescape(line2)
lines.append(line3 + "\n")
code_text = ''.join(lines)
# 書き込み
with open(path, 'w') as f:
f.write(code_text)
# C++の場合にはclang-formatを使ってフォーマットする
if "C++" in sub["language"]:
subprocess.call(["clang-format", "-i", "-style=file", path])
# 追加したファイルの数を増やす
add_cnt += 1
# アクセス負荷軽減のために時間をおく(3秒)
sleep(3)
driver.quit()
# In[16]:
if add_cnt == 0:
# 何も追加していなければGitにアクセスしない
print("No added submissions, end process")
else:
# GitHubにプッシュ
import git
import datetime
dt_now = datetime.datetime.now()
repo_url = "https://github.com/tishii2479/atcoder.git"
repo = git.Repo()
repo.git.add("submissions/*")
repo.git.commit("submissions/*", message="add submission: " + dt_now.strftime('%Y/%m/%d %H:%M:%S'))
repo.git.push("origin", "main")
print(f"Finished process, added {add_cnt} files")
Discussion
有益な情報をありがとうございます。
code.text
を使うと、期待しない空白が含まれるようでした。code.text
の代わりに、次のようにすると不要な空白などが含まれないコードを得られそうです。
何かの役に立てばと思い、コメントします。
コメントありがとうございます。
確かに実際の提出コードと違ったものが取得されていました。
記事にも追記させていただきます。