【競馬AI①】ほぼコピペだけ!Pythonでnetkeibaからデータを抽出する方法

競馬AI

この記事では、Pythonを用いnetkeibaからスクレイピングでデータを抽出する方法について説明します。

AIや技術系ブログをやっていて、何か実践してみたいなと思って始めたのが競馬予想AIを作ることでした。

競馬ファンなら一度は考えたことがあるのではありませんか?

競馬AIを作るにしてもまずはデータ集めからということで、netkeiba.comさんのデータをお借りして集めていきます。

この記事では難しい説明は抜きにして、プログラミング初心者の方でもほぼコピペだけで簡単に実装できることを目指していますので、是非読んでいってください。

環境準備

以下に示したものが必要なので、持っていないものがあればインストールしてください。

コードエディタ

  • Visual Studio Code

このサイトが参考になります。

パッケージ

  • Python

このサイトが参考になります。

ライブラリ

  • beautifulsoup4
  • requests

以下のコマンドでインストールできます。

pip install beautifulsoup4
pip install requests

もしpipが古いよーとメッセージが出たらアップグレードしてください。

py -m pip install --upgrade pip

※もしインストールでエラーが出た場合は、何度かインストールコマンドを打ってみてください。2~3回目で何故かインストールできることがあります。理由は分かりません。

スクレイピングについて

スクレイピングについては前回の記事で簡単に説明しています。

スクレイピングするためにはnetkeibaさんのURL構造を理解する必要があります。
※コピペするので不要という方は読み飛ばして大丈夫です

URL構造は以下のようになっています。

レースごとに異なる部分をもう少し説明します。
それぞれの数字は「西暦」「競馬場番号」「開催何回目か」「開催何日目か」「何レースか」を表しています。

なので取得したいレースデータの西暦から各数字をカウントアップさせていきながらループしてスクレイピングすれば良いことになります。

競馬場番号は以下の通りとなります。

01020304050607080910
札幌函館福島新潟東京中山中京京都阪神小倉

他のそれぞれの数字は以下のように考えれば大丈夫です。
開催1回~6回
開催日1日目~12日目
レース1R~12R

ここまでわかればURLの理解はOKです。早速スクレイピングしていきましょう。

スクレイピングの実施

コード

import requests
from bs4 import BeautifulSoup
import time
import csv
#取得開始年
year_start = 2019
#取得終了年
year_end = 2022

