[PIST6(競輪)を機械学習で儲かるか検証した話] Chapter 01 : データ収集とデータ整形

基本的は、https://keirin.netkeiba.com/ さんからデータをスクレイピングで抜いています。
あんまり負荷をかけるのは、よくないので各自モラルを守ってスクレイピングしてください。

keirin.netkeiba.comさんのレースURLは規則的に配置されていて、
https://keirin.netkeiba.com/pist6/entry/?race_id=202405099106
のように、下12桁の数字でレースの情報を見れます。

また、pist6は上のURLですが、一般の競輪は
https://keirin.netkeiba.com/race/entry/?race_id=201206134611
のように若干URLが違いますが、ほとんど同じ方法でデータが見れます。

プログラム

ライブラリ

BeautifulSoupとSeleniumの両方を使ってデータを持ってきます。BeautifulSoupで取れるデータは、BeautifulSoupで取りますが、オッズデータなどの動的ページはBeautifulSoupでは取れないので、Seleniumを使います。

import requests
from bs4 import BeautifulSoup
import json
import os
from timeout_decorator import timeout, TimeoutError
from selenium import webdriver
from selenium.webdriver.common.by import By
import selenium

get_shusso_data(race_id):

レースID(12桁の数字)から出走表のデータを取って来る関数です。今節成績と、前回成績はテーブル幅が変動して、固定データとして扱いにくいため今回はとりません。

出走データはBeautifulSoupで取れるため、BeautifulSoupで取ります(Seleniumでデータを取るのが遅いためです)

def get_shusso_data(race_id):
    url = f'https://keirin.netkeiba.com/pist6/entry/?race_id={race_id}'
    headers = requests.utils.default_headers()
    headers.update({'User-Agent': 'My User Agent 1.0', })
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')

    title = soup.find('title').text
    if '不明' in title:
        return []

    table = soup.find(id='RaceCard_Table_Static')
    if table == None:
        return []

    result = []
    for row in table.find_all('tr')[1:]:
        items = [item for item in row.find_all('td')]

        name_data = [txt for txt in items[3].text.split('\n') if len(txt) > 0]

        player_name_href = items[3].find('a').get('href')
        player_id = player_name_href.split('=')[-1]

        odds_data = items[4].text.split('倍')

        player_data = {
            'player_id':player_id,
            'number':items[0].text,
            'name':name_data[0],
            'age':name_data[-1].split()[-1],
            'country':name_data[-1].split()[0],
            'odds':odds_data[0],
            'popular':odds_data[-1][:-2],
            'gear_rate':items[5].text,
            'time_trial_time':items[6].text[:6],
            'time_trial_rank':items[6].text[6:-1],
        }
        result.append(player_data)
    return result

get_odds_data(race_id, driver):

レースID(12桁の数字)からオッズのデータを取って来る関数です。
オッズデータはBeautifulSoupで取れないため、Seleniumで取ります

def get_odds_data(race_id, driver):
    def get_tansyo_odds(driver, buttons):
        buttons[0].click()
        ticket_list = driver.find_elements(By.CLASS_NAME, 'PlayerList')
        result = []
        for ticket in ticket_list:
            row = ticket.find_elements(By.CSS_SELECTOR, 'td')


            ticket_data = {
                'ticket':int(row[1].text),
                'odds':float(row[5].text) if row[5].text != '取消' else None
            }

            result.append(ticket_data)
        return result

    def get_ticket_odds_info(driver, buttons, click_index):
        buttons[click_index].click()
        ticket_list = driver.find_elements(By.CLASS_NAME, 'PlayerList')
        result = []
        for ticket in ticket_list:
            row = ticket.find_elements(By.CSS_SELECTOR, 'td')
            ticket_text = row[2].text
            ticket = list(map(int, ticket_text.split('\n')[::2]))
            ticket_data = {
                'ticket':ticket,
                'odds':row[3].text
            }
            #print(ticket_data)
            result.append(ticket_data)
        return result
    url = f'https://keirin.netkeiba.com/pist6/odds/?race_id={race_id}'
    driver.get(url)

    buttons = driver.find_elements(By.CLASS_NAME, 'RaceOdds_MenuArea_listInner')
    result = {
        'tansyo':get_tansyo_odds(driver, buttons),
        'sanrentan':get_ticket_odds_info(driver, buttons, 1),
        'nirentan':get_ticket_odds_info(driver, buttons, 2),
        'sanrenhuku':get_ticket_odds_info(driver, buttons, 3),
        'nirenhuku':get_ticket_odds_info(driver, buttons, 4),
        'wide':get_ticket_odds_info(driver, buttons, 5),
    }
    return result

get_player_data(race_id):

レースID(12桁の数字)から選手のデータ(タイムトライアル情報と前走の情報)を取って来る関数です。
選手のデータはBeautifulSoupで取れるため、BeautifulSoupで取ります

def get_player_data(race_id):
    url = f'https://keirin.netkeiba.com/pist6/data/?race_id={race_id}'
    headers = requests.utils.default_headers()
    headers.update({'User-Agent': 'My User Agent 1.0', })
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')

    table = soup.find(id='RaceCard_Table_Static')
    if table == None:
        return []

    result = []
    for row in table.find_all('tr')[1:]:
        items = [item for item in row.find_all('td')]
        player_data = {
            'time_trial_time':items[4].text,
            'time_trial_gear_rate':items[5].text,
            'time_trial_kmps':items[6].text,
            'pre_lap_time':items[7].text,
            'pre_gear_rate':items[8].text
        }
        result.append(player_data)
    return result

