プログラミング初心者がChatGPTを使って競馬予想AIを作ることで、生成AIとプログラミングについて学んでいく企画の第2回です。
前回はnetkeiba.comからスクレイピングを行い、「着順」「馬名」「騎手」「単勝オッズ」の4つの特徴量をまとめたテーブルを作成しました。
今回は一定期間の全てのレースの結果を一気にスクレイピングしていきます。
1日分のデータを取得する
前回は1レース分のデータを取得していきましたが、競馬は1日に10レース以上行われるためこれらのデータを入手していきましょう。
それではいつもの調子でChatGPTに聞いていきます。
プロンプト
無事スクレイピングができました。 ただこれでは1レース分のみで1日に行われる全てのレースのデータをスクレイピングできていません。 指定した日に行われるレース数をnetkeiba.comからスクレイピングする方法と、forループで全てのレースの"着順", "馬名", "騎手", "単勝オッズ"を取得する方法、そして取得したデータにレースIDをつけて、一つのデータテーブルとしてまとめる方法を教えてください。欲張って「何レース行われるのか」や「データテーブルを結合する方法」など聞いてしまいましたが大丈夫でしょうか。
GPT君の出力を確認してみます。
回答
おおー!👏 文字化け解決してスクレイピングできたんですね。次のステップは「1日分の全レースをまとめて取る」ですね。流れはこうなります:
✅ ステップ
-
開催日を指定して、その日のレースID一覧を取得
- netkeibaの「開催日ページ」にはその日の全レースのリンクがあるので、そこから
race_idを拾う。
- netkeibaの「開催日ページ」にはその日の全レースのリンクがあるので、そこから
-
レースIDを for ループで回して scrape_race() を実行
- 各レースの DataFrame を取得する。
-
レースIDを列として追加
- 後でどのレースのデータか区別できるようにする。
-
全レースを結合して1つのDataFrameにまとめる
✅ サンプルコード
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
def get_race_ids(date: str, place_id: str):
"""
開催日と開催場からレースID一覧を取得
date: '20210101' のように指定 (YYYYMMDD)
place_id: '01' = 東京, '02' = 中山, '03' = 京都, etc.
"""
url = f"https://race.netkeiba.com/top/race_list.html?kaisai_date={date}&kaisai_place={place_id}"
headers = {"User-Agent": "Mozilla/5.0"}
res = requests.get(url, headers=headers)
res.encoding = res.apparent_encoding
soup = BeautifulSoup(res.text, "html.parser")
# レースIDを含むリンクを探す
links = soup.find_all("a", href=True)
race_ids = []
for link in links:
href = link["href"]
if "race_id" in href:
rid = href.split("race_id=")[-1].split("&")[0]
if rid.isdigit():
race_ids.append(rid)
return sorted(list(set(race_ids)))
def scrape_race(race_id: str):
url = f"https://race.netkeiba.com/race/result.html?race_id={race_id}"
headers = {"User-Agent": "Mozilla/5.0"}
res = requests.get(url, headers=headers)
res.encoding = res.apparent_encoding
soup = BeautifulSoup(res.text, "html.parser")
table = soup.find("table", class_="RaceTable01")
rows = table.find_all("tr")[1:]
data = []
for row in rows:
cols = row.find_all("td")
if len(cols) < 5:
continue
rank = cols[0].get_text(strip=True)
horse = cols[3].get_text(strip=True)
jockey = cols[6].get_text(strip=True)
odds = cols[7].get_text(strip=True)
data.append([race_id, rank, horse, jockey, odds])
df = pd.DataFrame(data, columns=["レースID", "着順", "馬名", "騎手", "単勝オッズ"])
return df
def scrape_day(date: str, place_id: str):
"""
指定日の開催場について全レースの結果を取得
date: '20210101'
place_id: '01'=東京, '02'=中山, ...
"""
race_ids = get_race_ids(date, place_id)
all_data = []
for rid in race_ids:
try:
df = scrape_race(rid)
all_data.append(df)
time.sleep(1) # アクセス間隔を空ける(マナー)
except Exception as e:
print(f"Error scraping {rid}: {e}")
return pd.concat(all_data, ignore_index=True)
# --- 動作確認 ---
date = "20210101" # 2021年1月1日
place_id = "01" # 東京競馬場
df_all = scrape_day(date, place_id)
print(df_all.head())
✅ 出力イメージ
レースID 着順 馬名 騎手 単勝オッズ
0 202101010101 1 サンプルホース ルメール 2.3
1 202101010101 2 テストホース 川田 5.6
2 202101010101 3 データホース 福永 8.9
3 202101010102 1 ランダムホース 武豊 1.9
4 202101010102 2 アナザー馬 戸崎 10.2
🔧 ポイント
- レースIDを一緒に保存 → 学習時に「同じレース内で順位を比較」できる。
- 1日複数の競馬場があるので、全場を取るなら
place_idを01〜10までループすればOK。 - さらに複数日を取るなら
dateを変えてループさせるだけ。
👉 ここで質問ですが:
最初の実験では「1日分 × 1競馬場」で十分でしょうか?
それとも「1年分の全競馬場」を一気に取れる形に拡張したいですか?
期待通り、その日のrace_idを取得するget_race_ids関数と、さらに複数ある競馬場も考慮して全会場のデータを入手できるようにしてくれました。
pandasのconcatでデータをまとめてくれてもいるようです。
極めつけは長期間(1年分)の拡張版も提案してくれています!
なんと優秀な子… ![]()
それではまず1日に行われるレース数(race_id)を取得するget_race_id関数を実装し、出力を確認していきます。
レースIDの取得
以下、ChatGPTが出力してくれた関数とその実行結果です。
def get_race_ids(date: str, place_id: str):
"""
開催日と開催場からレースID一覧を取得
date: '20210101' のように指定 (YYYYMMDD)
place_id: '01' = 東京, '02' = 中山, '03' = 京都, etc.
"""
url = f"https://race.netkeiba.com/top/race_list.html?kaisai_date={date}&kaisai_place={place_id}"
headers = {"User-Agent": "Mozilla/5.0"}
res = requests.get(url, headers=headers)
res.encoding = res.apparent_encoding
soup = BeautifulSoup(res.text, "html.parser")
# レースIDを含むリンクを探す
links = soup.find_all("a", href=True)
race_ids = []
for link in links:
href = link["href"]
if "race_id" in href:
rid = href.split("race_id=")[-1].split("&")[0]
if rid.isdigit():
race_ids.append(rid)
return sorted(list(set(race_ids)))
# --- 動作確認 ---
date = "20210101" # 2021年1月1日
place_id = "01" # 東京競馬場
df_race_id = get_race_ids(date, place_id)
print(df_race_id)
[]
レースIDが出力されることを想定していましたが、空のリストしか返ってきません…
いったい何が原因か、まずはURLをブラウザに打ち込んで確認していきます。

