順位ヒストグラムの定期スクレイピングによるログ作成!【AtCoder】【レーティング分布】

(2023年7月31日更新)AtCoderのレーティング分布の時系列変化をグラフにしました(随時更新予定)

ごごちと申します。

前回、AtCoderのヒストグラムの可視化グラフを紹介しました

また、HTMLから順位データをプログラムで処理するコードを紹介しました

今回はスクレイピングプログラムを作り、
AtCoderの順位ヒストグラムを定期取得する
プログラムを紹介します。
OSはwindows10です。

スクレイピングプログラムの実行

こちらのプログラムを実行しますと

実行時点の日時でフォルダと
エクセルファイルが生成されます。

実行結果

順位ヒストグラム画像

フォルダの中には4つの画像があります。

1. 順位ヒストグラム

2. 累積ヒストグラム(全レーティング)

3. 累積ヒストグラム(レーティング1200未満)

4. 累積ヒストグラム(レーティング1200以上)

ログ結果エクセルファイル

現時点では
「Graph」,「hist」,「cumsum」
の3つのシートを作成しています。
ログが増えていったら他のシートも
作ってみたいと思います。

Graphシート

開いたときに図が一望できるようにしました。

hist シート

日付indexの降順に各レーティングの
人数を記録していきます。

cumsum シート

日付indexの降順に各レーティングの
順位パーセンテージを記録していきます。

実行コード

工夫点

・実行時点で一番直近の
ログエクセルファイルを読み込み、
実行時のレコードを追加する。

・実行時点でログエクセルファイルが
なかったら新しくファイルを生成する。

・前回取得時のデータと差分が無かったら
処理を終了する。

・実行日時が付与されたディレクトリを
新規作成し、生成したグラフを保存する。

実行環境について

実行環境などは以前紹介しましたので
興味があればこちらをご覧ください。
DebankのPythonスクレイピングプログラムの環境構築!chromedriverまで解説!

pythonスクリプト

import pandas as pd
import numpy as np
import time
from datetime import datetime
import openpyxl
from openpyxl import load_workbook
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.styles import Font
import os.path
import glob
from selenium import webdriver
from selenium.webdriver.common.by import By
import matplotlib.pyplot as plt
import re

# グラフ作成関数
def make_plot(x, y, color, title,y_label,offset):
    
    x_label=x.astype("str")
    fig, ax = plt.subplots(figsize=(30, 32))
    #fig, ax = plt.subplots()
    rect = ax.bar(x_label, y, color=color)
    if title=='Participants':
        ax.set_title(f'{sum(y)} {title} ({current_date})', fontsize=44, pad=24)
    else:
        ax.set_title(f'{title} ({current_date})', fontsize=44, pad=24)
    ax.set_xticks(range(len(x))) # x軸の目盛りの位置を指定
    ax.set_xticklabels(x, rotation=90)
    ax.set_ylabel(y_label, labelpad=24, fontsize=36)
    if title=='Participants':
        add_value_label(x_label, y, offset)
    elif title=='CumSumRank':
        add_value_label(x_label, np.round(y,1), offset)
    elif title=="CumSumRank Rating < 1200":
        add_value_label(x_label, np.round(y,1), offset)
    elif title=="CumSumRank Rating >= 1200":
        add_value_label(x_label, np.round(y,3), offset)
            
    plt.subplots_adjust(bottom=0.1, top=0.95, left=0.1, right=0.95)
    return fig

        
# データラベルを張り付けるための関数
def add_value_label(x_list,y_list,offset):
    for i in range(0, len(x_list)):
        plt.text(x_list[i],y_list[i]+offset, y_list[i], ha='center', color='black',rotation=90) 
               #(x座標,y座標,表示するテキスト)


# フォントサイズの設定
plt.rcParams['font.size'] = 30
plt.rcParams['font.family'] = 'Arial'


#chromeでアクセス
options = webdriver.ChromeOptions()

#########################################################################################################################################
# chromedriver.exeの保存場所を引数にしてください
service = webdriver.chrome.service.Service(executable_path = "C:\\Users\\XXXXXXX\\Desktop\\AtCoder_scraping\\chromedriver.exe")

#########################################################################################################################################

driver = webdriver.Chrome(service=service, options=options)

