はじめに
「PythonのWebスクレイピングでAtCoderのレート分布を出してみた」の続きです。
今回は年代別ではなく所属大学別にレート分布を出しました。
基本的な部分は前のものと同じです。
所属について
AtCoder の所属はユーザーが好きなものを書けるため、所属大学が同じでも表記が異なることがあります。例えば、同じ東京大学所属でも、所属を「東京大学」にしている人と「The University of Tokyo」にしている人がいます。
このような表記揺れに対応するために、まずは各大学の表記例を以下のスプレッドシートにて集めました。
https://docs.google.com/spreadsheets/d/1I1jQHw1A7oZv-At2XjDwuAG2IKZS3Q-pbEWUjg76XLM/edit?usp=sharing
追加してくれた方々ありがとうございます。
実装
まずスプレッドシートにより集計された大学別所属名を csv ファイルとしてダウンロードして、そのファイルを読み込みます。空白は検索時に "+" に置き換える必要があるので、 replace() で置換しておきます。
# 所属名一覧
Affiliation_dict = defaultdict(list) # {大学名: [所属名のリスト], ...}
with open("AtCoder大学名表記一覧 - シート1.csv") as f:
reader = list(csv.reader(f))
for daigaku in reader[5:]:
for syozoku in daigaku[2:]:
if syozoku:
Affiliation_dict[daigaku[1]].append(syozoku.replace(" ", "+"))
# {'東京大学': ['The+University+of+Tokyo', '東京大学', 'UT', 'UTokyo'], ...}
今回は所属別のレート分布のため検索対象に日本語の文字列が入ります。そのため、日本語での検索に対応させる必要があります。「PythonでURLエンコード・デコード(urllib.parse.quote, unquote)」を参考にすると、
if is_japanese(aff): # 所属が日本語文字列か調べる
Affiliation = parse.quote(Affiliation) # URLエンコード
このように urllib.parse.quote() を使ってあげると日本語の文字列をURLエンコードしてあげることができます。
以下本コードです。
from urllib import request, parse
from bs4 import BeautifulSoup
from collections import defaultdict
import csv
import re
# 日本語かどうか
def is_japanese(string):
p = re.compile("[ぁ-んァ-ン一-龥]+")
return p.search(string) is not None
# 所属名一覧
Affiliation_dict = defaultdict(list) # {大学名: [所属名のリスト], ...}
with open("AtCoder大学名表記一覧 - シート1.csv") as f:
reader = list(csv.reader(f))
for daigaku in reader[6:]:
for syozoku in daigaku[2:]:
if syozoku:
Affiliation_dict[daigaku[1]].append(syozoku.replace(" ", "+"))
print(Affiliation_dict)
num = {} # {大学名: [灰色の人数, 茶色の人数, ...]} の形式で人数を入れる
for univ in Affiliation_dict:
print("-{}-".format(univ))
univ_num = [0] * 10 # 大学ごとの[灰色の人数, 茶色の人数, ...]
for aff in Affiliation_dict[univ]:
print("--{}--".format(aff))
if is_japanese(aff): # 所属が日本語文字列か調べる
Affiliation = parse.quote(aff) # URLエンコード
else:
Affiliation = aff
for rating in range(0, 4000, 400):
print(rating)
RatingLowerBound = rating
if rating == 3600: # 金以上
RatingUpperBound = 9999
else:
RatingUpperBound = rating + 399
# フィルタを URL にセットする
url_filter = "?f.Affiliation=" + Affiliation + \
"&f.RatingLowerBound=" + str(RatingLowerBound) + \
"&f.RatingUpperBound=" + str(RatingUpperBound) + \
"&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 = []
# 順位の最大値を調べるために、順位表の最後のページを見る
html = request.urlopen(url + url_filter + str(page))
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 rank:
# フィルタ内順位の最大値がその人数
univ_num[rating // 400] += max(rank) + rank.count(max(rank)) - 1
num[univ] = univ_num
# 人数が多い順にソートする
num_sort = sorted(num.items(), key=lambda x: sum(x[1]), reverse=True)
dict_sort = {}
for i in num_sort:
dict_sort[i[0]] = i[1]
print(dict_sort)
# CSV として書き出し
# 横軸が大学名、縦軸が色(上から灰色, 茶色, ...の順)
with open("rating_univ.csv", "w", encoding="shift_jis") as f:
writer = csv.DictWriter(f, dict_sort.keys())
writer.writeheader()
for i in range(10):
row = {}
for k, v in dict_sort.items():
row[k] = v[i]
writer.writerow(row)
結果
2019年8月10日8時00分時点での所属集計結果を元にして大学別レート分布を作成しました。数が多いため、10人以上いる大学のみになります。