[儲からない競馬予想AI] Chapter 01 : データ収集とデータ整形

最初に競馬予測をするためには、データ収集が必要です。
このデータ収集で、ミスしてたりすると、最初からやり直しなので、気分が折れます(3敗)。

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

プログラム

make_database():

netkeiba.comさんの結果URLは規則的に配置されていて、
_https://db.netkeiba.com/race/200003010109
のように、12桁の数字で結果を見れます
虱潰しに入れて行けば、どの数字が何を表すかがわかります。

そして

race_url = f'https://db.netkeiba.com/race/{year}{place_number}{place_open_count}{place_day_count}{round}'

このコードで、いつのレースかを指定できます。

  • year : 開催した年
  • place_number : 開催した場所コード
    • ’01’ : 札幌
    • ’02’ : 函館
    • ’03’ : 福島
    • ’04’ : 新潟
    • ’05’ : 東京
    • ’06’ : 中山
    • ’07’ : 中京
    • ’08’ : 京都
    • ’09’ : 阪神
    • ’10’ : 小倉
  • place_open_count : 「第n回」みたいなやつ
  • place_day_count : n日目みたいなやつ
  • round : その日のラウンド数
    だと思います。

つまり、指定した範囲のURLデータは次のコードで取ってこれます。

def make_database():
    years = [f'20{i:02}' for i in range(25)][::-1]
    race_place_numbers = [
        '01',#札幌
        '02',#函館
        '03',#福島
        '04',#新潟
        '05',#東京
        '06',#中山
        '07',#中京
        '08',#京都
        '09',#阪神
        '10'#小倉
    ]
    race_place_open_counts = [f'{i:02}' for i in range(1,7)]
    race_rounds = [f'{i:02}' for i in range(1,13)]
    for year in years:
        for place_number in race_place_numbers:
            save_name = f'keiba_source_data/{year}_{place_number}.data'
            if os.path.isfile(save_name):
                print(f'already created database :{save_name}')
            else:
                ret  = []
                for place_open_count in race_place_open_counts:
                    for place_day_count in race_place_open_counts:
                        for round in race_rounds:
                            race_url = f'https://db.netkeiba.com/race/{year}{place_number}{place_open_count}{place_day_count}{round}'
                            ret.append(wrapped_url_accecc_func(race_url))
                with open(save_name, mode="wb") as f:
                    pickle.dump(ret, f)
                    print(f'save file as {save_name}')

とりあえず、年と場所で、区切ってデータを収集し、それをpickle形式で保存します。
処理が途中で落ちちゃっても、再開できるようにファイルが既に有れば、取ってこないようにもしました。

回線強度等にも、依存すると思いますが、私の環境では数日かかりました。
joblib等で並列化アクセスすればもっと早いと思いますが、スクレイピングでは推奨されないので、個人の裁量に任せます。

wrapped_url_access_func(race_url, try_count=0):

エラー処理等が起きたときに、リトライするための関数です。

def wrapped_url_access_func(race_url, try_count=0):
    try:
        race_horses_data = url_access_func(race_url)
    except (ValueError, AttributeError, TimeoutError, requests.exceptions.SSLError, requests.exceptions.ChunkedEncodingError) as e:
        print('error page. url:', race_url)
        return {}
    except requests.exceptions.ConnectionError:
        if try_count == 10:
            print('connection error:', race_url, 'no retry')
            return {}
        else:
            print('connection error:', race_url, 'retry after 0.1 sec')
            time.sleep(0.1)
            race_horses_data = wrapped_url_access_func(race_url, try_count= try_count+1)
            return race_horses_data
    else:
        if len(race_horses_data) <= 1:
            print('error page. url:', race_url)
            return {}
        else:
            return race_horses_data

url_access_func(race_url):

更に一枚、関数を噛ませます。タイムアウト処理を施しました。

@timeout(300, use_signals=False)
def url_access_func(race_url):
    print(race_url)
    race_horses_data = get_race_data_from_race_after_URL(race_url)
    time.sleep(0.1)
    return race_horses_data

def get_race_data_from_race_after_URL(url):

長い処理ですが、やってることは単純です。
beautifulsoupで取ってきたhtmlを整形して処理しているだけです。

実際の機械学習には必要ないデータも取ってくるようにしました。
このあたりは、解析してみて、必要かどうかを検証したいと思います。

