よっしーの私的空間

機械学習を中心に興味のあることについて更新します

競馬データのスクレイピング

netkeibaから競馬データをスクレイピングする方法についてまとめます。既に同様の記事は世にたくさん出回ってますが、少し改良して効率化してみました。

1.既出のスクレイピング方法の問題点

競馬データのスクレイピングで検索すると、以下のようなソースコードが多いと思います。

for i in range(1, 11):
    for j in range(1, 7):
        for k in range(1, 13):
            for l in range(1, 13):
                Base = "https://race.netkeiba.com/race/result.html?race_id="
                race_id = "2019" + str(i).zfill(2) + str(j).zfill(2) + str(k).zfill(2) + str(l).zfill(2)
                url = Base + race_id
                '''------------------'''
                '''スクレイピング処理'''
                '''------------------'''

上記はrace_idを総当たりスクレイピングする方法ですが、残念ながら実際にはrace_idは飛び飛びで、存在しないrace_idがたくさんあります。よって無駄な検索をしていることになります。スクレイピング中にsleep処理を入れると思いますが、無駄な検索をするたびにsleepが入って、全体の処理時間が長引きます。

2.どうやって効率化するか

netkeibaには1日毎の開催レース一覧をまとめている以下のようなページがあります。
レース一覧 | 2023年5月20日 レース情報(JRA) - netkeiba.com

このページからrace_idの一覧を取得することで、実在するrace_idのリストを作成することができます。ここまで来たら後はあとはrace_idをキーにスクレイピングするだけです。

2.1.開催レース一覧ページからrace_idを取得する方法

開催レース一覧のページは動的にrace_idを設定しているようなので、beautifulsoupは使えません。こういうときはseleniumが効果的です。 以下のような実装でrace_idの一覧を取得できます。

import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

#selenium driverでブラウザ起動
def get_selenium_driver(url):
    options = Options()
    # ヘッドレスモードで実行する場合
    options.add_argument("--headless")
    driver = webdriver.Chrome(options=options)

    # 取得先URLにアクセス
    driver.get(url)
    # コンテンツが描画されるまで待機
    time.sleep(5)
    
    return driver

#指定した開催日に開催されるレースのrace_id一覧を取得
def get_race_ids(kaisai_dates):
    race_ids = []
    for kaisai_date in kaisai_dates:
        url = 'https://race.netkeiba.com/top/race_list.html?kaisai_date=' + kaisai_date
        driver = get_selenium_driver(url)
        elements = driver.find_elements(By.CLASS_NAME,"MyRace_List_Item")
        for element in elements:
            race_id = str(element.get_attribute('id'))
            race_id = race_id.replace("myrace_","")
            race_ids.append(race_id)
    
    return race_ids

race_ids = get_race_ids(["20230528"])

上記で2023年5月28日に開催された全レースの全race_idをリストとして取得できます。

2.2.競馬のレース結果のスクレイピングする方法

これは他にサイトがたくさんあるので、今更紹介する必要はないと思いますが、一応まとめておきます。

from bs4 import BeautifulSoup
import requests
import pandas as pd

def fetch_horse_datas(race_id, horse_datas):
    Base="https://race.netkeiba.com/race/result.html?race_id="
    url = Base + race_id
    kaisai_year = int(race_id[:4])

    request = requests.get(url)
    soup = BeautifulSoup(request.content, 'html.parser')
    horses = soup.find_all(class_='HorseList')

    data1 = soup.find(class_='RaceData01')
    data1 = data1.get_text().replace("/","").split()

    data2 = soup.find(class_='RaceData02')
    data2 = data2.select('span')
    data2 = [t.get_text(strip=True) for t in data2]

    data3 = soup.find(class_='Refundlink')
    data3 = str(data3)
    
    for horse in horses:
        horse_data = {}
        horse_data['start_time']    = data1[0]
        horse_data['race_type']     = data1[1]
        horse_data['ground']        = data1[2]
        horse_data['weather']       = data1[3]
        horse_data['gr_condition']  = data1[4]
        horse_data['place']         = data2[1]
        horse_data['rule']          = data2[3]
        horse_data['grade']         = data2[4]
        horse_data['sex']           = data2[5]
        horse_data['rule6']         = data2[6]
        horse_data['num_horse']     = data2[7]
        horse_data['year']          = kaisai_year
        horse_data['date']          = data3[data3.find("kaisai_date=")+12 : data3.find("kaisai_date=")+20]
        horse_data['race_id']       = race_id
        horse_data['rank']          = horse.find(class_='Rank').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['horse_number']  = horse.find(class_='Txt_C').find('div').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['horse_name']    = horse.find(class_='Horse_Name').find('a').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['seirei']        = horse.find(class_='Lgt_Txt Txt_C').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['jockey_weight'] = horse.find(class_='JockeyWeight').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['jockey_name']   = horse.find(class_='Jockey').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['race_time']     = horse.find(class_='RaceTime').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['popularity']    = horse.find(class_='OddsPeople').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['odds']          = horse.find(class_='Txt_R').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['passage']       = horse.find(class_='PassageRate').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['trainer']       = horse.find(class_='Trainer').find('a').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['weight']        = horse.find(class_='Weight').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_datas.append(horse_data)
    return horse_datas

