機械学習を用いた音ゲーのクリア難度推定 (その1)
音ゲーのクリア難度推定なんかデータサイエンスの入門にいいんじゃないかな?とふと思い立ってやることにしました。3記事くらいに分けて公開します。初回はデータ収集のためのWebスクレイピングについて。
やること
データの準備
LR2IRにはプレー人数の非常に少ない譜面や、アクティブでないアカウントが多く存在します。そこで、まず集計対象プレイヤーについて「BMS発狂難易度表の収録曲のうち125曲以上スコア登録済み」という基準を設けます。これはStairwayの基準を引用したものです。
そして、「集計対象プレイヤー(現在12000人くらい)のうち、100人以上がスコアをつけている」曲を集計対象曲とします。
import re from urllib.request import urlopen from bs4 import BeautifulSoup #stairwayの集計対象プレイヤーのid一覧を取得 player_ids=[] bs = BeautifulSoup(urlopen("http://stairway.sakura.ne.jp/bms/LunaticRave2/?contents=player&p=1")) for player_link in bs.find_all("a", {"href" : re.compile("/?contents=player&page=\d")}): player_id = int(player_link["href"].split("=")[-1]) player_ids.append(player_id) #ファイルに保存 with open("playerid","w") as f: f.write(str(player_ids[0])) for player_id in player_ids[1:]: f.write(",") f.write(str(player_id))
全ユーザーに対してプレー履歴の全取得をするとちょっとサーバーに負荷をかけてしまいそうなので、ユーザーリストはStairwayから拝借しました。
aタグのうち、プレーヤーのmypageへのリンクの形になっているものを選び出し、splitすることでplayer idを得ています。
これは単一ページのパースなのですぐできると思います。
次に、譜面ごとにプレー人数を集計します。譜面ID(bmsid)をkey、プレー人数をvalにした辞書で数えていきます。keyがないとき勝手に0を入れてくれるdefaultdict(int)が便利。負荷をかけないようsleepを入れながら、丸一日くらいで取得しました。
このうち、100人以上がプレーした譜面を残します。意外なことに、10000譜面ちょっとありました。
import re from collections import defaultdict from urllib.request import urlopen from bs4 import BeautifulSoup #各playerのプレーしたことあるbmsidを全取得、bmsidごとにプレーした人数を数える num_player = defaultdict(int) #key:val=(bmsid, プレー人数) for player_id in player_ids: for i in range(1, 10000): bs=BeautifulSoup(urlopen(f"http://www.dream-pro.info/~lavalse/LR2IR/search.cgi?mode=mylist&playerid={player_id}&sort=recent&filter=7&page={i}")) findresult=bs.find_all("a", {"href" : re.compile("search.cgi\?mode=ranking&bmsid=\d")}) if not findresult: break #空のページに至ったらbreak for bms_link in findresult: bms_id = int(bms_link["href"].split("=")[-1]) num_player[bms_id]+=1 time.sleep(0.01) print(f"player{player_id} done") #プレー人数がmin_playcountより多い譜面だけ残してbms_idsにidを集計 bms_ids=[] for k in num_player.keys(): if num_player[k]<min_playcount: continue: else bms_ids.append(k) #ファイルに保存 with open("bmsid","w") as f: f.write(str(bms_ids[0])) for bms_id in bms_ids[1:]: f.write(",") f.write(str(bms_id))
最後に、この集計対象譜面について全プレーヤーのスコアを取得し、集計対象ユーザーのものだけ残してファイルに保存します。1譜面1ファイルで書き出しています。
丸二日くらいかかりました。ファイルサイズは合計で770 MBでした。
import re import time import os from collections import defaultdict from urllib.request import urlopen from bs4 import BeautifulSoup with open("playerid","r") as f: player_ids=set([int(i) for i in f.read().split(",")]) with open("bmsid","r") as f: bms_ids=set([int(i) for i in f.read().split(",")]) for bms_id in bms_ids: #集計対象譜面についてループ if os.path.exists(str(bms_id)): print(f"bmsid {bms_id} ... skipped") continue with open(str(bms_id), "w") as f: print(f"bmsid {bms_id} ... ",end="") for i in range(1, 10000): bs=BeautifulSoup(urlopen(f"http://www.dream-pro.info/~lavalse/LR2IR/search.cgi?mode=ranking&page={i}&bmsid={bms_id}")) find_result=bs.find_all("a", {"href" : re.compile("search.cgi\?mode=mypage&playerid=\d")}) if not find_result: break #空のページに至ったらbreak for player_link in find_result: player_id = int(player_link["href"].split("=")[-1]) if player_id not in player_ids: continue playdata = player_link.parent.parent.contents[3:] f.write(str(player_id)) for item in playdata: f.write(",") f.write(item.get_text()) f.write("\n") time.sleep(0.01) print("done")
例えばBMS界のAAこと星の器(bmsid=15)に対する出力ファイルの冒頭はこんな感じ。各行で一番左の列はplayer id、残りがプレーデータです。
37188,FULLCOMBO,AAA,4012/4012(100%),2006/2006,0,2006,0,0,0,2,難,乱,KB,LR2 83367,FULLCOMBO,AAA,4011/4012(99.97%),2006/2006,0,2005,1,0,0,0,難,乱,BM,LR2 202,★FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,難,乱,KB,LR2 105711,FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,易,乱,KB,LR2 120831,FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,難,乱,KB,LR2 160637,FULLCOMBO,AAA,4009/4012(99.92%),2006/2006,0,2003,3,0,0,2,難,乱,BM,LR2 15499,FULLCOMBO,AAA,4008/4012(99.9%),2006/2006,2,2002,4,0,0,4,難,乱,BM,LR2 72359,FULLCOMBO,AAA,4008/4012(99.9%),2006/2006,0,2002,4,0,0,0,易,乱,KB,LR2 94774,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2 3906,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2 36488,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,KB,LR2 2725,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2002,3,0,0,1,難,乱,BM,LR2 4314,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2002,3,0,0,1,難,乱,BM,LR2 95222,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2 139053,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,1,難,乱,BM,LR2 22923,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,0,難,乱,KB,LR2 4812,★FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,13,難,乱,BM,LR2 51433,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,8,普,乱,BM,LR2 25546,FULLCOMBO,AAA,4005/4012(99.82%),2006/2006,0,2000,5,0,0,4,普,乱,BM,LR2 ...
次回はMarix Factorizationという方法を用いて、これらのデータから難度推定を行っていきます。
→すでに同等の成果が公表されていたのでこの件は保留とします。難度の他に推定したい量が思いついたら続けます。