基本的は、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件のレースデータが集められました。
機械学習としては少なめですが、分析を始めたいと思います。