TECH MEMO

技術的な内容に関する個人的なメモ

世界カーリング選手権のデータをCSV形式で取得する

統計とかの勉強用に何か面白いデータがないか探していたところ、以下のサイトにオリンピックや世界選手権のカーリングのデータを発見!

results.worldcurling.org

ただ、CSV形式のデータなどは用意されておらず、REST APIhttp://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をパースしました。

  1. 所望のHTML要素が出現するまで子要素を順にたどっていく(lxmlパッケージ、getchildren()関数)
  2. XPathを用いて所望のHTML要素を取得する(lxmlパッケージ、xpath()関数)

まずはroot要素からXPathを使って、トーナメント名があるHTML要素(h2)、試合結果が格納されているHTML要素(div)を取得します。WCFのサイトでは、以下のように6個のタブで構成されており、試合結果はResultsタブにあります。

f:id:curlyst:20160718233031p:plain
[出典: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を他の大会に変更すれば、他の大会のデータも取得できます。(たぶん。。。動かなかったら誰か教えてください。。。)

今後は取得したデータを使って、とりあえず可視化からやっていきたいと思います。