#########################################################################################################################################
# 順位ヒストグラムが見えるページを入力してください
driver.get('https://atcoder.jp/users/xxxxxxxxxx?graph=dist')

#########################################################################################################################################
time.sleep(5)

overall = driver.find_element(By.XPATH, '//*[@id="main-container"]/div[1]/div[3]/div/script[2]')
data_str = overall.get_attribute("text")
    
# 前半のリストを取得
xaxis_list = re.findall(r'\d+', re.search(r'var xaxis = \[(.*?)\];', data_str).group(1))
xaxis_list = list(map(int, xaxis_list))

# 後半のリストを取得
data_list = re.findall(r'\d+', re.search(r'var data = \[(.*?)\];', data_str).group(1))
data_list = list(map(int, data_list))

# numpy配列に変換して累積和を計算
var_xaxis = np.array(xaxis_list)
var_data = np.array(data_list)
cum_var= np.round((1-var_data.cumsum()/var_data.sum())*100,6)

# 横軸用にnumpyの文字列の配列としておく
xlabel = var_xaxis.astype('str')

# x軸の生成
x=np.arange(1,len(var_data)+1)

#色のリストを作る
color = []
for x in var_xaxis:
    if x < 400:
        color.append('gray')
    elif x < 800:
        color.append('brown')
    elif x < 1200:
        color.append('green')
    elif x < 1600:
        color.append('aqua')
    elif x < 2000:
        color.append('blue')
    elif x < 2400:
        color.append('y')
    elif x < 2800:
        color.append('orange')
    else:
        color.append('red')


# 現在の日付を取得
current_date = datetime.now().strftime("%Y%m%d_%H%M%S")

# numpy配列に変換して累積和を計算
var_xaxis = np.array(xaxis_list)
var_data = np.array(data_list)
cum_var = np.round((1-var_data.cumsum()/var_data.sum())*100,6)

# 横軸用にnumpyの文字列の配列としておく
xlabel = var_xaxis.astype('str')

# ファイル名のパターンを指定して、最新のファイルを取得する
filename_pattern = 'AtCoder_record_*_.xlsx'
latest_file = max(glob.glob(filename_pattern), key=os.path.getctime, default=None)

# 既存のExcelファイルがある場合、Bookオブジェクトを作成する
book = pd.read_excel(latest_file, sheet_name=None, engine='openpyxl', index_col=0) if latest_file else {}

#ログ作成用のデータフレーム定義
df_hist = pd.DataFrame(index=[current_date],columns=var_xaxis)
df_hist.loc[current_date,var_xaxis] = var_data

# histシートにデータを追加する
if "hist" in book:
    df_book = book["hist"]
    if np.array_equal(df_book.iloc[0].values, df_hist.iloc[0].values):
        driver.quit()
        exit()  # プログラムの終了    
    
    df_hist = pd.concat([df_book, df_hist], axis=0)
    df_hist.sort_index(ascending=False,inplace=True)


# Excelファイルに保存するためのWriterオブジェクトを作成
new_filename = f"AtCoder_record_{current_date}_.xlsx"
writer = pd.ExcelWriter(new_filename, engine='openpyxl')

df_hist.to_excel(writer, sheet_name="hist", index=True)

df_cumsum = pd.DataFrame(index=[current_date],columns=var_xaxis)
df_cumsum.loc[current_date,var_xaxis] = cum_var

# cumsumシートにデータを追加する
if "cumsum" in book:
    df_book = book["cumsum"]
    df_cumsum = pd.concat([df_book, df_cumsum], axis=0)
df_cumsum.sort_index(ascending=False,inplace=True)    
df_cumsum.to_excel(writer, sheet_name="cumsum", index=True)


# 画像保存先ディレクトリのパスを指定
directory = f'{current_date}_directory'

# ディレクトリが存在しない場合に作成
if not os.path.exists(directory):
    os.makedirs(directory)

fig1 = make_plot(var_xaxis, var_data, color, "Participants",'Number of Participants',offset=50)
figname1 = f'{current_date}_participants.jpg'
fig1.savefig(os.path.join(directory,figname1),dpi=300)

fig2 = make_plot(var_xaxis, cum_var, color, "CumSumRank",'Percentage_Rank(%)',offset=1)
figname2 = f'{current_date}_CumSumRank.jpg'
fig2.savefig(os.path.join(directory,figname2),dpi=300)