for year in range(year_start, year_end):
    race_data_all = []
    #取得するデータのヘッダー情報を先に追加しておく
    race_data_all.append(['race_id','馬','騎手','馬番','走破時間','オッズ','通過順','着順','体重','体重変化','性','齢','斤量','上がり','人気','レース名','日付','開催','クラス','芝・ダート','距離','回り','馬場','天気','場id','場名'])
    List=[]
    #競馬場
    l=["01","02","03","04","05","06","07","08","09","10"]
    for w in range(len(l)):
        place = ""
        if l[w] == "01":
            place = "札幌"
        elif l[w] == "02":
            place = "函館"
        elif l[w] == "03":
            place = "福島"
        elif l[w] == "04":
            place = "新潟"
        elif l[w] == "05":
            place = "東京"
        elif l[w] == "06":
            place = "中山"
        elif l[w] == "07":
            place = "中京"
        elif l[w] == "08":
            place = "京都"
        elif l[w] == "09":
            place = "阪神"
        elif l[w] == "10":
            place = "小倉"

        #開催回数分ループ(6回)
        for z in range(7):
            continueCounter = 0  # 'continue'が実行された回数をカウントするためのカウンターを追加
            #開催日数分ループ(12日)
            for y in range(13):
                race_id = ''
                if y<9:
                    race_id = str(year)+l[w]+"0"+str(z+1)+"0"+str(y+1)
                    url1="https://db.netkeiba.com/race/"+race_id
                else:
                    race_id = str(year)+l[w]+"0"+str(z+1)+str(y+1)
                    url1="https://db.netkeiba.com/race/"+race_id
                #yの更新をbreakするためのカウンター
                yBreakCounter = 0
                #レース数分ループ(12R)
                for x in range(12):
                    if x<9:
                        url=url1+str("0")+str(x+1)
                        current_race_id = race_id+str("0")+str(x+1)
                    else:
                        url=url1+str(x+1)
                        current_race_id = race_id+str(x+1)
                    try:
                        r=requests.get(url)
                    #リクエストを投げすぎるとエラーになることがあるため
                    #失敗したら10秒待機してリトライする
                    except requests.exceptions.RequestException as e:
                        print(f"Error: {e}")
                        print("Retrying in 10 seconds...")
                        time.sleep(10)  # 10秒待機
                        r=requests.get(url)
                    #バグ対策でdecode
                    soup = BeautifulSoup(r.content.decode("euc-jp", "ignore"), "html.parser")
                    soup_span = soup.find_all("span")
                    #馬の数
                    allnum=(len(soup_span)-6)/3
                    #urlにデータがあるか判定
                    if allnum < 1:
                        yBreakCounter+=1
                        print('continue: ' + url)
                        continue
                    allnum=int(allnum)
                    race_data = []
                    for num in range(allnum):
                        #馬の情報
                        soup_txt_l=soup.find_all(class_="txt_l")
                        soup_txt_r=soup.find_all(class_="txt_r")
                        #走破時間
                        runtime=''
                        try:
                            runtime=soup_txt_r[2+5*num].contents[0]
                        except IndexError:
                            runtime = ''
                        soup_nowrap = soup.find_all("td",nowrap="nowrap",class_=None)
                        #通過順
                        pas = ''
                        try:
                            pas = str(soup_nowrap[3*num].contents[0])
                        except:
                            pas = ''
                        weight = 0
                        weight_dif = 0
                        #体重
                        var = soup_nowrap[3*num+1].contents[0]
                        try:
                            weight = int(var.split("(")[0])
                            weight_dif = int(var.split("(")[1][0:-1])
                        except ValueError:
                            weight = 0
                            weight_dif = 0
                        weight = weight
                        weight_dif = weight_dif

                        soup_tet_c = soup.find_all("td",nowrap="nowrap",class_="txt_c")
                        #上がり
                        last = ''
                        try:
                            last = soup_tet_c[6*num+3].contents[0].contents[0]
                        except IndexError:
                            last = ''
                        #人気
                        pop = ''
                        try:
                            pop = soup_span[3*num+10].contents[0]
                        except IndexError:
                            pop = ''
                        
                        #レースの情報
                        try:
                            var = soup_span[8]
                            sur=str(var).split("/")[0].split(">")[1][0]
                            rou=str(var).split("/")[0].split(">")[1][1]
                            dis=str(var).split("/")[0].split(">")[1].split("m")[0][-4:]
                            con=str(var).split("/")[2].split(":")[1][1]
                            wed=str(var).split("/")[1].split(":")[1][1]
                        except IndexError:
                            try:
                                var = soup_span[7]
                                sur=str(var).split("/")[0].split(">")[1][0]
                                rou=str(var).split("/")[0].split(">")[1][1]
                                dis=str(var).split("/")[0].split(">")[1].split("m")[0][-4:]
                                con=str(var).split("/")[2].split(":")[1][1]
                                wed=str(var).split("/")[1].split(":")[1][1]
                            except IndexError:
                                var = soup_span[6]
                                sur=str(var).split("/")[0].split(">")[1][0]
                                rou=str(var).split("/")[0].split(">")[1][1]
                                dis=str(var).split("/")[0].split(">")[1].split("m")[0][-4:]
                                con=str(var).split("/")[2].split(":")[1][1]
                                wed=str(var).split("/")[1].split(":")[1][1]
                        soup_smalltxt = soup.find_all("p",class_="smalltxt")
                        detail=str(soup_smalltxt).split(">")[1].split(" ")[1]
                        date=str(soup_smalltxt).split(">")[1].split(" ")[0]
                        clas=str(soup_smalltxt).split(">")[1].split(" ")[2].replace(u'\xa0', u' ').split(" ")[0]
                        title=str(soup.find_all("h1")[1]).split(">")[1].split("<")[0]

                        race_data = [
                            current_race_id,
                            soup_txt_l[4*num].contents[1].contents[0],#馬の名前
                            soup_txt_l[4*num+1].contents[1].contents[0],#騎手の名前
                            soup_txt_r[1+5*num].contents[0],#馬番
                            runtime,#走破時間
                            soup_txt_r[3+5*num].contents[0],#オッズ,
                            pas,#通過順
                            num+1,#着順
                            weight,#体重
                            weight_dif,#体重変化
                            soup_tet_c[6*num].contents[0][0],#性
                            soup_tet_c[6*num].contents[0][1],#齢
                            soup_tet_c[6*num+1].contents[0],#斤量
                            last,#上がり
                            pop,#人気,
                            title,#レース名
                            date,#日付
                            detail,
                            clas,#クラス
                            sur,#芝かダートか
                            dis,#距離
                            rou,#回り
                            con,#馬場状態
                            wed,#天気
                            w,#場
                            place]
                        race_data_all.append(race_data)
                    
                    print(detail+str(x+1)+"R")#進捗を表示
                    
                if yBreakCounter == 12:#12レース全部ない日が検出されたら、その開催中の最後の開催日と考える
                    break
    #1年毎に出力
    #出力先とファイル名は修正してください
    with open('data/'+str(year)+'.csv', 'w', newline='',encoding="SHIFT-JIS") as f:
        csv.writer(f).writerows(race_data_all)
    print("終了")

year_start(開始年)とyear_end(終了年)の期間を延ばせば10年以上のデータを取ることも可能ではありますが、PCのスペックによってはプログラムが停止してしまいます。
2~3年ずつ取得することをオススメします。

コードで何をしているかは簡単にではありますが、コメントで書き込んでいるので読んでみてください。
プログラミングに詳しくない人はあまり気にしなくて良いです。

実行

実行する前に、「data」フォルダを作成してください。作成しないまま実行してしまうと、数十分時間がたった後に出力先のフォルダがないとエラーになってしまいます!
フォルダ構成は以下のようなイメージです。

data(ここが出力先)
 - 2021.csv(スクレイピングで抽出したデータ)
historical_scraping.py

実行するにはターミナル上に以下のコマンドを打ってください。

python historical_scraping.py

ファイル名を変えた場合は「historical_scraping.py」の部分を自分で決めたファイル名にしてください。

ちなみに実行時間は1年分でだいたい30分程度かかります。

動きを試したい場合は、期間を1年にして、競馬場を減らして(01だけにする)実行してみてください。数分で終わると思います。

実行すると指定した期間分の以下のデータが取得されます。

race_id,馬,騎手,馬番,走破時間,オッズ,通過順,着順,体重,体重変化,性,齢,斤量,上がり,人気,レース名,日付,開催,クラス,芝・ダート,距離,回り,馬場,天気,場id,場名

いらないデータなどあれば、削除していただいて結構です。

エラーがなく、実行が完了すれば先ほど作成した「data」フォルダに出力されたはずです。

まとめ

いかがでしたでしょうか?

内容を理解しようとしなければ、ほぼコピペだけで競馬のデータが取得できたのではないでしょうか?
Pythonでスクレイピングと聞くと難しそうな気がしてきますが、意外と簡単に実装して実行することが出来ます。

これで競馬AIに必要なデータの取得が出来ましたので、次回はAIで学習するためのデータの加工を行っていきます。

ブックマークしてお待ちください!

追記

一部コードに不具合がありました。
race_data_allを初期化する位置がループの外にあったため、ループするたびに前の周のデータが残ったままになっていました。(2010~2011でループした場合に、2011.csvに2年分のデータが出力される状態)
すみませんでした。

7/20以前にコピペされた方は上記のコードで取得しなおすか、下記のコードで不要行を削除してください。

import pandas as pd

# ファイルを読み込む
df = pd.read_csv('data/2010.csv', encoding='SHIFT_JIS')
df['race_id'] = df['race_id'].astype(str)
# ヘッダー以外で、特定の文字列から始まらない行を削除
df = df.loc[1:][df['race_id'].str.startswith('2010')]

# CSVに保存
df.to_csv('data/2010_new.csv', encoding='SHIFT_JIS', index=False)

追記②2023/12/13

上記のスクレイピングのコードでは人気や着順が正しく取得できないとご指摘を頂いたので、修正版を以下に記載します。

併せて競馬AI②のencode.pyも修正しないとエラーになるので、そちらもご確認ください。

import requests
from bs4 import BeautifulSoup
import time
import csv
#取得開始年
year_start = 2019
#取得終了年
year_end = 2020

for year in range(year_start, year_end):
    race_data_all = []
    #取得するデータのヘッダー情報を先に追加しておく
    race_data_all.append(['race_id','馬','騎手','馬番','走破時間','オッズ','通過順','着順','体重','体重変化','性','齢','斤量','上がり','人気','レース名','日付','開催','クラス','芝・ダート','距離','回り','馬場','天気','場id','場名'])
    List=[]
    #競馬場
    l=["01","02","03","04","05","06","07","08","09","10"]
    for w in range(len(l)):
        place = ""
        if l[w] == "01":
            place = "札幌"
        elif l[w] == "02":
            place = "函館"
        elif l[w] == "03":
            place = "福島"
        elif l[w] == "04":
            place = "新潟"
        elif l[w] == "05":
            place = "東京"
        elif l[w] == "06":
            place = "中山"
        elif l[w] == "07":
            place = "中京"
        elif l[w] == "08":
            place = "京都"
        elif l[w] == "09":
            place = "阪神"
        elif l[w] == "10":
            place = "小倉"

        #開催回数分ループ(6回)
        for z in range(7):
            continueCounter = 0  # 'continue'が実行された回数をカウントするためのカウンターを追加
            #開催日数分ループ(12日)
            for y in range(13):
                race_id = ''
                if y<9:
                    race_id = str(year)+l[w]+"0"+str(z+1)+"0"+str(y+1)
                    url1="https://db.netkeiba.com/race/"+race_id
                else:
                    race_id = str(year)+l[w]+"0"+str(z+1)+"0"+str(y+1)
                    url1="https://db.netkeiba.com/race/"+race_id
                #yの更新をbreakするためのカウンター
                yBreakCounter = 0
                #レース数分ループ(12R)
                for x in range(12):
                    if x<9:
                        url=url1+str("0")+str(x+1)
                        current_race_id = race_id+str("0")+str(x+1)
                    else:
                        url=url1+str(x+1)
                        current_race_id = race_id+str(x+1)
                    try:
                        r=requests.get(url)
                    #リクエストを投げすぎるとエラーになることがあるため
                    #失敗したら10秒待機してリトライする
                    except requests.exceptions.RequestException as e:
                        print(f"Error: {e}")
                        print("Retrying in 10 seconds...")
                        time.sleep(10)  # 10秒待機
                        r=requests.get(url)
                    #バグ対策でdecode
                    soup = BeautifulSoup(r.content.decode("euc-jp", "ignore"), "html.parser")
                    soup_span = soup.find_all("span")
                    # テーブルを指定
                    main_table = soup.find("table", {"class": "race_table_01 nk_tb_common"})

                    # テーブル内の全ての行を取得
                    try:
                        main_rows = main_table.find_all("tr")
                    except:
                        print('continue: ' + url)
                        continueCounter += 1  # 'continue'が実行された回数をカウントアップ
                        if continueCounter == 2:  # 'continue'が2回連続で実行されたらループを抜ける
                            continueCounter = 0
                            break
                        continue

                    race_data = []
                    for i, row in enumerate(main_rows[1:], start=1):# ヘッダ行をスキップ
                        cols = row.find_all("td")
                        #走破時間
                        runtime=''
                        try:
                            runtime= cols[7].text.strip()
                        except IndexError:
                            runtime = ''
                        soup_nowrap = soup.find_all("td",nowrap="nowrap",class_=None)
                        #通過順
                        pas = ''
                        try:
                            pas = str(cols[10].text.strip())
                        except:
                            pas = ''
                        weight = 0
                        weight_dif = 0
                        #体重
                        var = cols[14].text.strip()
                        try:
                            weight = int(var.split("(")[0])
                            weight_dif = int(var.split("(")[1][0:-1])
                        except ValueError:
                            weight = 0
                            weight_dif = 0
                        weight = weight
                        weight_dif = weight_dif
                        #上がり
                        last = ''
                        try:
                            last = cols[11].text.strip()
                        except IndexError:
                            last = ''
                        #人気
                        pop = ''
                        try:
                            pop = cols[13].text.strip()
                        except IndexError:
                            pop = ''
                        
                        #レースの情報
                        try:
                            var = soup_span[8]
                            sur=str(var).split("/")[0].split(">")[1][0]
                            rou=str(var).split("/")[0].split(">")[1][1]
                            dis=str(var).split("/")[0].split(">")[1].split("m")[0][-4:]
                            con=str(var).split("/")[2].split(":")[1][1]
                            wed=str(var).split("/")[1].split(":")[1][1]
                        except IndexError:
                            try:
                                var = soup_span[7]
                                sur=str(var).split("/")[0].split(">")[1][0]
                                rou=str(var).split("/")[0].split(">")[1][1]
                                dis=str(var).split("/")[0].split(">")[1].split("m")[0][-4:]
                                con=str(var).split("/")[2].split(":")[1][1]
                                wed=str(var).split("/")[1].split(":")[1][1]
                            except IndexError:
                                var = soup_span[6]
                                sur=str(var).split("/")[0].split(">")[1][0]
                                rou=str(var).split("/")[0].split(">")[1][1]
                                dis=str(var).split("/")[0].split(">")[1].split("m")[0][-4:]
                                con=str(var).split("/")[2].split(":")[1][1]
                                wed=str(var).split("/")[1].split(":")[1][1]
                        soup_smalltxt = soup.find_all("p",class_="smalltxt")
                        detail=str(soup_smalltxt).split(">")[1].split(" ")[1]
                        date=str(soup_smalltxt).split(">")[1].split(" ")[0]
                        clas=str(soup_smalltxt).split(">")[1].split(" ")[2].replace(u'\xa0', u' ').split(" ")[0]
                        title=str(soup.find_all("h1")[1]).split(">")[1].split("<")[0]

                        race_data = [
                            current_race_id,
                            cols[3].text.strip(),#馬の名前
                            cols[6].text.strip(),#騎手の名前
                            cols[2].text.strip(),#馬番
                            runtime,#走破時間
                            cols[12].text.strip(),#オッズ,
                            pas,#通過順
                            cols[0].text.strip(),#着順
                            weight,#体重
                            weight_dif,#体重変化
                            cols[4].text.strip()[0],#性
                            cols[4].text.strip()[1],#齢
                            cols[5].text.strip(),#斤量
                            last,#上がり
                            pop,#人気,
                            title,#レース名
                            date,#日付
                            detail,
                            clas,#クラス
                            sur,#芝かダートか
                            dis,#距離
                            rou,#回り
                            con,#馬場状態
                            wed,#天気
                            w,#場
                            place]
                        race_data_all.append(race_data)
                    
                    print(detail+str(x+1)+"R")#進捗を表示
                    
                if yBreakCounter == 12:#12レース全部ない日が検出されたら、その開催中の最後の開催日と考える
                    break
    #1年毎に出力
    #出力先とファイル名は修正してください
    with open('data/'+str(year)+'.csv', 'w', newline='',encoding="SHIFT-JIS") as f:
        csv.writer(f).writerows(race_data_all)
    print("終了")

