メガネをかけたキリンのブログ

アウトドアや旅行の記事を中心に投稿します。

【Python】競馬のデータを取得するスクリプトをアレンジしてみた

f:id:null10blgcom:20200509092610p:plain

今回はPythonで競馬の過去データをスクレイピングするスクリプトを書いてみました。

参考とさせていただいたのはこちらのサイトです。 qiita.com

基本的にはこちらをリスペクトさせていただく形で取り組みましたが、追加として以下を取得できるようにしました。

  • 枠番
  • 着差
  • 3Fタイム

あとで機械学習などのデータ解析をしてみたいなぁということでなるべく数値データとして扱える型に変換しています。

そのため、着差については

  • ハナ → 0.0625
  • アタマ → 0.125
  • クビ → 0.25

など、1馬身を基準に1/2ずつ値を区切る形で表現しています。

この辺りは実際もっと厳密にできる気もしますが、いまはこれでヨシとしています。

作者さんのサイトでも言及されていますが、スクリプトの実行は結構時間がかかります。

取得年数に比例する形で実行時間が大きくなるため、期間を小さく区切りながら実行したほうがいいかもしれません。

あと、requestはやりすぎるとサーバ負荷をかけてしまうため、1回ごとに1秒間をあけるようにしています。

以下は今回のPythonのスクリプトです。


# %% ライブラリの読み込み
import requests
import re
from tqdm import tqdm
import time
from bs4 import BeautifulSoup
import pandas as pd


def numStr(num):
    if num >= 10:
        return str(num)
    else:
        return '0' + str(num)


Base = "http://race.sp.netkeiba.com/?pid=race_result&race_id="
dst = ''
df_col = ['year', 'date', 'field', 'race', 'race_name', 'course', 'head_count',
          'rank', 'horse_name', 'gender', 'age', 'trainerA', 'trainerB',
          'weight', 'c_weight', 'jockey', 'j_weight', 'odds', 'popu',
          'time', 'gap', 'time_3f', 'condition', 'bracket_number']
df = pd.DataFrame()

# データ取得開始年と終了年を設定
start_year = 2018
end_year = 2020

for year in tqdm(range(start_year, end_year+1)):
    for i in tqdm(range(1, 11)):
        for j in tqdm(range(1, 11)):
            for k in tqdm(range(1, 11)):
                for l in range(1, 13):
                    # urlでぶっこ抜く
                    url = Base + str(year) + numStr(i) + \
                        numStr(j) + numStr(k) + numStr(l)
                    time.sleep(1)
                    try:
                        html = requests.get(url)
                    except ZeroDivisionError:
                        pass
                    else:

                        html.encoding = 'EUC-JP'

                        # scraping
                        soup = BeautifulSoup(html.text, 'html.parser')
                        # ページがあるかの判定
                        if soup.find_all('div', attrs={'class', 'Result_Guide'}) != []:
                            break
                        else:
                            # 共通部分を抜き出す
                            CommonYear = year
                            CommonDate = soup.find_all('div', attrs={'class', 'Change_Btn Day'})[
                                0].string.strip()
                            CommonField = soup.find_all('div', attrs={'class', 'Change_Btn Course'})[
                                0].string.strip()
                            CommonRace = soup.find_all('div', attrs={'Race_Num'})[
                                0].span.string.strip('R')
                            CommonRname = soup.find_all('dt', attrs={'class', 'Race_Name'})[
                                0].contents[0].strip()
                            CommonCourse = soup.find_all('dd', attrs={'Race_Data'})[
                                0].span.string.strip('m')
                            CommonCondition = soup.find_all('span', attrs={'class', 'Item03'})[
                                0].string
                            # たまに頭数取得ができないケースがあるのでこちらで取得
                            CommonHcount = len(
                                soup.find_all('div', attrs='Rank'))

                            for m in range(len(soup.find_all('div', attrs='Rank'))):
                                dst = pd.Series(index=df_col)
                                try:
                                    dst['year'] = CommonYear
                                    dst['date'] = CommonDate
                                    dst['field'] = CommonField  # 開催場所
                                    dst['race'] = CommonRace
                                    dst['race_name'] = CommonRname
                                    dst['course'] = CommonCourse
                                    dst['head_count'] = CommonHcount  # 頭数
                                    dst['condition'] = CommonCondition
                                    dst['rank'] = soup.find_all('div', attrs='Rank')[
                                        m].contents[0]
                                    dst['horse_name'] = soup.find_all(
                                        'dt', attrs=['class', 'Horse_Name'])[m].a.string
                                    detailL = soup.find_all(
                                        'span', attrs=['class', 'Detail_Left'])[m]
                                    dst['gender'] = list(
                                        detailL.contents[0].split()[0])[0]
                                    dst['age'] = list(
                                        detailL.contents[0].split()[0])[1]
                                    dst['trainerA'] = detailL.span.string.split('・')[
                                        0]
                                    dst['trainerB'] = detailL.span.string.split('・')[
                                        1]
                                    if len(detailL.contents[0].split()) >= 2:
                                        dst['weight'] = detailL.contents[0].split()[
                                            1].split('(')[0]
                                        if len(detailL.contents[0].split()[1].split('(')) >= 2:
                                            dst['c_weight'] = detailL.contents[0].split()[1].split(
                                                '(')[1].strip(')').strip('+')   # 多分馬の体重変動
                                    detailR = soup.find_all('span', attrs=['class', 'Detail_Right'])[
                                        m].contents
                                    if "\n" in detailR or "\n▲" in detailR or '\n☆' in detailR:
                                        detailR.pop(0)
                                    dst['jackie'] = detailR[0].string.strip()
                                    if detailR[2].string is not None:
                                        dst['j_weight'] = detailR[2].strip().replace(
                                            '(', '').replace(')', '').strip('.0')   # 多分jockeyの体重変動
                                    else:
                                        dst['j_weight'] = detailR[2]
                                    Odds = soup.find_all('td', attrs=['class', 'Odds'])[
                                        m].contents[1]
                                    if Odds.dt.string is not None:
                                        dst['odds'] = Odds.dt.string.strip('倍')
                                        dst['popu'] = Odds.dd.string.strip(
                                            '人気')  # 何番人気か
                                    Time = soup.find_all('td', attrs=['class', 'Time'])[
                                        m].contents[1]
                                    if Time.dt.string is not None:
                                        race_time = Time.find_all('dd')
                                        # 走破タイム
                                        dst['time'] = Time.dt.string
                                        dst['gap'] = race_time[0].string  # 着差
                                        dst['time_3f'] = race_time[1].string.strip(
                                            '()')  # 3Fタイム
                                    Number = soup.find_all(
                                        'td', attrs=['class', re.compile('Num Waku')])[m*2]  # 枠番は偶数番目の要素に格納される
                                    Number2 = soup.find_all(
                                        'td', attrs=['class', re.compile('Num Waku')])[m*2+1]  # 馬番は奇数番目の要素に格納される
                                    dst['bracket_number'] = Number.div.string
                                    dst['horse_number'] = Number2.div.string
                                except ZeroDivisionError:
                                    pass
                                dst.name = str(
                                    year) + numStr(i) + numStr(j) + numStr(k) + numStr(l) + numStr(m)

                                df = df.append(dst)