Rr=30
fig3 = make_plot(var_xaxis[:Rr], cum_var[:Rr], color[:Rr], "CumSumRank Rating < 1200",'Percentage_Rank(%)',offset=1)
figname3 = f'{current_date}_CumSumRank_Rating_below_1200.jpg'
fig3.savefig(os.path.join(directory,figname3),dpi=300)

fig4 = make_plot(var_xaxis[Rr:], cum_var[Rr:], color[Rr:], "CumSumRank Rating >= 1200",'Percentage_Rank(%)',offset=0.1)
figname4 = f'{current_date}_CumSumRankRating_above_1200.jpg'
fig4.savefig(os.path.join(directory,figname4),dpi=300)

ws = writer.book.create_sheet("Graph")

fig_width = 600
fig_height = 600

img1 = openpyxl.drawing.image.Image(f'{directory}/{figname1}')
img1.width = fig_width
img1.height = fig_height
ws.add_image(img1, 'A1')

img2 = openpyxl.drawing.image.Image(f'{directory}/{figname2}')
img2.width = fig_width
img2.height = fig_height
ws.add_image(img2, 'I1')

img3 = openpyxl.drawing.image.Image(f'{directory}/{figname3}')
img3.width = fig_width
img3.height = fig_height
ws.add_image(img3, 'A33')

img4 = openpyxl.drawing.image.Image(f'{directory}/{figname4}')
img4.width = fig_width
img4.height = fig_height
ws.add_image(img4, 'I33')
# ファイルを保存してWriterオブジェクトを閉じる
writer.close()


#以降はxlsxファイルを整形するための部分

# Excelファイルを開く
wb = openpyxl.load_workbook(new_filename)
# シート名のリストを取得
sheet_names = wb.sheetnames
# 新しいシート名のリストを作成 (例: A, C, B)
new_sheet_names = ['Graph','hist','cumsum']
# シートを並び替え

for i, name in enumerate(new_sheet_names):
    j = sheet_names.index(name)  # 新しい位置を取得
    wb.move_sheet(sheet_names[j], i+1)  # シートを移動

    # 全シートの列幅を20に指定
    ws = wb[name]
    for column in ws.columns:
        for cell in column:
            ws.column_dimensions[cell.column_letter].width = 20
    
    # 全シートの行幅を12に指定
    for row in ws.rows:
        ws.row_dimensions[row[0].row].height = 12

#全シートのフォントサイズを10に設定する        
    fontsize = Font(size=10)
    for row in ws.iter_rows():
        for cell in row:
            cell.font = fontsize

#全シートのA列(日付index)の幅を20に設定する
for ws in wb.worksheets:
    ws.column_dimensions['A'].width = 25

#cumsumシートの数値を少数第3位まで表示するようにする    
for ws in wb.worksheets:
    if ws.title == "cumsum":
        for row in ws.iter_rows(min_row=2):
            for cell in row:
                if isinstance(cell.value, (int, float)):
                    cell.number_format = "0.000"
        
# 変更を保存
wb.save(new_filename)

driver.quit()

可読性はあまり良くありませんが、
とりあえず動くコードを作りました。
chromedriver.exeの使い方や
seleniumライブラリの
webelementの扱いでつまづきやすいですが、
欲しいデータさえ取り出せば
あとはグラフとログ作成の
地道な作業です😅

定期実行方法

windowsタスクスケジューラを使います。
スリープ復帰して使うことが出来ますので
環境に優しいです。

詳しい方法はこちらの記事に書きました。
スリープ中でもPythonスクレイピングプログラムを定期実行する方法【タスクスケジューラ】【Windows10】

AtCoderのレーティング更新は
「日」「月」曜日と認識していますが、
いったん24時間間隔で稼働させようと思います。

懸念箇所

レーティングの区切りが変動する場合があります。
例えば、最上位のレーティングは
3800もしくは3900と変動するため、
これらの違いによって
ログに不具合が生じるかもしれません。

今後の予定

スクレイピングプログラムを作成し、
順位ヒストグラムのデータを定期的に
ログ取得できるようになりました。

ログがある程度たまったら、
推移グラフも可視化したいと思います!

(2023年7月31日更新)AtCoderのレーティング分布の時系列変化をグラフにしました(随時更新予定)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA