🐈

AtCoderの提出コードを取得し、GitHubにプッシュする

2021/04/13に公開
2

AtCoderの提出コードを取得し、GitHubにプッシュする

AtCoderという競技プログラミングサイトで自分が提出したコードを保存したくなりました。
スクレイピングの練習も兼ねて自動で取得できるプログラムを作成したので記録に残します。
勝手に使用してもらって大丈夫ですが、ライブラリやChromeDriverのインストールの環境を作るのがちょっと大変かもしれません。

https://atcoder.jp/

目的

  • 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を用いたユーザの提出データのjsonの取得

自分が提出したコードの情報の取得にはAtCoder Problemsが提供してくれているAPIを感謝しながら使用します。

https://github.com/kenkoooo/AtCoderProblems/blob/master/doc/api.md#submission-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'&nbsp;', '', 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")

使い方

  1. リポジトリとするフォルダを作ってそこに移動します。
  2. git initをしてリポジトリを作ります、リモートにプッシュする場合はリモートリポジトリも作り、紐付けます。
  3. rootfetch_submission.pyというファイルを作り、コードを貼り付けます。
  4. terminalで$ python3 fetch_submission.pyでOK。
  5. フォルダ内にsubmissionというフォルダが作られ、提出コードがそこに入ります。

最後に

完全に自分用に作ってしまったので、誰にでも導入しやすいものではないかもしれないです。
疑問点、指摘点あればください。

自分のレポジトリ

https://github.com/tishii2479/atcoder

AtCoderアカウント

https://atcoder.jp/users/tishii24

コード全体

#!/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'&nbsp;', '', 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 の代わりに、
次のようにすると不要な空白などが含まれないコードを得られそうです。
何かの役に立てばと思い、コメントします。

    driver.get(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'&nbsp;', '', line1)
        line3 = html.unescape(line2)
        lines.append(line3 + "\n")
    code_text = ''.join(lines)
tishii2479tishii2479

コメントありがとうございます。

確かに実際の提出コードと違ったものが取得されていました。
記事にも追記させていただきます。