世界カーリング選手権のデータをCSV形式で取得する
統計とかの勉強用に何か面白いデータがないか探していたところ、以下のサイトにオリンピックや世界選手権のカーリングのデータを発見!
ただ、CSV形式のデータなどは用意されておらず、REST API(http://resultsapi.worldcurling.org/)に何度アクセスしても、データを取得できず。。。(誰か取得方法を知っていたら教えてください。。。)
なので、Pythonを使ってHTMLをパースして、データを抽出することにしました。
1. ライブラリの読み込み
世界カーリング選手権のページのHTMLの取得にはrequestsパッケージ、取得したHTMLのパースにはlxmlパッケージを利用しました。
import requests import csv from lxml import html
2. HTMLソースの取得
HTMLソースの取得は簡単で、requestパッケージのgetメソッドを使ってGETリクエストを送信すれば良いです。
とりあえず世界カーリング選手権2016(男子)のデータを取得します。
# 取得するページのURL base_url = "http://results.worldcurling.org" URL = base_url + "/Championship/Details/555" # 取得 result = requests.get(URL)
3. HTMLソースからデータを取得
3-1. トーナメント名、Resultsタブの取得
ここからHTMLをパースしてデータを抽出していくのですが、基本的には以下の2つの操作を繰り返し実行することで、HTMLをパースしました。
- 所望のHTML要素が出現するまで子要素を順にたどっていく(lxmlパッケージ、getchildren()関数)
- XPathを用いて所望のHTML要素を取得する(lxmlパッケージ、xpath()関数)
まずはroot要素からXPathを使って、トーナメント名があるHTML要素(h2)、試合結果が格納されているHTML要素(div)を取得します。WCFのサイトでは、以下のように6個のタブで構成されており、試合結果はResultsタブにあります。
[出典:http://results.worldcurling.org/Championship/Details/555]
以下ではXPathを用いてResultsタブのHTML要素(div)を取得し、後述するget_results_data()でデータを抽出しています。
# HTMLのrootを取得 root = html.fromstring(result.content) # トーナメント名 h2_element = root.xpath('//h2')[0] tournament_name = h2_element.text.strip() # Resultsタブを取得 results_tab_list = root.xpath('//div[contains(@class, "tab-pane fade") and @id="inforesults"]') if len(results_tab_list) > 1: print "deata acquisition error : Result tab" results_tab = results_tab_list[0] game_info_list, player_stats_list = get_results_data(results_tab, base_url)
3-2. 各ゲームのスコアの取得
Resultsタブの中では、リンク「Show all games」をクリックすると、すべてのゲームの結果が表示されるようになっています。そのため、以下では、まずリンク先のURLを取得し、GETリクエストを送信して、ゲーム結果のHTMLを取得します。
ゲーム結果には、①スコア情報と、②各選手のショット率のデータが含まれています。
① スコア情報
スコア情報では、「ドロー」「シート」「チーム名」「ハンマーの有無」「各エンドの得点」「合計得点」の6種類のデータを取得します。各データの取得にはXPathを利用し、それぞれが格納されているHTML要素のclass属性を指定することで、該当するHTML要素が取得できます。
② 各選手のショット率
各選手のショット率は、「チーム名」「選手名」「ショット率」のデータを取得します。XPathとgetchildren()関数を用いて、子要素を辿っていくことで、各データを取得します。
取得したデータは、いずれもListに格納して、呼び出し元に返します。
def get_results_data(results_tab, base_url): ''' Resultsタブのデータを取得 Keyword arguments: results_tab --- html data of Results tab base_url --- url of WCF Results & Statistics Page ''' # <a>を取得 a_element_list = results_tab.xpath(".//a") # Show all games a_element = a_element_list[-1] url = base_url + a_element.get("href") # データのHTMLを取得 all_data_html = requests.get(url) # HTMLのrootを取得 root = html.fromstring(all_data_html.content) root_children = root.getchildren() game_info_list = [] player_stats_list = [] for idx in range(1, len(root_children)): child = root_children[idx] #--- ゲームのスコア情報を取得 --- table_list = child.xpath('.//table[@class="table game-table"]') if len(table_list) > 1: print "deata acquisition error : game table data" table = table_list[0] table_children = table.getchildren() thread = table_children[0] tbody = table_children[1] # Draw X を取得 grandchild = thread.getchildren()[0].getchildren()[0] draw = grandchild.text.strip() # シートを取得 tr_element_list = tbody.getchildren() td_element_list = tr_element_list[0].xpath('.//td[@class="game-sheet"]') if len(td_element_list) > 1: print "data acquisition error : sheet name" td_element = td_element_list[0] sheet_name = td_element.text.strip() for tr_element in tr_element_list: # チーム名 td_element_list = tr_element.xpath('.//td[@class="game-team"]') if len(td_element_list) > 1: print "data acquisition error : team" team = td_element_list[0].text.strip() # ハンマー td_element_list = tr_element.xpath('.//td[@class="game-hammer"]') if len(td_element_list) > 1: print "data acquisition error : hammer" hammer_symbol = td_element_list[0].text.strip() hammer = "0" if hammer_symbol == '*': hammer = "1" # スコア td_element_list = tr_element.xpath('.//td[contains(@class, "game-end10")]') score_list = [] end_num = len(td_element_list) if end_num != 12: print "data acquisition error : score" score_list = [] for td_element in td_element_list: score_list.append(td_element.text.strip()) # スコア(トータル) td_element_list = tr_element.xpath('.//td[contains(@class, "game-total")]') if len(td_element_list) > 1: print "data acquisition error : score(total)" score_total = td_element_list[0].text.strip() # Drow,シート,チーム名,ハンマー,スコア,スコアトータル game_info = draw + "," + sheet_name + "," + team + "," + hammer + "," + ",".join(score_list) + "," + score_total game_info = [] game_info.append(draw) game_info.append(sheet_name) game_info.append(team) game_info.append(hammer) game_info.extend(score_list) game_info.append(score_total) game_info_list.append(game_info) #--- ゲームの各選手のショット率を取得 --- div_col_md_12_list = child.xpath('./div[@class="col-md-12"]') if len(div_col_md_12_list) > 1: print "deata acquisition error : player stats (col-md-12)" div_col_md_12 = div_col_md_12_list[0] div_col_md_6_list = div_col_md_12.xpath('./div[@class="col-md-6"]') if len(div_col_md_6_list) != 2: print "deata acquisition error : player stats (col-md-6)" # 先行・後攻チームの選手のデータを取得 for div_col_md_6 in div_col_md_6_list: div_list = div_col_md_6.getchildren() # チーム名 div_team_name = div_list[0] h5_element = div_team_name.getchildren()[0] team_name = h5_element.text.strip() player_dict = {} shot_rate_dict = {} for idx in range(1, 6): if 1+(idx-1)*3 > len(div_list) - 1: break # ポジション div_position = div_list[1+(idx-1)*3] position = div_position.text.strip() if "(" in position: parenthesis_idx = position.find("(") position = position[0:parenthesis_idx].strip() # 選手 div_player = div_list[2+(idx-1)*3] a_element = div_player.getchildren()[0] player_name = a_element.text.strip() player_dict[position] = player_name # ショット率 div_shot_rate = div_list[3+(idx-1)*3] shot_rate = div_shot_rate.text.strip() shot_rate_dict[position] = shot_rate # Draw,シート,チーム名, # Skip選手名,Third選手名,Second選手名,Lead選手名,Alternate選手名, # Skipショット率,Thirdショット率,Secondショット率,Leadショット率,Alternateショット率 player_stats = [] player_stats.append(draw) player_stats.append(sheet_name) player_stats.append(team_name) fourth_name = player_dict.get("Skip") if fourth_name is None: fourth_name = player_dict.get("Fourth") player_stats.append(fourth_name) player_stats.append(player_dict.get("Third")) player_stats.append(player_dict.get("Second")) player_stats.append(player_dict.get("Lead")) player_stats.append(player_dict.get("Alternate")) fourth_shot_rate = shot_rate_dict.get("Skip") if fourth_shot_rate is None: fourth_shot_rate = shot_rate_dict.get("Fourth") player_stats.append(fourth_shot_rate) player_stats.append(shot_rate_dict.get("Third")) player_stats.append(shot_rate_dict.get("Second")) player_stats.append(shot_rate_dict.get("Lead")) player_stats.append(shot_rate_dict.get("Alternate")) player_stats_list.append(player_stats) return game_info_list, player_stats_list
4. CSVファイルの出力
取得した各データは、それぞれ以下のファイル名でCSVファイルを出力します。
① スコア情報:<大会名>_results_score.csv
② 各選手のショット率:<大会名>_results_player.csv
選手名にマルチバイト文字が使われているため、出力の際にはutf8に変換しています。
output_dir = u"/Users/takayuki/Documents/work/Study/data" #--- Rankingタブ --- output_file_path = output_dir + "/" + tournament_name + "_ranking.csv" output_file = open(output_file_path, "w") csv_writer = csv.writer(output_file) header = u"team,ranking,win,lose,member1,member2,member3,member4,member5" column_name_list = header.split(',') ranking_data.insert(0, column_name_list) for row in ranking_data: tmp_row = map(lambda x: x if x is not None else "-", row) output_row_utf8 = map(lambda x: x.encode('utf8'), tmp_row) csv_writer.writerow(output_row_utf8) output_file.close() #--- Resultsタブ --- # スコア output_file_path = output_dir + "/" + tournament_name + "_results_score.csv" output_file = open(output_file_path, "w") csv_writer = csv.writer(output_file) header = u"draw,sheet,team,hammer,end1,end2,end3,end4,end5,end6,end7,end8,end9,end10,extra1,extra2,score(total)" column_name_list = header.split(',') game_info_list.insert(0, column_name_list) for row in game_info_list: tmp_row = map(lambda x: x if x is not None else "-", row) output_row_utf8 = map(lambda x: x.encode('utf8'), tmp_row) csv_writer.writerow(output_row_utf8) output_file.close() # 選手のデータ output_file_path = output_dir + "/" + tournament_name + "_results_player.csv" output_file = open(output_file_path, "w") csv_writer = csv.writer(output_file) header = u"draw,sheet,team,Fourth name,Third name,Second name,Lead name,Alternate name,Fourth shot rate,Third shot rate,Second shot rate,Lead shot rate,Alternate shot rate" column_name_list = header.split(',') player_stats_list.insert(0, column_name_list) for row in player_stats_list: tmp_row = map(lambda x: x if x is not None else "-", row) output_row_utf8 = map(lambda x: x.encode('utf8'), tmp_row) csv_writer.writerow(output_row_utf8) output_file.close()
5. さいごに
一応、最初のURLを他の大会に変更すれば、他の大会のデータも取得できます。(たぶん。。。動かなかったら誰か教えてください。。。)
今後は取得したデータを使って、とりあえず可視化からやっていきたいと思います。