def get_race_data_from_race_after_URL(url):
    time.sleep(0.1)
    headers = requests.utils.default_headers()
    headers.update({'User-Agent': 'My User Agent 1.0', })

    race_horses_data = []
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')
    race_name = soup.find('dl', class_='racedata fc')
    if race_name == None:
        return []
    temp_data = [item for item in race_name.text.split('\n') if item!='']
    race_num = temp_data[0]
    race_name = temp_data[1]
    race_info = temp_data[2]
    race_type, race_weather, race_ground_type, race_start_time = race_info.strip().split('/')
    #print(race_type, race_weather, race_ground_type, race_start_time)
    race_place_data = {}
    race_place_data['race_name'] = race_name
    race_place_data['race_type'] = race_type.strip()[0]
    race_place_data['race_distance'] = float(race_type.strip()[2:-1])
    race_place_data['weather'] = race_weather.strip()[-1]
    race_place_data['race_condition'] = race_ground_type.strip()[-1]

    temp_data = soup.find('p', class_='smalltxt').text.split(' ')
    race_date = temp_data[0]
    race_date = datetime.datetime.strptime(race_date, '%Y年%m月%d日')
    race_place_data['race_date'] = race_date

    race_grade = temp_data[2]
    race_place_data['race_grade'] = race_grade

    table = soup.find('table', class_='race_table_01 nk_tb_common')
    for row in table.find_all('tr')[1:]:
        #print(race_place_data)
        items = [item for item in row.find_all('td')]
        horse_data_dict = {
            'rank':string_to_number(items[0].text),
            'waku':string_to_number(items[1].text),
            'horse_number':string_to_number(items[2].text),
            'name':items[3].text.strip(),
            'sex':0 if items[4].text[0] == '牡' else 1,
            'age':string_to_number(items[4].text[1]),
            'jocky_weight':string_to_number(items[5].text),
            'jocky_name':items[6].text.strip(),
            'time':items[7].text.strip(),
            'odds':string_to_number(items[12].text),
            'popular':string_to_number(items[13].text),
            'weight':string_to_number(items[14].text[0:3]),
            'weight_sub':string_to_number(items[14].text[4:-1]),
            'prize':string_to_number(items[20].text)
        }
        for key in ['rank', 'waku', 'horse_number', 'age', 'jocky_weight', 'odds', 'popular', 'weight', 'weight_sub']:
            if horse_data_dict[key] == None:
                return []
        href = items[3].find('a').get('href')
        horse_id = href.split('/')[-2]
        horse_ped_data_url = f'https://db.netkeiba.com/horse/ped/{horse_id}/'
        horse_data_dict['ped_data'] = get_ped_data_from_URL(horse_ped_data_url)
        race_horses_data.append(horse_data_dict)
    race_horses_data.sort(key=lambda item : item['horse_number'])

    table = soup.find('table', class_='pay_table_01')
    rows = table.find_all('tr')
    tansyo_items = [item.text.strip() for item in rows[0].find_all('td')]
    hukusyo_items = [item.get_text(',').split(',') for item in rows[1].find_all('td')]
    race_payout_data = {
        'tansyo_ret':tansyo_items[0],
        'tansyo_payout':tansyo_items[1],
        'tansyo_popular':tansyo_items[2],
        'hukusyo_ret':hukusyo_items[0],
        'hukusyo_payout':hukusyo_items[1],
        'hukusyo_popular':hukusyo_items[2],
    }
    ret = {
        'race_place_data':race_place_data,
        'race_horses_data':race_horses_data,
        'race_payout_data':race_payout_data
    }
    return ret

get_ped_data_from_URL(url):

血統データを見る関数です。血統は解析するのが難しいのですが、とりあえず取ってきました。

def get_ped_data_from_URL(url):
    time.sleep(0.1)
    headers = requests.utils.default_headers()
    headers.update({'User-Agent': 'My User Agent 1.0', })
    ped_data_response = requests.get(url, headers=headers)
    ped_data_soup = BeautifulSoup(ped_data_response.content, 'html.parser')
    ped_data_table = ped_data_soup.find('table', class_='blood_table detail')
    peds = []
    for pre_data_row in ped_data_table.find_all('tr'):
        for item in pre_data_row.find_all('td'):
            name = item.find('a').get_text().strip().replace('\n', '')
            peds.append(name)
    return peds

string_to_number

結構な割合で不要な文字(括弧とか)が、切り出した文字中に含まれていることがあります。
しかも、馬が欠場とかでいないと、数値じゃないものが入ってたりします。

既製のfloat関数で文字をキャストすると、数値以外が入っていたときに止まってしまうので、自分で書きました。

def string_to_number(str):
    number = [f'{n}' for n in range(10)] + ['.', '-']
    number_str = ''
    for s in str:
        if s in number:
            number_str += s
    if number_str == '':
        return None
    else:
        return float(number_str)

if name == ‘main‘:

あとは上記の関数を一つのプログラム中に書いて、mainでmake_databaseを呼べば完成です。

if __name__ == '__main__':
    make_database()

実行されると、永遠とページを彷徨ってログが出力されます。

...

競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
error page. url: https://db.netkeiba.com/race/200407030409
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
error page. url: https://db.netkeiba.com/race/200407030411
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
競馬データベース | 競走馬・騎手など情報満載 - netkeiba
netkeibaが誇る国内最大級の競馬データベースです。50万頭以上の競走馬、騎手・調教師・馬主・生産者の全データがご覧いただけます。
...

2000年から2024年までのページを取ってきているので、少し時間がかかります。
実際には機械学習が活発に利用され始める2015年を堺に、オッズ等の結果が変わっていそうです。
ですから、そんな昔まで要らないという方は、パラメータを弄って適度にしてください。

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