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

# 追記
大学別のレート分布も出しました。
-> [AtCoderの大学別レート分布を出してみた](https://qiita.com/saba/items/23b49dfbd1eeef6212b0)

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

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">1980年〜2010年生まれのレート分布です（1枚目が各年割合、2枚目が各年人数） <a href="https://t.co/2F6xLnZhRI">pic.twitter.com/2F6xLnZhRI</a></p>&mdash; saba (@saba_kpr) <a href="https://twitter.com/saba_kpr/status/1136022020439781377?ref_src=twsrc%5Etfw">2019年6月4日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

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

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

#PythonでのWebスクレイピングについて
・[Python Webスクレイピング 実践入門](https://qiita.com/Azunyan1111/items/9b3d16428d2bcc7c9406)
・[PythonとBeautiful Soupでスクレイピング](https://qiita.com/itkr/items/513318a9b5b92bd56185)
の2つがわかりやすくて参考になります。

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

```python
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)
```

# 結果
<img width="873" alt="スクリーンショット 2019-06-08 2.19.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/5bdacfda-2c11-28a4-08d7-bb8a308f742c.png">
こんな感じの CSV ファイルが出力されます。1行目は誕生年で、2行目から9行目は上から順に灰色、茶色、緑、水色、青、黄色、橙、赤のレートの人数です。あとはこれを Excel でインポートしてあげれば、以下のようなレート分布のグラフが簡単に作れます。（それぞれ、100% 積み上げ縦棒と積み上げ縦棒）
<img width="1100" alt="スクリーンショット 2019-06-08 3.06.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/2d1dbca7-632b-1fe3-0467-37e8dfebb2e2.png">

<img width="1100" alt="スクリーンショット 2019-06-08 2.27.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/dc964a5e-2148-ee23-f39e-7dd1fa47c04e.png">

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

```python
CompetitionsLowerBound = 10  # コンテスト参加回数下限
```

<img width="981" alt="スクリーンショット 2019-06-09 10.32.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/2ef85f24-3ac4-5f85-6ca1-1e8d93677a8c.png">
<img width="980" alt="スクリーンショット 2019-06-09 10.32.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/0b7a315f-ddc1-41a4-4046-ecd93fb37aa9.png">

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

```python
to_ = 2005
```

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

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

# おまけ
簡単に作れるようになったので、中国とロシアの分布も作ってみました。
## 中国
1985年〜2010年生まれです。
<img width="1099" alt="スクリーンショット 2019-06-08 2.50.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/5e12c518-440e-5374-40cd-cfcb26302e7d.png">
<img width="1095" alt="スクリーンショット 2019-06-08 2.50.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/b4d19a2a-01ef-384b-9b1b-2fa7fb5ac6aa.png">
若い層が多いですね。


## ロシア
1980年〜2005年生まれです。
<img width="1099" alt="スクリーンショット 2019-06-08 2.44.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/7132c488-e8f2-651d-3998-60aad808e7a1.png">
<img width="1101" alt="スクリーンショット 2019-06-08 2.44.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/e1a318fa-69e7-df5b-ff94-a8fc73e5978f.png">
おそロシア……