予想した結果はこちらで公開中!

コメント

  1. ごまちゃん より:

    情報ありがとうございます。上記試しに16行l=6のみで中山のみで実行したところ、2019~2021のcsvは作成されるのですが、2022.csvは作成されません。何か原因って分かりますか?

    • agus agus より:

      コメントありがとうございます。
      pythonのrange関数の仕様です。
      range(0, 5)
      –> 0 1 2 3 4
      今回の場合は以下のようになります。
      range(2019, 2022)
      –> 2019 2020 2021
      rangeの幅を広げれば2022年も作成されます。

      • ぱおん より:

        レースIDが場所名ごとで同じになってしまいます。

        • agus agus より:

          状況がわかりませんが、
          レースIDはループの中で生成しているので、全く同じになるということはないと思います。
          処理の中でprintを使って、レースIDがどうなっているか確認してみてください。

  2. 女王杯 より:

    このコードでできたCSVファイルって日付とか書式設定を変えて保存しなおしたらレースIDが変化しなくなりますか?
    保存しなおしたら毎回レースIDが西暦と競馬場以外0になってしまいます。

    • agus agus より:

      文字コードがUTF-8で出来ているので、保存しなおすと後続の処理で読み込めなくなります。
      そのまま使用いただくようにしてください。

  3. huena より:

    情報提供大変助かります。
    開催日数分のループですが、2回東京の10日目以降(オークス、ダービー)が拾えずなんでだろうと思っていたところ、

    #開催日数分ループ(12日)
    for y in range(13):
    race_id = ”
    if y<9:
    race_id = str(year)+l[w]+"0"+str(z+1)+"0"+str(y+1)
    url1="https://db.netkeiba.com/race/"+race_id
    else:
    race_id = str(year)+l[w]+"0"+str(z+1)+"0"+str(y+1)
    url1="https://db.netkeiba.com/race/"+race_id

    上記のelseがtrue文と同一で 開催日数前の0が不要かと思われます。
    上記だと10日目以降のrace idが「20220501001(例)」になってしまい拾えないかと・・・。
    何分素人でうまく伝えられないですがよろしくお願いします。

  4. hken より:

    コードありがとうございます。
    開催日が10日目以上のデータが取得できませんでした。※2019の東京優駿のデータが無く気が付きました

    49行目は
    race_id = str(year)+l[w]+”0″+str(z+1)+”0″+str(y+1)
    ではなく
    race_id = str(year)+l[w]+”0″+str(z+1)+str(y+1)
    かと思われます。
    ※開催日数が10日以上のとき(すなわちy>=9のとき)、開催日数の前にゼロをつける処理は不要のため

  5. ディンゴ より:

    コードありがとうございます。

    人気のところで取得される数値が8までしかなくというか実際の人気と異なる数値が出力され、最終着順の馬は空白になっています。
    原因は何か分かりますでしょうか?

    • agus agus より:

      全てのデータがそのような結果になっているのでしょうか?
      特定のレースだけというのであれば、そのレースのページの作りが他と異なる可能性などが考えられます。

      • ディンゴ より:

        抽出した全てのレースで同様の結果になっております。

        • agus agus より:

          現象を理解しました。
          元々のソースコードが間違っているようなので、修正してみます。
          ご指摘ありがとうございます。

          記事に追記で、コードを記載したので試してみてください。

          • kon より:

            横から失礼いたします。
            追記いただいたコードなのですが、

            78行目
            main_rows = main_table.find_all(“tr”)
            AttributeError: ‘NoneType’ object has no attribute ‘find_all’

            81行目
            continueCounter += 1 # ‘continue’が実行された回数をカウントアップ
            NameError: name ‘continueCounter’ is not defined

            というエラーが発生してしまいます。
            原因などお判りになりますでしょうか…。

          • agus agus より:

            1点目に関してはmain_tableが正しく取得できていない可能性があります。
            print(main_rows)で期待通りの内容が取得できているか確認してみてください。

            2点目に関してはこちらの修正ミスでした。
            42行目に「continueCounter」の定義を追加したので、確認してみてください。

  6. よしたか より:

    初めまして実行をすると135行目(正確には134行?)で以下のエラーが表示されるのですが

    line 135, in
    con=str(var).split(“/”)[2].split(“:”)[1][1]
    ~~~~~~~~~~~~~~~~~~~^^^
    IndexError: list index out of range

    こちらはどう修正すればいいのかご教授いただけないでしょうか。

    • agus agus より:

      varの中身がどうなっているかを見てみないと回答が難しいですね。
      print(var)で中身を見てみてください。
      varはcols[14].text.strip()なので、
      print(cols)でちゃんと値が入っているのかも見てみてください。

  7. 初心者 より:

    まったく知識のない初心者質問でお恥ずかしいのですが、「実行する前に、「data」フォルダを作成してください」というのはどこにどうフォルダを作成したらよいのでしょうか。

    お手数をおかけしますがご教授頂けると幸いです。

    • agus agus より:

      以下のようなフォルダ構成のイメージです。
      -dataフォルダ
      |-2023.csv
      -encodedフォルダ
      |-encoded_data.csv
      encode.py
      predict.py
      race_table_scraping.py

      • 初心者 より:

        返信ありがとうございます。

        記載していただいたフォルダ構成と、修正済みコードをそのままPython上にコピペして、ターミナルで実行コマンドを打てば完成ということでよろしいでしょうか?

        コピペ以外に入力しなければいけないものや、準備が必要なことがあれば教えていただきたいです。

        まったく初めてで基礎もよくわかっていませんが、ご迷惑でなければよろしくお願いいたします。

        • agus agus より:

          基本的にはご認識の通りです。
          あとはコードの先頭でimportしているライブラリがインストールされていなければインストールする必要があります。
          実行してみてエラーが出たらエラーメッセージでググるか、「ライブラリ名 インストール」などと調べればコマンドが出てきます。

          他に修正するとすれば、ファイル名やフォルダ名などは任意ですので、自分でわかりやすい名称に変更するくらいだと思います。

  8. 初心者 より:

    重ね重ねありがとうございます。
    Pythonに先ほどいただいたデータのコードをコピペすると、このようなエラーが出てしまうのですが、何か対処法などはございますでしょうか?
    ご迷惑をおかけしてしまい、お恥ずかしいばかりですがよろしくお願いします。

    >>> -dataフォルダ
    Traceback (most recent call last):
    File “”, line 1, in
    NameError: name ‘dataフォルダ’ is not defined
    >>> |-2023.csv
    File “”, line 1
    |-2023.csv
    ^
    SyntaxError: invalid syntax
    >>> -encodedフォルダ
    Traceback (most recent call last):
    File “”, line 1, in
    NameError: name ‘encodedフォルダ’ is not defined
    >>> |-encoded_data.csv
    File “”, line 1
    |-encoded_data.csv
    ^
    SyntaxError: invalid syntax
    >>> encode.py
    Traceback (most recent call last):
    File “”, line 1, in
    NameError: name ‘encode’ is not defined
    >>> predict.py
    Traceback (most recent call last):
    File “”, line 1, in
    NameError: name ‘predict’ is not defined
    >>> race_table_scraping.py

    • agus agus より:

      -data/
      |-2023.csv
      -encoded/
      |-encoded_data.csv
      encode.py
      predict.py
      race_table_scraping.py

      ↑をどこかに書き込んだんですか?
      上記はフォルダ構成のイメージになります。
      自分のソースコード、フォルダが上記のような配置になっているかを確認していただければよくて、
      どこにも書き込む必要はないです。

      • 初心者 より:

        返信ありがとうございます。

        デスクトップ上に新規作成でdataファイルを作るだけで大丈夫ということですか?何分知識がないもので難しく考えていたかもしれません。

        また、Pythonにコピペ後コマンドプロンプト上に
        python historical_scraping.py
        と打ち込んでも

        C:\Users\81***>python historical_scraping.py
        Python
        C:\Users\81***>

        と出るだけで何も始まりません。
        何か根本的に間違えているのでしょうか。
        お手数おかけして本当に申し訳ありません。

        • agus agus より:

          historical_scraping.pyと同じ階層にdataフォルダを作るだけで大丈夫です。
          デスクトップ上ではなく、「keiba」など何でもいいのでフォルダ作ってファイルはまとめておいた方が良いです。
          あとPCにpythonがインストールされているか確認してみてください。

          • 初心者 より:

            返信ありがとうございます。

            ファイルなどの作成はできたのですが、今度は

            C:\keiba>python historical_scraping.py
            C:\Users\81***\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\python.exe: can’t find ‘__main__’ module in ‘C:\\keiba\\historical_scraping.py’

            というエラーが出てしまいます。
            調べてもピンとこないのですが、何か解決方法はありますでしょうか。
            何度も申し訳ございません。

          • agus agus より:

            エラーを見る限り、このあたりのエラーだと思いますが、こちらでは出ないので何とも。。。
            https://aiacademy.jp/media/?p=1478

  9. ルイ より:

    はじめまして!プログラミングというのが何もわからいのですが競馬AIを自分で作ってみたいと思い色々調べたらこのページに辿りついたので今から競馬AIを作成していきたいと思っていますが、最初のVisual Studio CodeをPCに入れてその後このVisual Studio Code内にPythonを入れました
    この後ってどのようにしていけばいいのでしょうか?
    ライブラリ内の記載してあるbeautifulsoup4とrequestsをインストールしたいのですができずに手が止まってしまっています

    PythonはVisual Studio Code内にしかなくコマンドプロンプトでPythonと記載するとインストール画面に行ってしまいます

    • agus agus より:

      pipコマンドはVisual Studio CodeのTerminalウィンドウ内で実行してください。
      pythonのインストール以外基本的にコマンドプロンプトは使用せず、Visual Studio CodeのTerminalウィンドウを使用します。

  10. ルイ より:

    ご返信ありがとうございます
    Visual Studio CodeのTerminalウィンドウの欄で上記のpipでインストールしたら
    Requirement already satisfied: beautifulsoup4 in c:\users\81***\appdata\local\programs\python\python312\lib\site-packages (4.12.2)
    Requirement already satisfied: soupsieve>1.2 in c:\users\81***\appdata\local\programs\python\python312\lib\site-packages (from beautifulsoup4) (2.5)と出たのですが、これってインストールできてるんですかね?
    最初えあらーが出てアップデートのコード入れてから再度コピペしたら先程のコードが出ました

    またこれでもし大丈夫であれば、今度コードをコピペして実行する前にファイルを作ると思うのですが、これってVisual Studio Codeのファイル➡新しいファイル➡Pythonファイルで作成して、1度保存して名前を.pyにしてコードを実行していけば良いのですか?無知ですみませんが教えて頂けると幸いです。

  11. なか より:

    追記②2023/12/13のコードを実行すると
    …….
    1回札幌6日目10R
    1回札幌6日目11R
    1回札幌6日目12R
    continue: https://db.netkeiba.com/race/201901010701
    Traceback (most recent call last):
    File “C:\Users……\historical_scraping.py”, line 78, in
    main_rows = main_table.find_all(“tr”)
    ^^^^^^^^^^^^^^^^^^^
    AttributeError: ‘NoneType’ object has no attribute ‘find_all’

    During handling of the above exception, another exception occurred:
    とエラーが出ます。
    「修正版を以下に記載します。」と書いてありましたのでこちらを実行したのですが、一番上のコードでよかったのでしょうか?

  12. kura より:

    修正版の②のコードを実行したのですが出力されるCSVファイルで「通過順」が一部日付表示になっているのは仕様ですか?

    • agus agus より:

      excelやスプレッドシートの仕様です。
      通過順が1-1-1だと「2001/1/1」と認識されてしまっているだけです。
      excelやスプレッドシートで上書きしなければ問題ないです。

  13. より:

    スクレイピングのやり方を教えていただきありがとうございます。
    追加で厩舎のデータをスクレイピングするにはどのようにすればよいでしょうか。
    お手数をおかけしますがご教授頂けると幸いです。

    • agus agus より:

      西とか東という部分のことであってますか?
      対象のデータは18カラム目にあり、2文字目だけを抽出するには以下のようにすれば出来ると思います。
      cols[18].text[2].strip()

      • より:

        お返事ありがとうございます。
        cols[6].text.strip(),#騎手の名前
        のように厩舎(調教師)のデータを取りたいと考えております。

  14. 勉強中 より:

    お世話になっております。このコードを地方で使いたい場合は、どのような変更を加えればよいですか?また、ほかの投稿にあるコードも変更が必要になりますか?お手隙の際にお返事お願いいたします。

    • agus agus より:

      レースIDの仕組みを解読して、ループの変数を修正する必要があります。
      3月9日佐賀競馬1RのIDが以下です。
      202455030901
      分解すると
      2024 55 0309 01
      年 競馬場 日付 レース
      であることがわかります。
      全ての競馬場のIDを整理し、1年間すべての日付でループするように加工します。

      スクレイピング出来て、取得されたデータが同じであればある程度他のプログラムも動くと思います。

  15. TM より:

    コードありがとうございます。
    このサイトで公開されているコードはしっかり出力させることが出来ました。個人的な要望でなのですが、年ごとだけでなく、日付ごとでデータを抽出したいと思い、コードを書き換えたのですが、どうしてもレースデータが反映されません。そのコードでは出力したスクレイピング先のURLを踏むとおそらく読み込む該当ページではあるもののレース情報が表示されていません。特別コードを触ったわけではないとは思うのですが何かnetkeibaのデータを読み取る上で日付指定にすると問題点があるのでしょうか。幾らか助言頂けると幸いです。

    • agus agus より:

      まずはprintでurlが正しいものなのか確認してみてください。
      try:
      print(url)
      r=requests.get(url)

      日付指定についてですが、中央競馬のrace_idには日付は使用されていません。
      「西暦」「競馬場番号」「開催何回目か」「開催何日目か」「何レースか」で構成されているので、
      日付指定の方法が出来るのか私にもわかりません。

      • TM より:

        url自体間違っておりました。urlの情報がrace_idと日付で混同しておりそれによってデータだけでないような形での不具合が生じておりました。日付ではなく指定したい開催日をrace_idに変換するような形で組み立てようと思います。助言の程助かりました。ありがとうございます。

  16. UU より:

    コメント失礼します。記載されている手順通りに実施したのですが
    VS Codeのターミナル上で「python historical_scraping.py」を実行しても
    データのスクレイピングがされずコマンドが終了してしまいます。

    【事象】
    デバッグをすると、「for year in range(year_start, year_end):」以降のコードが実行されていないようでした。
    実験として上記文の前にprint文を記載すると、その内容はターミナルに出力されていました。
    【環境】
    ・OS:Windows11
    ・Python:3.12.2

    原因がお分かりでしたらご教示いただけますと幸いです。

    • agus agus より:

      エラーメッセージなどは何か出ていましたか?
      あとPCの設定でスクレイピングが許可されていないのか。
      私のPCも再起動後は「許可する」「許可しない」のダイアログが出たりします。

      • UU より:

        連絡ありがとうございます。
        はっきりとした原因が分からず終いですが、ひとまず解決しました。

        【解決策】
        質問文に記載した「for year in range(year_start, year_end):」
        のループを削除し、単一年度のみで実行させるよう修正

  17. ビギナーパナ より:

    ソースコードありがとうございます。
    とても参考になりました!
    お時間ある際に追加のアドバイスいただけると嬉しいです。
    実験的に競馬場を絞って実行したところ、書き込みのタイミングでエラーがでてきました。札幌競馬場のみ(01)なら出力されますが、他競馬場で実施した場合は下記エラーになります。

    Traceback (most recent call last):
    File “c:\Users\81907\Desktop\プログラミング\競馬スクレイピング\hello.py”, line 193, in
    with open(‘data/’+str(year)+’.csv’, ‘w’, newline=”,encoding=”SHIFT-JIS”) as f:
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    FileNotFoundError: [Errno 2] No such file or directory: ‘data/2024.csv’

    ※成功したときはdataに格納されてます。

    また、別件になりますが追記2番について
    こちらの0の部分が反映されてなさそうでした。
    if y<9:
    race_id = str(year)+l[w]+"0"+str(z+1)+"0"+str(y+1)
    url1="https://db.netkeiba.com/race/"+race_id
    else:
    race_id = str(year)+l[w]+"0"+str(z+1)+str(y+1)
    url1="https://db.netkeiba.com/race/"+race_id

    • agus agus より:

      コメントありがとうございます。
      こちらは特にエラーになりませんでした。
      特定の競馬場の実のデータを取得する場合、16行目のコードを修正しました。
      試してみてください。
      l=[“01″,”02″,”03″,”04″,”05″,”06″,”07″,”08″,”09″,”10”]

      l=[“03”]

  18. nana より:

    はじめまして
    元々競馬が好き+機械学習に興味があり始めてみましたが、とても参考になっています。
    今回ご意見いただきたくコメントいたしました。

    予想の精度を上げるために上記のhistorical_scraping.py中に出走馬毎のurl(例:コントレイルならhttps://db.netkeiba.com/horse/2017101835/)を取得し、3世代前までの血統情報を追加しようと考えていますが中々血統情報の取得ができず、ターミナルに
    「Error parsing pedigree for horse https://db.netkeiba.com/horse/2021106247/: list index out of range」(下記コードで表示するようにしたエラー)が表示され、どうしたものかと思案しています。
    そもそも別で血統情報を取得したほうがよいのか等、agus様ならどのように取得しますでしょうか?

    以下にコード記載します。
    def get_horse_pedigree(horse_url):
    response = requests.get(horse_url)
    response.encoding = response.apparent_encoding
    soup = BeautifulSoup(response.text, ‘html.parser’)

    # 血統情報を格納する辞書
    pedigree = {
    ‘horse_id’: horse_url.split(‘/’)[-2],
    ‘父’: ‘不明’, ‘母’: ‘不明’, ‘父父’: ‘不明’, ‘父母’: ‘不明’,
    ‘母父’: ‘不明’, ‘母母’: ‘不明’, ‘父父父’: ‘不明’, ‘父父母’: ‘不明’,
    ‘父母父’: ‘不明’, ‘父母母’: ‘不明’, ‘母父父’: ‘不明’, ‘母父母’: ‘不明’,
    ‘母母父’: ‘不明’, ‘母母母’: ‘不明’
    }

    try:
    pedigree_table = soup.find(‘table’, class_=’blood_table’)
    rows = pedigree_table.find_all(‘tr’)

    # 血統情報を辞書に追加
    pedigree[‘父’] = rows[0].find_all(‘td’)[1].text.strip()
    pedigree[‘母’] = rows[2].find_all(‘td’)[1].text.strip()
    pedigree[‘父父’] = rows[0].find_all(‘td’)[3].text.strip()
    pedigree[‘父母’] = rows[1].find_all(‘td’)[3].text.strip()
    pedigree[‘母父’] = rows[2].find_all(‘td’)[3].text.strip()
    pedigree[‘母母’] = rows[3].find_all(‘td’)[3].text.strip()
    pedigree[‘父父父’] = rows[0].find_all(‘td’)[5].text.strip()
    pedigree[‘父父母’] = rows[1].find_all(‘td’)[5].text.strip()
    pedigree[‘父母父’] = rows[2].find_all(‘td’)[5].text.strip()
    pedigree[‘父母母’] = rows[3].find_all(‘td’)[5].text.strip()
    pedigree[‘母父父’] = rows[2].find_all(‘td’)[7].text.strip()
    pedigree[‘母父母’] = rows[3].find_all(‘td’)[7].text.strip()
    pedigree[‘母母父’] = rows[4].find_all(‘td’)[7].text.strip()
    pedigree[‘母母母’] = rows[5].find_all(‘td’)[7].text.strip()
    except Exception as e:
    print(f”Error parsing pedigree for horse {horse_url}: {e}”)

    return pedigree
    以上

    • agus agus より:

      コメントありがとうございます。
      私は2世代前までの血統情報を取得しています。
      3世代以上となると「https://db.netkeiba.com/horse/ped/2017101835/」のURLになるのではないでしょうか?

      また、私は別で血統情報を取得し後からマージしています。(初期に組み込まなかっただけです)
      他の情報と一緒にスクレイピング出来るのであれば、一緒で良いと思います。

      2世代前までなら、以下のコードで取得できるかもしれません。
      try:
      request = requests.get(url)
      #リクエストを投げすぎるとエラーになることがあるため
      #失敗したら10秒待機してリトライする
      except requests.exceptions.RequestException as e:
      print(f”Error: {e}”)
      print(“Retrying in 10 seconds…”)
      time.sleep(10) # 10秒待機
      request = requests.get(url)
      #バグ対策でdecode
      soup_horse = BeautifulSoup(request.content.decode(“euc-jp”, “ignore”), “html.parser”)
      ml = soup_horse.find_all(“td”, class_=”b_ml”)
      fa = ml[0].text.strip()#父
      fafa = ml[1].text.strip()#父父
      mo = soup_horse.find(“td”, class_=”b_fml”).text.strip()#母
      mofa = ml[2].text.strip()#母父

      • nana より:

        回答ありがとうございます。
        確かに3世代前となるとURLが違いました
        コードも記載いただいてありがとうございます。
        参考にさせていただきます。

  19. 初心者 より:

    デスクトップにdataフォルダを作り
    Visual Studio Codeの
    ファイル→新しいファイル→pythonファイル→名前をつけて
    →コードをペースト→ターミナルを押すと勝手に実行されます。
    でもdataには出力?できてないみたいで
    FileNotFoundError: [Errno 2] No such file or directory: ‘data/2019.csv’
    と表示をされます。
    フォルダの作る場所が違うのでしょうか?

    • agus agus より:

      違うと思われます。
      以下のようなフォルダ構成になります。
      -source/
      -data/
      |-2023.csv
      historical_scraping.py

  20. かっち より:

    追記②2023/12/13を拝借して、地方競馬用に加工してダウンロードしましたが、笠松競馬で降雪で中止になった日(2024年1月24日、https://db.netkeiba.com/race/202447012401)があると、結果データが空欄のためかPYTHONがエラーで止まってしまいます。
    対処法はありますでしょうか(PYTHON初心者です)

    • agus agus より:

      どのようなエラーが出ているかわからないので確実なことは言えませんが、
      レース自体が中止になった場合は空欄になる箇所があると思います。(天候、馬場状態、着順やタイムなど)
      取得した値がもし空欄であればcontinueもしくはbreakして次のループに進める処理を入れれば大丈夫だと思います。

      • かっち より:

        笠松競馬2024年1月9日~1月23日、1月25日~1月26日、2月5日~7月3日と期間を分けたら問題なくダウンロードできました。

  21. やまお より:

    このブログで学ばせていただいております。
    初心者のため、質問が初歩的かもしれませんが、各レースの出走頭数をデータに加えたい場合はどのようなコードを使用すれば良いかご教示ください。

    • agus agus より:

      75行目にallnumがあるので、それを183行目の最後尾に追加すれば行けると思います。
      ヘッダーも追加が必要なので、13行目の修正もお忘れなく。

  22. やまお より:

    初心者です。
    スクレイピングする項目の中に発走頭数を追加したいのですが、どのようなプログラムを組めばよろしいかご教示いただけますか?

    • agus agus より:

      75行目にallnumがあるので、それを183行目の最後尾に追加すれば行けると思います。
      ヘッダーも追加が必要なので、13行目の修正もお忘れなく。

  23. さくら より:

    初心者で申し訳ございません。
    data(ここが出力先)
     - 2021.csv(スクレイピングで抽出したデータ)
    historical_scraping.py

    このhistorical_scraping.pyと言うデータはどういったデータになりますか?

  24. Python初心者 より:

    はじめまして、Pythonでのスクレイピングによるデータ収集の練習として参考にさせていただいています。
    先述にもありますが、私も実行後に
    “FileNotFoundError: [Errno 2] No such file or directory: ‘data/2023.csv’ ”
    というエラーが表示されてしまいます。
    現在デスクトップに「python」というフォルダを作り、その中に「historical_scraping.py」としてコードを保存し、さらに「data」というフォルダを作成して、中に「2022.csv」や「2023.csv」という名前で事前にからのファイルを作成、保存しています。
    デスクトップ→Python→data→任意のファイルという感じです。
    これでもエラーが解決できないのですが、私の認識で修正個所がありましたら教えてください!

    • agus agus より:

      事前に空のファイルを作っておく必要はありません。
      dataフォルダが見つかっていないエラーのようなので、
      ファイル書き込み直前に以下のコードを入れてみて、どこにフォルダが作成されるか確認してみてください。

      # ディレクトリが存在しない場合、作成する
      if not os.path.exists(‘data’):
      os.makedirs(‘data’)

  25. ハル より:

    初心者です
    何度やっても2019 札幌 2開催 6日までしか抽出できません
    年度を2020、開催場所を 函館だけにしても同じです
    何が悪いのでしょうか?

    • agus agus より:

      札幌の開催回数は2回なので正しいと思います。
      函館は1回12日開催のようです。

      • ハル より:

        お返事ありがとうございます。
        質問の意図が伝わっていなかったようです

        #取得開始年
        year_start = 2022
        #取得終了年
        year_end = 2022

        としても2019年だけが結果出力されます

        #競馬場
        l=[“02”]

        としても札幌だけが結果出力されます

        • ハル より:

          上記を投稿したばかりですが、何故か採取できるようになりました
          何もしていないので不思議ですが、解決したのでOKです
          ありがとうございました
          今後いろいろ試していきます