URLを打ち込んだものの、一向に開催レース一覧が表示されません。
URLはどこから間違えているのでしょうか?
一つずつ確認していきます。
・"https://race.netkeiba.com/top/race_list.html"まで

今週のレース情報が表示されました。無事URLは機能しています。
・"https://race.netkeiba.com/top/race_list.html?kaisai_date={YYYYMMDD}"まで

これも無事URLが機能してます。
・"https://race.netkeiba.com/top/race_list.html?kaisai_date={YYYYMMDD}&kaisai_place={place_id}"まで

ここから少し様子が変です。kaisai_placeをURLに追加することで、その馬場のレースのみのページが表示されることを期待したのですが、相変わらずその日一日のレース情報が表示されます。
GPT君にこの原因を聞いていきましょう。
プロンプト文
教えてくれた以下のURLだとレース情報がみれません。何が間違えている? url = f"https://race.netkeiba.com/top/race_list.html?kaisai_date={date}&kaisai_place={place_id}"回答
いい質問です 👍そのURLだと正しくありません。
netkeibaのレース一覧ページのURLは次の形式です:
https://race.netkeiba.com/top/race_list_sub.html?kaisai_date=YYYYMMDD&kaisai_place=XX
つまり、race_list.html ではなく race_list_sub.html を使う必要があります。
修正例
date = "20230903" # 開催日 (YYYYMMDD)
place_id = "06" # 開催場所 (阪神=06など)
url = f"https://race.netkeiba.com/top/race_list_sub.html?kaisai_date={date}&kaisai_place={place_id}"
print(url)
これで例えば 2023年9月3日の阪神競馬 のレース一覧が表示されるはずです。
👉 補足
-
kaisai_placeのIDは場ごとに決まっていて、以下のようになります:- 01: 札幌
- 02: 函館
- 03: 福島
- 04: 新潟
- 05: 東京
- 06: 中山
- 07: 中京
- 08: 京都
- 09: 阪神
- 10: 小倉
質問ですが:
このスクレイピングでは「レース一覧ページからレースIDを取得する」ところまでやりたいですか? それとも「各レース詳細(出馬表や結果)」まで取りにいきたいですか?
回答によるとrace_list.html ではなくrace_list_sub.htmlを使う必要があるそうです。
試しに以下のURLを打ち込んで、場所ごとのレース結果が出力されるか確認してみます。
https://race.netkeiba.com/top/race_list_sub.html?kaisai_date=20240901&kaisai_place=01
一見場所ごとの結果が出力されたように見えますが、札幌のデータ(kaisai_place=01)を確認しているが新潟のデータが表示されている、という現象に気づきます。
しかもkaisai_placeを変えていっても新潟が表示され続けます。
そもそも各kaisai_placeごとにrace_idを取得する必要はなく、一日の開催レース一覧ページからrace_idを取得すればよいはず…
(1日最大3場しか開催されない様ですし、場所毎に分けなくても大したデータ量にならないはず)
そこで、月ごとのレース日程("kaisai_dates")をスクレイプ → kaisai_datesを用いて1日に行われる全てのレースID(race_id)をスクレイピングという流れにしていきます。
kaisai_datesからのレースIDの取得
月ごとのrace_idを取得するようGPT君に聞いてみます。
プロンプト
netkeiba.comのレース日程(calendar)のページから、その月一か月分のレースIDを取得するスクレイピングのPythonコードを教えて回答
なるほど! `netkeiba.com` の **カレンダーページ**(例: `https://race.netkeiba.com/top/calendar.html?year=2024&month=9`)から、その月に行われるレースのリンクをたどれば、1か月分のレースIDをまとめて取得できます。以下のサンプルコードを試してみてください👇
import requests
from bs4 import BeautifulSoup
import re
import pandas as pd
def get_race_ids(year: int, month: int):
"""指定した年・月の全レースIDを取得する"""
url = f"https://race.netkeiba.com/top/calendar.html?year={year}&month={month}"
headers = {
"User-Agent": "Mozilla/5.0"
}
res = requests.get(url, headers=headers)
res.encoding = "EUC-JP" # 文字化け回避
soup = BeautifulSoup(res.text, "html.parser")
race_ids = []
race_dates = []
# 各日付のリンクを取得(例: ?kaisai_date=20240901)
date_links = soup.select("a[href*='kaisai_date=']")
for a in date_links:
href = a.get("href")
m = re.search(r"kaisai_date=(\d+)", href)
if m:
date = m.group(1)
# その日付のレース一覧ページへアクセス
race_list_url = f"https://race.netkeiba.com/top/race_list.html?kaisai_date={date}"
res_list = requests.get(race_list_url, headers=headers)
res_list.encoding = "EUC-JP"
soup_list = BeautifulSoup(res_list.text, "html.parser")
# レースIDリンクを抽出 (例: race_id=202406010611)
race_links = soup_list.select("a[href*='race_id=']")
for ra in race_links:
m2 = re.search(r"race_id=(\d+)", ra.get("href"))
if m2:
race_id = m2.group(1)
race_ids.append(race_id)
race_dates.append(date)
# pandas DataFrameにまとめる
df = pd.DataFrame({"race_id": race_ids, "date": race_dates})
return df.drop_duplicates().reset_index(drop=True)
# 使用例: 2024年9月の全レースID取得
df_race_ids = get_race_ids(2024, 9)
print(df_race_ids.head())
print(f"取得件数: {len(df_race_ids)}")
📌 このコードのポイント
-
カレンダーページから日付リンクを取得
kaisai_date=YYYYMMDDを抽出。 -
各日付ページからレースリンクを抽出
race_id=202409010611のようなIDを正規表現で抜き出し。 -
月全体のレースIDをまとめて DataFrame 化
→ これを保存すればスクレイピングの元データに使える。
👉 このコードを改良して 1年分まとめて取得 したり、CSVに保存 したりもできます。
次のステップとして「取得した race_id を使って各レース結果をスクレイピングするコード」も作りましょうか?
プロンプト
正規表現をするreが定義されてません。モジュールか何か必要?回答
いい気付きです — `re` は **標準ライブラリの正規表現モジュール** なので、使うにはファイル先頭で `import re` が必要です。追加で、月単位スクレイピング用のコードを少し堅牢に直しておきます(`race_list_sub.html` を使う、文字コード対策、重複排除、スリープ、エラーハンドリングなど)。そのままコピペして実行できる修正版は以下です。
import requests
from bs4 import BeautifulSoup
import re # ← ここが必要
import time
import pandas as pd
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
def get_monthly_race_ids(year: int, month: int, sleep: float = 0.5):
"""
指定した年・月の全レースIDを取得して DataFrame を返す。
- year: 2024
- month: 9 (1〜12)
"""
calendar_url = f"https://race.netkeiba.com/top/calendar.html?year={year}&month={month}"
resp = requests.get(calendar_url, headers=HEADERS)
resp.encoding = resp.apparent_encoding or "EUC-JP"
soup = BeautifulSoup(resp.text, "html.parser")
# カレンダーページから kaisai_date=YYYYMMDD を抽出(重複除去)
date_vals = sorted(set(re.findall(r"kaisai_date=(\d{8})", str(soup))))
all_race_ids = []
all_dates = []
for date in date_vals:
try:
# 日付ごとのレース一覧ページ(sub を使うのが確実)
race_list_url = f"https://race.netkeiba.com/top/race_list_sub.html?kaisai_date={date}"
r = requests.get(race_list_url, headers=HEADERS)
r.encoding = r.apparent_encoding or "EUC-JP"
soup_list = BeautifulSoup(r.text, "html.parser")
# ページ内の race_id= を持つリンクから race_id を抽出
ids = set(re.findall(r"race_id=(\d{12})", str(soup_list)))
for rid in ids:
all_race_ids.append(rid)
all_dates.append(date)
except Exception as e:
print(f"[Warning] {date} の取得でエラー: {e}")
time.sleep(sleep) # マナーとして間隔をあける
df = pd.DataFrame({"race_id": all_race_ids, "date": all_dates})
return df.drop_duplicates().reset_index(drop=True)
# 使用例
if __name__ == "__main__":
df = get_monthly_race_ids(2024, 9, sleep=0.8)
print(df.head())
print("取得件数:", len(df))
# CSVに保存する場合:
# df.to_csv("race_ids_202409.csv", index=False, encoding="utf-8")
ポイントまとめ:
-
import reを忘れずに(追加の外部ライブラリは不要。reは標準モジュールです)。 - カレンダー→日付→日付のレース一覧(
race_list_sub.html)→そのページ中のrace_id=を正規表現で拾う流れにしています。 -
resp.apparent_encodingを使って文字コードを合わせています(必要なら"EUC-JP"に固定しても可)。 -
time.sleep()を入れてアクセス頻度を下げるのを忘れずに。
必要ならこの df を受け取って、先に作った scrape_race(race_id) を for ループで回して月次の全レース結果を一つの DataFrame に結合するスクリプトまで用意します。やりますか?
ここでGPT君がスクレイピングの時間間隔をあけるtime sleep機能やエラーハンドリングを追加で入れてくれたのですが、その中に重要な変更が隠れていました。
それがURLのrace_list.htmlからrace_list_sub.htmlへの変更です。
この二つのURLの違いは「JSにより動的にレース一覧アイテムが挿入されている(race_list.html)」か「部分的な情報表示しかしていない(race_list_sub.html)」です。
これにより、ブラウザ読み込みが速い/遅いといったレスポンスの違いが生まれますが、この違いに応じてスクレイピングのモジュールも変えなければいけません。
具体的には、CSSやHTMLで書かれている軽いページはrequestモジュール、JavaScriptなどの動的動作をする重いページはseleniumモジュールといった使い分けが必要です。
したがって、IDを取得するくらいでしたらrace_list_sub.htmlをrequestモジュールでスクレイピングするだけで大丈夫ですが、今後動的なスクレイピングをしたい場合は注意しましょう。
(実際にrace_list.htmlのままrequestモジュールを使ってスクレイピングしようとしても何も返ってきません。)
長くなってしまったため、最後に採用したコードを出力結果を確認して今回は締めようと思います。
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
def get_monthly_race_ids(year: int, month: int, sleep: float = 0.5):
"""
指定した年・月の全レースIDを取得して DataFrame を返す。
- year: 2024
- month: 9 (1〜12)
"""
calendar_url = f"https://race.netkeiba.com/top/calendar.html?year={year}&month={month}"
resp = requests.get(calendar_url, headers=HEADERS)
resp.encoding = resp.apparent_encoding or "EUC-JP"
soup = BeautifulSoup(resp.text, "html.parser")
# カレンダーページから kaisai_date=YYYYMMDD を抽出(重複除去)
date_vals = sorted(set(re.findall(r"kaisai_date=(\d{8})", str(soup))))
all_race_ids = []
all_dates = []
for date in date_vals:
try:
# 日付ごとのレース一覧ページ(sub を使うのが確実)
race_list_url = f"https://race.netkeiba.com/top/race_list_sub.html?kaisai_date={date}"
r = requests.get(race_list_url, headers=HEADERS)
r.encoding = r.apparent_encoding or "EUC-JP"
soup_list = BeautifulSoup(r.text, "html.parser")
# ページ内の race_id= を持つリンクから race_id を抽出
ids = set(re.findall(r"race_id=(\d{12})", str(soup_list)))
for rid in ids:
all_race_ids.append(rid)
all_dates.append(date)
except Exception as e:
print(f"[Warning] {date} の取得でエラー: {e}")
time.sleep(sleep) # マナーとして間隔をあける
df = pd.DataFrame({"date": all_dates, "race_id": all_race_ids})
return df.drop_duplicates().reset_index(drop=True)
# 使用例
if __name__ == "__main__":
df = get_monthly_race_ids(2024, 9, sleep=0.8)
print("取得件数:", len(df))
print(df)
# CSVに保存する場合:
# df.to_csv("race_ids_202409.csv", index=False, encoding="utf-8")
取得件数: 252
date race_id
0 20240901 202407020811
1 20240901 202404030805
2 20240901 202404030802
3 20240901 202401020810
4 20240901 202401020812
.. ... ...
247 20240929 202407030909
248 20240929 202406040904
249 20240929 202406040902
250 20240929 202406040905
251 20240929 202407030904
[252 rows x 2 columns]
今回は一か月分のレースIDを取得するところまで行けました。次回は任意の期間のレースIDを取得するようにコードを修正し、さらにこのレースIDを元にレース結果を特徴量としてスクレイピングしていきます。
