はじめに
WINC(早稲田コンピュータ研究会) Advent Calendar 2022 20日目の記事です。
僕は最近C++でAtCoderをはじめました。過去問を解いてる際に、よくファイル名をA.cpp
とパフォーマンスだけではなく、検索しやすいようにA_contest_name.cpp
と問題名をつけて管理してました。しかし、その作業が意外とめんどくさいので、自動でそれをやってくれるコードを書きました(ほぼ需要はない)。また、本コードはrequestsというpythonのライブラリを主に使っています。次に少しだけrequestsの解説をします。
requestsとは
Requests is an elegant and simple HTTP library for Python, built for human beings.
公式サイトによると、requestsとはhttpを簡単に扱えるpythonのライブラリです。
これを使えば、APIへ自由にアクセスができ、BeautifulSoupと組み合わせクレイピングができます。sessionを保持しておくことができるので、ログインが必要なサイトでも同様に使えます。また、さまざまなデータ形式をサポートしているため、一括にファイルをアップロードや削除できないサイトのために、効率化コードを書くこともできます。便利なのでみんな使ってみれください。
プログラムについて
概要
プログラムがやっていることは非常にシンプルで、AtCoderに自動ログインし、コンテスト名のフォルダを作成し、各問題名のファイルを作成し、テンプレートを貼り付けるだけです。
インストール
プログラムを使用する前に以下のライブラリをダウンロードしてください。
python3 -m pip install logging
python3 -m pip install tqdm
python3 -m pip install validators
python3 -m pip install beautifulsoup4
python3 -m pip install requests
サンプルコード
(1) input contest name
python3 scrape.py abc278, python3 scrape.py abs
or
(2) input contest url
python3 scrapeABC.py https://atcoder.jp/contests/abc278
python3 scrapeABC.py https://atcoder.jp/contests/abc278/
python3 scrapeABC.py https://atcoder.jp/contests/abc278/tasks
python3 scrapeABC.py https://atcoder.jp/contests/abc278/tasks/
全コード
import logging
import requests
import os, sys, re
from bs4 import BeautifulSoup
import validators
import urllib.parse
from tqdm import tqdm
from logging import getLogger
from time import sleep
logging.basicConfig(format = '%(asctime)s (%(msecs)03dmsec) --%(module)s.py: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
logger = getLogger(__name__) # ログの設定
# ファイルにコピーされるテンプレート(カスタマイズ可)
TEMPLATE = \
"""
#include <bits/stdc++.h>
using namespace std;
#define rep(i, s, n) for (int i = (s); i < (int)(n); i++) // rep(i, 0, N) cin >> D[i]
#define all(v) v.begin(), v.end() // sort(all(v));
//---------------------------------------------------------------------------------------------------
"""
EXTENSION = 'cpp'
# 各自のユーザー名とパスワード
USERNAME = ''
PASSWORD = ''
def login():
login_url = "https://atcoder.jp/login"
session = requests.session() # sessionを作成
res = session.get(login_url) # cookieを取得するためにgetでアクセス
revel_session = res.cookies.get_dict()['REVEL_SESSION'] # revel_sessionを取得
revel_session = urllib.parse.unquote(revel_session) # revel_sessionをデコード
csrf_token = re.search(r'csrf_token\:(.*)_TS', revel_session).groups()[0].replace('\x00\x00', '') # csrf_tokenを正規表現で取得し、余分な文字を除去する
sleep(1) # 取得するまで待つ
headers = {'content-type': 'application/x-www-form-urlencoded'} # ヘッダーの定義
params = {
'username': USERNAME,
'password': PASSWORD,
'csrf_token': csrf_token,
} # クエリーの定義
data = {
'continue': 'https://atcoder.jp/'
} # データの定義
res = session.post(login_url, params=params, data=data, headers=headers) # 必要な情報を使ってpostでログイン
res.raise_for_status() # http statusが異常なら例外を起こす
return session
def validate_url(url):
# コンテスト名とURLのバリデーション
if(not validators.url(url)):
url = f'https://atcoder.jp/contests/{url}'
# URLの'/tasks'や'/'の処理
url = url[:-1] if (url[-1] == '/') else url
url = url if ('tasks'in url) else url + '/tasks'
return url
def main(key):
# ログイン
session = login()
url = validate_url(key)
# コンテストページから各問題のリンクを取得
base_url = 'https://atcoder.jp'
try:
response = session.get(url).text
sleep(1)
except requests.exceptions.RequestException as e:
logger.error('invalid url or contest name')
exit()
# bs4で読み込む
soup = BeautifulSoup(response, features="lxml")
# 各問題のリンクを取得
td_list = soup.find_all('td', class_='text-center no-break')
link_list = [td.find('a').get('href') for td in td_list]
access_url_list = [base_url + link for link in link_list]
# コンテスト名からフォルダを作成
try:
contest_title = soup.find('a', class_='contest-title').contents[0]
contest_title = contest_title.strip().replace(' - ', '_').replace(' ', '_')
os.makedirs(contest_title, exist_ok=True)
except Exception as e:
logger.error('contest name is not exist')
exit()
# 各問題のタイトルを取得し、ファイルを作成
for url in tqdm(access_url_list):
response = session.get(url).text
soup = BeautifulSoup(response, features="lxml") # bs4で読み込む
title = soup.find('span', class_='h2').contents[0]
title = title.strip().replace(' - ', '_').replace(' ', '_') # スペースやハイフン(-)をアンダーバー(_)に変換
filename = f'{contest_title}/{title}.{EXTENSION}' # プログラムファイルを生成
with open(filename, 'w', encoding='utf-8') as f:
f.writelines(TEMPLATE.strip())
if __name__ == '__main__':
try:
key = sys.argv[1]
except IndexError as e:
logger.error('please input url or contest name')
exit()
main(key)
コード解説
ライブラリをインストール
import logging
import requests
import os, sys, re
from bs4 import BeautifulSoup
import validators
import urllib.parse
from tqdm import tqdm
from logging import getLogger
from time import sleep
テンプレート、拡張子を定義。自分のユーザー名、パスワードを定義。
# ファイルにコピーされるテンプレート(カスタマイズ可)
TEMPLATE = \
"""
#include <bits/stdc++.h>
using namespace std;
#define rep(i, s, n) for (int i = (s); i < (int)(n); i++) // rep(i, 0, N) cin >> D[i]
#define all(v) v.begin(), v.end() // sort(all(v));
//---------------------------------------------------------------------------------------------------
"""
EXTENSION = 'cpp'
USERNAME = ''
PASSWORD = ''
ログイン処理をする関数、詳しくはコメント参照。
def login():
login_url = "https://atcoder.jp/login"
session = requests.session() # sessionを作成
res = session.get(login_url) # cookieを取得するためにgetでアクセス
revel_session = res.cookies.get_dict()['REVEL_SESSION'] # revel_sessionを取得
revel_session = urllib.parse.unquote(revel_session) # revel_sessionをデコード
csrf_token = re.search(r'csrf_token\:(.*)_TS', revel_session).groups()[0].replace('\x00\x00', '') # csrf_tokenを正規表現で取得し、余分な文字を除去する
sleep(1) # 取得するまで待つ
headers = {'content-type': 'application/x-www-form-urlencoded'} # ヘッダーの定義
params = {
'username': USERNAME,
'password': PASSWORD,
'csrf_token': csrf_token,
} # クエリーの定義
data = {
'continue': 'https://atcoder.jp/'
} # データの定義
res = session.post(login_url, params=params, data=data, headers=headers) # 必要な情報を使ってpostでログイン
res.raise_for_status() # http statusが異常なら例外をを起こす
return session # ログイン済みのsessionを返す
URLのバリデーション処理をする関数、詳しくはコメント参照。
入力がコンテスト名であればそれをもとにコンテストのURLを返し、URL名であればそのまま返す。
def validate_url(url):
# コンテスト名とURLのバリデーション
if(not validators.url(url)):
url = f'https://atcoder.jp/contests/{url}'
# URLの'/tasks'や'/'の処理
url = url[:-1] if (url[-1] == '/') else url
url = url if ('tasks'in url) else url + '/tasks'
return url
main処理の関数、詳しくはコメント参照。
def main(key):
# ログイン
session = login()
url = validate_url(key)
# コンテストページから各問題のリンクを取得
base_url = 'https://atcoder.jp'
try:
response = session.get(url).text
sleep(1)
except requests.exceptions.RequestException as e:
logger.error('invalid url or contest name')
exit()
# bs4で読み込む
soup = BeautifulSoup(response, features="lxml")
# 各問題のリンクを取得
td_list = soup.find_all('td', class_='text-center no-break')
link_list = [td.find('a').get('href') for td in td_list]
access_url_list = [base_url + link for link in link_list]
# コンテスト名からフォルダを作成
try:
contest_title = soup.find('a', class_='contest-title').contents[0]
contest_title = contest_title.strip().replace(' - ', '_').replace(' ', '_')
os.makedirs(contest_title, exist_ok=True)
except Exception as e:
logger.error('contest name is not exist')
exit()
# 各問題のタイトルを取得し、ファイルを作成
for url in tqdm(access_url_list):
response = session.get(url).text
soup = BeautifulSoup(response, features="lxml") # bs4で読み込む
title = soup.find('span', class_='h2').contents[0]
title = title.strip().replace(' - ', '_').replace(' ', '_') # スペースやハイフン(-)をアンダーバー(_)に変換
filename = f'{contest_title}/{title}.{EXTENSION}' # プログラムファイルを生成
with open(filename, 'w', encoding='utf-8') as f:
f.writelines(TEMPLATE.strip())
if __name__ == '__main__':
try:
key = sys.argv[1]
except IndexError as e:
logger.error('please input url or contest name')
exit()
main(key)
沼りポイント
1.ログイン時にPOSTするデータがクエリ形式だった
chromeの開発者モードのネットワークタブを開きながらログインすると、そのとにhttpメソッドの情報を見ることができる。一般的に、POSTメソッドはjson形式でデータを送信するが、atcoderのサイトはクエリ形式で行う。クエリ形式とはusername=xxxxxxxxx&password=xxxxxxxxx&csrf_token=hmyQfF%2Botanxyv%2BDUMCKYiLViAtM5s5F%2BbXdo9xxfVQ%3D
のような各パラメータを=
と&
で繋げる形式である。そのため、post
メソッドのparams
にクエリとしてのusername
、password
、csrf_token
を格納し、data
にパラメータのcontinue
を格納した。
params = {
'username': USERNAME,
'password': PASSWORD,
'csrf_token': csrf_token,
} # クエリーの定義
data = {
'continue': 'https://atcoder.jp/'
} # データの定義
res = session.post(login_url, params=params, data=data, headers=headers)
2.CSRFトークンを取得する必要があった
CSRF(Cross-site Request Forgery)トークンとは、正規のページからアクセスが行われていることを証明するための値です。そのため、一回get
メソッドでログインurlにアクセスし、そのcookieを取得する必要があります。
res = session.get(login_url) # cookieを取得するためにgetでアクセス
cookieを辞書型に変更し、REVEL_SESSION
を取り出す。
revel_session = res.cookies.get_dict()['REVEL_SESSION'] # revel_sessionを取得
一部記号が%
にエンコードされるため、デコードする。
revel_session = urllib.parse.unquote(revel_session) # revel_sessionをデコード
正規表現でcsrf_token
を取得する、パターンは下の図の桁数と見比べながら書いた。また、最後に変な文字コード('\x00\x00'
)が入るため消してあげよう。
csrf_token = re.search(r'csrf_token\:(.*)_TS', revel_session).groups()[0].replace('\x00\x00', '') # csrf_tokenを正規表現で取得し、余分な文字を除去する
sleep(1) # 取得するまで待つ
※補足
最初は以下のようにREVEL_CSRF
に関してエラーが出ていたため、REVEL_CSRF
に関してググったら似たことをやってる記事を見つけて面白かった。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Forbidden</title>
</head>
<body>
<h1>
Forbidden
</h1>
<p>
REVEL_CSRF: tokens mismatch.
</p>
</body>
</html>