67
36

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 3 years have passed since last update.

PythonのWebスクレイピングでAtCoderのレート分布を出してみた

Last updated at Posted at 2019-06-07

追記

大学別のレート分布も出しました。
-> AtCoderの大学別レート分布を出してみた

きっかけ

レート分布に興味があったので、はじめは手入力でグラフを作っていました。

しかしこれだと1回作るのに時間がかかる上、手も疲れるし入力ミスもありました。そもそもせっかくPythonが書けるのに自動化しないのはアホらしいということで、Webスクレイピングの勉強も兼ね自動化しました。

やったこと

言語: Python 3.6
ライブラリ: urllib, BeautifulSoup, csv
を用いて、 https://atcoder.jp/ranking から国籍や年代、レートなどを指定して当てはまる人数を取得できるようにしました。

#PythonでのWebスクレイピングについて
Python Webスクレイピング 実践入門
PythonとBeautiful Soupでスクレイピング
の2つがわかりやすくて参考になります。

実装

以下のコードは条件内での誕生年ごとのレート分布を出すものです。

from urllib import request
from bs4 import BeautifulSoup
import csv

# 検索フィルタ
Affiliation = ""  # 所属
BirthYearLowerBound = 0  # 誕生年下限
BirthYearUpperBound = 9999  # 誕生年上限
CompetitionsLowerBound = 0  # コンテスト参加回数下限
CompetitionsUpperBound = 9999  # コンテスト参加回数上限
Country = "JP"  # 国籍
HighestRatingLowerBound = 0  # Rating最高値下限
HighestRatingUpperBound = 9999  # Rating最高値上限
RatingLowerBound = 0  # Rating下限
RatingUpperBound = 9999  # Rating上限
UserScreenName = ""  # ユーザ
WinsLowerBound = 0  # 優勝数下限
WinsUpperBound = 9999  # 優勝数上限


num = {}  # {誕生年: [灰色の人数, 茶色の人数, ...]} の形式で人数を入れる

# 誕生年 from_ から to_ まで
from_ = 1980
to_ = 2010

for birth in range(from_, to_ + 1):
    print("-{}-".format(birth))

    BirthYearLowerBound = birth
    BirthYearUpperBound = birth

    for rating in range(0, 3200, 400):
        print(rating)

        RatingLowerBound = rating
        if rating == 2800:  # 赤以上
            RatingUpperBound = 9999
        else:
            RatingUpperBound = rating + 399

        # フィルタを URL にセットする
        url_filter = "?f.Affiliation=" + Affiliation +\
                     "&f.BirthYearLowerBound=" + str(BirthYearLowerBound) +\
                     "&f.BirthYearUpperBound=" + str(BirthYearUpperBound) +\
                     "&f.CompetitionsLowerBound=" + str(CompetitionsLowerBound) +\
                     "&f.CompetitionsUpperBound=" + str(CompetitionsUpperBound) +\
                     "&f.Country=" + Country +\
                     "&f.HighestRatingLowerBound=" + str(HighestRatingLowerBound) +\
                     "&f.HighestRatingUpperBound=" + str(HighestRatingUpperBound) +\
                     "&f.RatingLowerBound=" + str(RatingLowerBound) +\
                     "&f.RatingUpperBound=" + str(RatingUpperBound) +\
                     "&f.UserScreenName=" + UserScreenName +\
                     "&f.WinsLowerBound=" + str(WinsLowerBound) +\
                     "&f.WinsUpperBound=" + str(WinsUpperBound) +\
                     "&page="

        url = "https://atcoder.jp/ranking"
        html = request.urlopen(url + url_filter + "0")
        soup = BeautifulSoup(html, "html.parser")

        ul = soup.find_all("ul")

        a = []

        # 指定したフィルタでのページ数を調べる
        page = 0
        for tag in ul:
            try:
                string_ = tag.get("class")

                if "pagination" in string_:
                    a = tag.find_all("a")
                    break

            except:
                pass

        for tag in a:
            try:
                string_ = tag.get("href")

                if "ranking" in string_:
                    page = max(page, int(tag.string))

            except:
                pass

        # フィルタ内順位を入れていく
        rank = []

        # 順位の最大値を調べるために、後ろから3ページほどを見る
        for i in range(max(1, page - 3), page + 1):
            html = request.urlopen(url + url_filter + str(i))
            soup = BeautifulSoup(html, "html.parser")

            td = soup.find_all("span")

            for tag in td:
                try:
                    string_ = tag.get("class").pop(0)

                    if string_ == "small":
                        rank.append(int(tag.string[1:-1]))

                except:
                    pass

        if birth not in num:
            num[birth] = []
        if rank:
            # フィルタ内順位の最大値がその人数
            num[birth].append(max(rank) + rank.count(max(rank)) - 1)
        else:
            num[birth].append(0)

print(num)

# CSV として書き出し
# 横軸が誕生年、縦軸が色(上から灰色, 茶色, ...の順)
with open("rating_{}{}_{}.csv".format(from_, to_, Country), "w") as f:
    writer = csv.DictWriter(f, num.keys())
    writer.writeheader()
    for i in range(len(num[from_])):
        row = {}
        for k, v in num.items():
            row[k] = v[i]
        writer.writerow(row)

結果

スクリーンショット 2019-06-08 2.19.49.png こんな感じの CSV ファイルが出力されます。1行目は誕生年で、2行目から9行目は上から順に灰色、茶色、緑、水色、青、黄色、橙、赤のレートの人数です。あとはこれを Excel でインポートしてあげれば、以下のようなレート分布のグラフが簡単に作れます。(それぞれ、100% 積み上げ縦棒と積み上げ縦棒) スクリーンショット 2019-06-08 3.06.44.png スクリーンショット 2019-06-08 2.27.01.png

条件を変えてみる

例えば、9行目の「コンテスト参加回数下限」を以下のように変えてあげると、

CompetitionsLowerBound = 10  # コンテスト参加回数下限
スクリーンショット 2019-06-09 10.32.41.png スクリーンショット 2019-06-09 10.32.54.png

このように参加回数が10回以上の人に絞ってグラフを作れます。2006年生まれ以降がほとんどいないので、こういう時は25行目を

to_ = 2005

としてあげれば、2005年生まれまでに範囲を絞れます。

注意

このレート分布は誕生年を用いているため、そもそも AtCoder のアカウントに誕生年を記載していないアカウントは含まれていません。誕生年が書かれているアカウントは全体の6割ほどなので、グラフの人数も実際の約6割になっています。

おまけ

簡単に作れるようになったので、中国とロシアの分布も作ってみました。

中国

1985年〜2010年生まれです。
スクリーンショット 2019-06-08 2.50.39.png
スクリーンショット 2019-06-08 2.50.52.png
若い層が多いですね。

ロシア

1980年〜2005年生まれです。
スクリーンショット 2019-06-08 2.44.02.png
スクリーンショット 2019-06-08 2.44.55.png
おそロシア……

67
36
2

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
67
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?