# レースデータをスクレイピング
horse_datas = []
race_ids = get_race_ids(["20230528"])
for race_id in race_ids:
    try:
        # 出馬表を読み取る    
        horse_datas = fetch_horse_datas(race_id, horse_datas)
        time.sleep(1.5)

    except Exception as e:
        print("error in race_id:",race_id,e)
        pass
                    
df = pd.DataFrame(horse_datas)

上記で2023年5月28日の全レース結果をスクレイピングできます。

3.ソースコード一式

以下にソースコード一式をまとめます。 以下では取得したい「年」を指定することで、その年の全レースデータ結果を取得できるようにしてます。

from bs4 import BeautifulSoup
import requests
import pandas as pd
pd.set_option('display.max_columns', 150)
pd.set_option('display.max_rows', 500)
from tqdm import tqdm
import time
import datetime

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

# 今日の日付を取得
today = datetime.date.today()

# 読み取り開始年、終了年
st_year = 2015
ed_year = today.year

# 土日リストを取得
def get_weekends():
    weekends = []
    for target_year in range(st_year, ed_year+1):
        baseDate = datetime.date(target_year, 1, 1)
        days = (baseDate - datetime.date(target_year - 1, 1, 1)).days
        weekends.extend([(baseDate + datetime.timedelta(i)).strftime("%Y%m%d") for i in range(0, days) if (baseDate + datetime.timedelta(i)).weekday() >= 5])
    return weekends

def get_kaisai_dates():
    weekends = get_weekends()
    return [weekend for weekend in weekends if (weekend<today.strftime("%Y%m%d"))]

#selenium driverでブラウザ起動
def get_selenium_driver(url):
    options = Options()
    # ヘッドレスモードで実行する場合
    options.add_argument("--headless")
    driver = webdriver.Chrome(options=options)

    # 取得先URLにアクセス
    driver.get(url)
    # コンテンツが描画されるまで待機
    time.sleep(5)
    
    return driver

#指定した開催日に開催されるレースのrace_id一覧を取得
def get_race_ids(kaisai_dates):
    race_ids = []
    for kaisai_date in tqdm(kaisai_dates):
        url = 'https://race.netkeiba.com/top/race_list.html?kaisai_date=' + kaisai_date
        driver = get_selenium_driver(url)
        elements = driver.find_elements(By.CLASS_NAME,"MyRace_List_Item")
        for element in elements:
            race_id = str(element.get_attribute('id'))
            race_id = race_id.replace("myrace_","")
            race_ids.append(race_id)
    
    return race_ids

def fetch_horse_datas(race_id, horse_datas):
    Base="https://race.netkeiba.com/race/result.html?race_id="
    url = Base + race_id
    kaisai_year = int(race_id[:4])

    request = requests.get(url)
    soup = BeautifulSoup(request.content, 'html.parser')
    horses = soup.find_all(class_='HorseList')

    data1 = soup.find(class_='RaceData01')
    data1 = data1.get_text().replace("/","").split()

    data2 = soup.find(class_='RaceData02')
    data2 = data2.select('span')
    data2 = [t.get_text(strip=True) for t in data2]

    data3 = soup.find(class_='Refundlink')
    data3 = str(data3)

    for horse in horses:
        horse_data = {}
        horse_data['start_time']    = data1[0]
        horse_data['race_type']     = data1[1]
        horse_data['ground']        = data1[2]
        horse_data['weather']       = data1[3]
        horse_data['gr_condition']  = data1[4]
        horse_data['place']         = data2[1]
        horse_data['rule']          = data2[3]
        horse_data['grade']         = data2[4]
        horse_data['sex']           = data2[5]
        horse_data['rule6']         = data2[6]
        horse_data['num_horse']     = data2[7]
        horse_data['year']          = kaisai_year
        horse_data['date']          = data3[data3.find("kaisai_date=")+12 : data3.find("kaisai_date=")+20]
        horse_data['race_id']       = race_id
        horse_data['rank']          = horse.find(class_='Rank').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['horse_number']  = horse.find(class_='Txt_C').find('div').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['horse_name']    = horse.find(class_='Horse_Name').find('a').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['seirei']        = horse.find(class_='Lgt_Txt Txt_C').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['jockey_weight'] = horse.find(class_='JockeyWeight').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['jockey_name']   = horse.find(class_='Jockey').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['race_time']     = horse.find(class_='RaceTime').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['popularity']    = horse.find(class_='OddsPeople').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['odds']          = horse.find(class_='Txt_R').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['passage']       = horse.find(class_='PassageRate').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['trainer']       = horse.find(class_='Trainer').find('a').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_data['weight']        = horse.find(class_='Weight').get_text().replace('\n','').replace(' ','').replace(' ','')
        horse_datas.append(horse_data)
    return horse_datas

# レースデータをスクレイピング
kaisai_dates = get_kaisai_dates()
race_ids = get_race_ids(kaisai_dates)
horse_datas = []
for race_id in tqdm(race_ids):
    try:
        # 出馬表を読み取る    
        horse_datas = fetch_horse_datas(race_id, horse_datas)
        time.sleep(1.5)

    except Exception as e:
        print("error in race_id:",race_id,e)
        pass
                    
df = pd.DataFrame(horse_datas)