# 出頭数を整数化
df['head_count'] = df['head_count'].astype('i')

# courseをcourse_typeとdistanceに分割
df['course'] = df['course'].replace('(.*)ダ(.*)', r'\1ダ_\2', regex=True)  # _を追加
df['course'] = df['course'].replace('(.*)芝(.*)', r'\1芝_\2', regex=True)  # _を追加
df['course'] = df['course'].replace('(.*)障(.*)', r'\1障_\2', regex=True)  # _を追加
df = pd.concat([df, df['course'].str.split('_', expand=True)],
               axis=1).drop('course', axis=1)
df.rename(columns={0: 'course_type', 1: 'distance'}, inplace=True)

# 走破タイムを秒に変換
base_time = pd.to_datetime('00:00.0', format='%M:%S.%f')
df['time'] = pd.to_datetime(df['time'], format='%M:%S.%f') - base_time
df['time'] = df['time'].dt.total_seconds().astype('f')

# 着差のデータ整形
df['gap'] = df['gap'].fillna(0)  # 欠損値に0を代入
df['gap'] = df['gap'].replace('(.*)/(.*)', r'\1_\2', regex=True)
df['gap'] = df['gap'].replace('(.*).1_2(.*)', r'\1.5\2', regex=True)
df['gap'] = df['gap'].replace('(.*)1_2(.*)', '0.5', regex=True)
df['gap'] = df['gap'].replace('(.*).3_4(.*)', r'\1.75\2', regex=True)
df['gap'] = df['gap'].replace('(.*)3_4(.*)', '0.75', regex=True)
df['gap'] = df['gap'].replace('(.*).1_4(.*)', r'\1.25\2', regex=True)
df['gap'] = df['gap'].replace('(.*)1_4(.*)', '0.25', regex=True)
df['gap'] = df['gap'].replace('大', '11')
df['gap'] = df['gap'].replace('クビ', '0.25')
df['gap'] = df['gap'].replace('アタマ', '0.125')
df['gap'] = df['gap'].replace('ハナ', '0.0625')
df['gap'] = df['gap'].astype('f')  # 着差を数値化(float)

# 開催年の整数化
df['year'] = df['year'].astype('i')

# 除外が含まれる行を削除
# df = df[df['rank'] != '除外']  # 除外が含まれる行以外を選択してdfに代入している

# 出力ファイル名の設定
file_name = 'keiba_PS_' + start_year + '-' + end_year + '_.xlsx'

# データフレームをExcelに出力
df.to_excel(file_name, sheet_name='race_data')

次回以降、このデータをもとに機械学習を使ってデータ分析していきます。(やるとはいってない)

pre{ position: relative } #selectPre{ position: absolute; top: 0; right: 0; border: none; padding: 2px 3px; font-family: consolas }