get_rank_data(race_id, driver):

レースID(12桁の数字)から結果のデータを取って来る関数です。
結果のデータはBeautifulSoupで取れないため、Seleniumで取ります

def get_rank_data(race_id, driver):
    url = f'https://keirin.netkeiba.com/pist6/result/?race_id={race_id}'
    driver.get(url)
    button = driver.find_element(By.CLASS_NAME, 'Result_Show_Btn')
    button.click()
    players = driver.find_elements(By.CLASS_NAME, 'PlayerList')
    result = []
    for n, player in enumerate(players):
        row = player.find_elements(By.CSS_SELECTOR, 'td')
        player_data = {
            'rank':n+1,
            'number':int(row[1].text),
            'time':row[3].text.split('\n')[0]
        }
        result.append(player_data)
    return result

get_payout_data(race_id, driver):

レースID(12桁の数字)から選手の支払い情報を取って来る関数です。
支払い情報はBeautifulSoupで取れるため、BeautifulSoupで取ります

def get_payout_data(race_id, driver):

    def get_ticket_payout_info(soup, target):
        tables = soup.find_all(class_='Payout_Detail_Table')
        payout_info_list = tables[-1].find_all(class_=target)
        ticket_rets = []
        if len(payout_info_list) != 0:
            for payout_info in payout_info_list:
                ticket_rets.append({
                    'ret':payout_info.find(class_='Result').text,
                    'payout':payout_info.find(class_='Payout').text,
                    'popular':payout_info.find(class_='Ninki').text
                })
        return ticket_rets

    url = f'https://keirin.netkeiba.com/pist6/result/?race_id={race_id}'
    headers = requests.utils.default_headers()
    headers.update({'User-Agent': 'My User Agent 1.0', })
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')

    title = soup.find('title').text
    if '不明' in title:
        return {}

    result = {
        'tansyo':get_ticket_payout_info(soup, 'Tansho'),
        'nirenhuku':get_ticket_payout_info(soup, 'Umaren'),
        'nirentan':get_ticket_payout_info(soup, 'Umatan'),
        'wide':get_ticket_payout_info(soup, 'Wide'),
        'sanrenhuku':get_ticket_payout_info(soup, 'Fuku3'),
        'sanrentan':get_ticket_payout_info(soup, 'Tan3'),
    }
    return result

get_race_data(race_id)

レースID(12桁の数字)から各関数を呼び出して、データを集める関数です。
それぞれのデータをdictに格納して返します。
存在しないレースIDの場合は空のdictを返します。

def get_race_data(race_id):
    print(f'get race : {race_id}')
    info = {
        'race_id':race_id,
        'date':f'{race_id[:4]}-{race_id[4:6]}-{race_id[6:8]}'
    }
    try:
        info['shusso_data'] = get_shusso_data(race_id)
    except (requests.exceptions.ConnectionError, selenium.common.exceptions.NoSuchElementException):
        print('error requests')
        return {}

    if len(info['shusso_data']) == 0:
        print('not exist race')
        return {}
    info['player_data'] = get_player_data(race_id)
    with webdriver.Remote(
        command_executor="http://chrome:4444/wd/hub",
        options=webdriver.ChromeOptions()
    ) as driver:
        driver.implicitly_wait(10)
        info['odds_data'] = get_odds_data(race_id, driver)
        info['rank_data'] = get_rank_data(race_id, driver)
        info['payout_data'] = get_payout_data(race_id, driver)
    return info

if name == ‘main‘:

レースのIDは左から、[年、月、日、場所、ラウンド]となっています。
pist6の場合は、場所は91で固定されています(開催場所が一つしか無い)。
また、年は2021年からの開催なので、それ以降です。
これの12桁のレースIDを総当りでレース情報を取ってきます(存在しないレースIDもあります)

その後、取ってきたデータをjson形式で保存します。
また、存在しないレースIDはno_exist_races.txtに保存します。再度プログラムを回すときに、存在しないレースIDにアクセスせずに済みます。

    save_dir = 'souce_data/raw_data'
    os.makedirs(save_dir, exist_ok=True)
    '''
    race_id = '202110029101'
    data = get_race_data(race_id)
    #print(data)
    exit()
    '''

    no_exist_check_file_path = f'souce_data/no_exist_races.txt'
    no_exist_check = set()
    if os.path.isfile(no_exist_check_file_path):
        with open(no_exist_check_file_path, 'r') as f:
            text = f.read()
        no_exist_check = set(text.split('\n'))

    for year in [2021, 2022, 2023, 2024]:
        for month in range(1, 13):
            for day in range(1, 32):
                for race_number in range(1, 13):
                    race_id = f'{year}{month:02d}{day:02d}91{race_number:02d}'
                    save_path = f'{save_dir}/{race_id}.json'
                    if race_id not in no_exist_check and os.path.isfile(save_path) == False: 
                        data = get_race_data(race_id)
                        if data != {}:
                            with open(save_path, 'w') as f:
                                json.dump(data, f, ensure_ascii=False, indent=4)
                        else:
                            with open(no_exist_check_file_path, 'a') as f:
                                f.write(f'{race_id}\n')

実行すると、半日程度でデータが集まります。
2024-05-01時点で、2736件のレースデータが集められました。
機械学習としては少なめですが、分析を始めたいと思います。

タイトルとURLをコピーしました