この記事では、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構造は以下のようになっています。
![](https://agusblog.net/wp-content/uploads/2023/06/image-16.png)
レースごとに異なる部分をもう少し説明します。
それぞれの数字は「西暦」「競馬場番号」「開催何回目か」「開催何日目か」「何レースか」を表しています。
![](https://agusblog.net/wp-content/uploads/2023/06/image-17.png)
なので取得したいレースデータの西暦から各数字をカウントアップさせていきながらループしてスクレイピングすれば良いことになります。
競馬場番号は以下の通りとなります。
01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 |
札幌 | 函館 | 福島 | 新潟 | 東京 | 中山 | 中京 | 京都 | 阪神 | 小倉 |
他のそれぞれの数字は以下のように考えれば大丈夫です。
開催は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("終了")
予想した結果はこちらで公開中!
コメント
情報ありがとうございます。上記試しに16行l=6のみで中山のみで実行したところ、2019~2021のcsvは作成されるのですが、2022.csvは作成されません。何か原因って分かりますか?
コメントありがとうございます。
pythonのrange関数の仕様です。
range(0, 5)
–> 0 1 2 3 4
今回の場合は以下のようになります。
range(2019, 2022)
–> 2019 2020 2021
rangeの幅を広げれば2022年も作成されます。
レースIDが場所名ごとで同じになってしまいます。
状況がわかりませんが、
レースIDはループの中で生成しているので、全く同じになるということはないと思います。
処理の中でprintを使って、レースIDがどうなっているか確認してみてください。
このコードでできたCSVファイルって日付とか書式設定を変えて保存しなおしたらレースIDが変化しなくなりますか?
保存しなおしたら毎回レースIDが西暦と競馬場以外0になってしまいます。
文字コードがUTF-8で出来ているので、保存しなおすと後続の処理で読み込めなくなります。
そのまま使用いただくようにしてください。
情報提供大変助かります。
開催日数分のループですが、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(例)」になってしまい拾えないかと・・・。
何分素人でうまく伝えられないですがよろしくお願いします。
コメントありがとうございます。
修正して記事を更新します!
コードありがとうございます。
開催日が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のとき)、開催日数の前にゼロをつける処理は不要のため
修正コードまでありがとうございます。
修正して記事を更新します!
コードありがとうございます。
人気のところで取得される数値が8までしかなくというか実際の人気と異なる数値が出力され、最終着順の馬は空白になっています。
原因は何か分かりますでしょうか?
全てのデータがそのような結果になっているのでしょうか?
特定のレースだけというのであれば、そのレースのページの作りが他と異なる可能性などが考えられます。
抽出した全てのレースで同様の結果になっております。
現象を理解しました。
元々のソースコードが間違っているようなので、修正してみます。
ご指摘ありがとうございます。
記事に追記で、コードを記載したので試してみてください。
横から失礼いたします。
追記いただいたコードなのですが、
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
というエラーが発生してしまいます。
原因などお判りになりますでしょうか…。
1点目に関してはmain_tableが正しく取得できていない可能性があります。
print(main_rows)で期待通りの内容が取得できているか確認してみてください。
2点目に関してはこちらの修正ミスでした。
42行目に「continueCounter」の定義を追加したので、確認してみてください。
初めまして実行をすると135行目(正確には134行?)で以下のエラーが表示されるのですが
line 135, in
con=str(var).split(“/”)[2].split(“:”)[1][1]
~~~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range
こちらはどう修正すればいいのかご教授いただけないでしょうか。
varの中身がどうなっているかを見てみないと回答が難しいですね。
print(var)で中身を見てみてください。
varはcols[14].text.strip()なので、
print(cols)でちゃんと値が入っているのかも見てみてください。
まったく知識のない初心者質問でお恥ずかしいのですが、「実行する前に、「data」フォルダを作成してください」というのはどこにどうフォルダを作成したらよいのでしょうか。
お手数をおかけしますがご教授頂けると幸いです。
以下のようなフォルダ構成のイメージです。
-dataフォルダ
|-2023.csv
-encodedフォルダ
|-encoded_data.csv
encode.py
predict.py
race_table_scraping.py
返信ありがとうございます。
記載していただいたフォルダ構成と、修正済みコードをそのままPython上にコピペして、ターミナルで実行コマンドを打てば完成ということでよろしいでしょうか?
コピペ以外に入力しなければいけないものや、準備が必要なことがあれば教えていただきたいです。
まったく初めてで基礎もよくわかっていませんが、ご迷惑でなければよろしくお願いいたします。
基本的にはご認識の通りです。
あとはコードの先頭でimportしているライブラリがインストールされていなければインストールする必要があります。
実行してみてエラーが出たらエラーメッセージでググるか、「ライブラリ名 インストール」などと調べればコマンドが出てきます。
他に修正するとすれば、ファイル名やフォルダ名などは任意ですので、自分でわかりやすい名称に変更するくらいだと思います。
重ね重ねありがとうございます。
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
-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***>
と出るだけで何も始まりません。
何か根本的に間違えているのでしょうか。
お手数おかけして本当に申し訳ありません。
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’
というエラーが出てしまいます。
調べてもピンとこないのですが、何か解決方法はありますでしょうか。
何度も申し訳ございません。
エラーを見る限り、このあたりのエラーだと思いますが、こちらでは出ないので何とも。。。
https://aiacademy.jp/media/?p=1478
はじめまして!プログラミングというのが何もわからいのですが競馬AIを自分で作ってみたいと思い色々調べたらこのページに辿りついたので今から競馬AIを作成していきたいと思っていますが、最初のVisual Studio CodeをPCに入れてその後このVisual Studio Code内にPythonを入れました
この後ってどのようにしていけばいいのでしょうか?
ライブラリ内の記載してあるbeautifulsoup4とrequestsをインストールしたいのですができずに手が止まってしまっています
PythonはVisual Studio Code内にしかなくコマンドプロンプトでPythonと記載するとインストール画面に行ってしまいます
pipコマンドはVisual Studio CodeのTerminalウィンドウ内で実行してください。
pythonのインストール以外基本的にコマンドプロンプトは使用せず、Visual Studio CodeのTerminalウィンドウを使用します。
ご返信ありがとうございます
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にしてコードを実行していけば良いのですか?無知ですみませんが教えて頂けると幸いです。
追記②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:
とエラーが出ます。
「修正版を以下に記載します。」と書いてありましたのでこちらを実行したのですが、一番上のコードでよかったのでしょうか?
コードを再度修正したので取得しなおしていただけますか?
追記コードの42行目付近にcontinueCounter の定義を追加しました。
ありがとうございました。
エラーなくできました!
修正版の②のコードを実行したのですが出力されるCSVファイルで「通過順」が一部日付表示になっているのは仕様ですか?
excelやスプレッドシートの仕様です。
通過順が1-1-1だと「2001/1/1」と認識されてしまっているだけです。
excelやスプレッドシートで上書きしなければ問題ないです。
スクレイピングのやり方を教えていただきありがとうございます。
追加で厩舎のデータをスクレイピングするにはどのようにすればよいでしょうか。
お手数をおかけしますがご教授頂けると幸いです。
西とか東という部分のことであってますか?
対象のデータは18カラム目にあり、2文字目だけを抽出するには以下のようにすれば出来ると思います。
cols[18].text[2].strip()
お返事ありがとうございます。
cols[6].text.strip(),#騎手の名前
のように厩舎(調教師)のデータを取りたいと考えております。
であれば、[2]を消して以下で取得できると思います。
cols[18].text.strip()
お世話になっております。このコードを地方で使いたい場合は、どのような変更を加えればよいですか?また、ほかの投稿にあるコードも変更が必要になりますか?お手隙の際にお返事お願いいたします。
レースIDの仕組みを解読して、ループの変数を修正する必要があります。
3月9日佐賀競馬1RのIDが以下です。
202455030901
分解すると
2024 55 0309 01
年 競馬場 日付 レース
であることがわかります。
全ての競馬場のIDを整理し、1年間すべての日付でループするように加工します。
スクレイピング出来て、取得されたデータが同じであればある程度他のプログラムも動くと思います。
コードありがとうございます。
このサイトで公開されているコードはしっかり出力させることが出来ました。個人的な要望でなのですが、年ごとだけでなく、日付ごとでデータを抽出したいと思い、コードを書き換えたのですが、どうしてもレースデータが反映されません。そのコードでは出力したスクレイピング先のURLを踏むとおそらく読み込む該当ページではあるもののレース情報が表示されていません。特別コードを触ったわけではないとは思うのですが何かnetkeibaのデータを読み取る上で日付指定にすると問題点があるのでしょうか。幾らか助言頂けると幸いです。
まずはprintでurlが正しいものなのか確認してみてください。
try:
print(url)
r=requests.get(url)
日付指定についてですが、中央競馬のrace_idには日付は使用されていません。
「西暦」「競馬場番号」「開催何回目か」「開催何日目か」「何レースか」で構成されているので、
日付指定の方法が出来るのか私にもわかりません。
url自体間違っておりました。urlの情報がrace_idと日付で混同しておりそれによってデータだけでないような形での不具合が生じておりました。日付ではなく指定したい開催日をrace_idに変換するような形で組み立てようと思います。助言の程助かりました。ありがとうございます。
コメント失礼します。記載されている手順通りに実施したのですが
VS Codeのターミナル上で「python historical_scraping.py」を実行しても
データのスクレイピングがされずコマンドが終了してしまいます。
【事象】
デバッグをすると、「for year in range(year_start, year_end):」以降のコードが実行されていないようでした。
実験として上記文の前にprint文を記載すると、その内容はターミナルに出力されていました。
【環境】
・OS:Windows11
・Python:3.12.2
原因がお分かりでしたらご教示いただけますと幸いです。
エラーメッセージなどは何か出ていましたか?
あとPCの設定でスクレイピングが許可されていないのか。
私のPCも再起動後は「許可する」「許可しない」のダイアログが出たりします。
連絡ありがとうございます。
はっきりとした原因が分からず終いですが、ひとまず解決しました。
【解決策】
質問文に記載した「for year in range(year_start, year_end):」
のループを削除し、単一年度のみで実行させるよう修正
ソースコードありがとうございます。
とても参考になりました!
お時間ある際に追加のアドバイスいただけると嬉しいです。
実験的に競馬場を絞って実行したところ、書き込みのタイミングでエラーがでてきました。札幌競馬場のみ(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
コメントありがとうございます。
こちらは特にエラーになりませんでした。
特定の競馬場の実のデータを取得する場合、16行目のコードを修正しました。
試してみてください。
l=[“01″,”02″,”03″,”04″,”05″,”06″,”07″,”08″,”09″,”10”]
↓
l=[“03”]
はじめまして
元々競馬が好き+機械学習に興味があり始めてみましたが、とても参考になっています。
今回ご意見いただきたくコメントいたしました。
予想の精度を上げるために上記の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
以上
コメントありがとうございます。
私は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()#母父
回答ありがとうございます。
確かに3世代前となるとURLが違いました
コードも記載いただいてありがとうございます。
参考にさせていただきます。
デスクトップにdataフォルダを作り
Visual Studio Codeの
ファイル→新しいファイル→pythonファイル→名前をつけて
→コードをペースト→ターミナルを押すと勝手に実行されます。
でもdataには出力?できてないみたいで
FileNotFoundError: [Errno 2] No such file or directory: ‘data/2019.csv’
と表示をされます。
フォルダの作る場所が違うのでしょうか?
違うと思われます。
以下のようなフォルダ構成になります。
-source/
-data/
|-2023.csv
historical_scraping.py
追記②2023/12/13を拝借して、地方競馬用に加工してダウンロードしましたが、笠松競馬で降雪で中止になった日(2024年1月24日、https://db.netkeiba.com/race/202447012401)があると、結果データが空欄のためかPYTHONがエラーで止まってしまいます。
対処法はありますでしょうか(PYTHON初心者です)
どのようなエラーが出ているかわからないので確実なことは言えませんが、
レース自体が中止になった場合は空欄になる箇所があると思います。(天候、馬場状態、着順やタイムなど)
取得した値がもし空欄であればcontinueもしくはbreakして次のループに進める処理を入れれば大丈夫だと思います。
笠松競馬2024年1月9日~1月23日、1月25日~1月26日、2月5日~7月3日と期間を分けたら問題なくダウンロードできました。
このブログで学ばせていただいております。
初心者のため、質問が初歩的かもしれませんが、各レースの出走頭数をデータに加えたい場合はどのようなコードを使用すれば良いかご教示ください。
75行目にallnumがあるので、それを183行目の最後尾に追加すれば行けると思います。
ヘッダーも追加が必要なので、13行目の修正もお忘れなく。
初心者です。
スクレイピングする項目の中に発走頭数を追加したいのですが、どのようなプログラムを組めばよろしいかご教示いただけますか?
75行目にallnumがあるので、それを183行目の最後尾に追加すれば行けると思います。
ヘッダーも追加が必要なので、13行目の修正もお忘れなく。
初心者で申し訳ございません。
data(ここが出力先)
- 2021.csv(スクレイピングで抽出したデータ)
historical_scraping.py
↑
このhistorical_scraping.pyと言うデータはどういったデータになりますか?
本記事で紹介したプログラムファイルになります。