6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AtCoderをrequestsで自動ログインする

Last updated at Posted at 2022-12-19

はじめに

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に自動ログインし、コンテスト名のフォルダを作成し、各問題名のファイルを作成し、テンプレートを貼り付けるだけです。
image.png

インストール

プログラムを使用する前に以下のライブラリをダウンロードしてください。

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/

全コード

scrapeABC.py
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するデータがクエリ形式だった

image.png
image.png
chromeの開発者モードのネットワークタブを開きながらログインすると、そのとにhttpメソッドの情報を見ることができる。一般的に、POSTメソッドはjson形式でデータを送信するが、atcoderのサイトはクエリ形式で行う。クエリ形式とはusername=xxxxxxxxx&password=xxxxxxxxx&csrf_token=hmyQfF%2Botanxyv%2BDUMCKYiLViAtM5s5F%2BbXdo9xxfVQ%3Dのような各パラメータを=&で繋げる形式である。そのため、postメソッドのparamsにクエリとしてのusernamepasswordcsrf_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')が入るため消してあげよう。
image.png

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>
6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?