2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ChatGPTと作る競馬予想AI (3)

Last updated at Posted at 2025-09-15

プログラミング初心者がChatGPTを使って競馬予想AIを作ることで、生成AIとプログラミングについて学んでいく企画の第2回です。

第1回の記事はこちら
第2回の記事はこちら

前回はnetkeiba.comからスクレイピングを行い、「着順」「馬名」「騎手」「単勝オッズ」の4つの特徴量をまとめたテーブルを作成しました。
今回は一定期間の全てのレースの結果を一気にスクレイピングしていきます。

1日分のデータを取得する

前回は1レース分のデータを取得していきましたが、競馬は1日に10レース以上行われるためこれらのデータを入手していきましょう。
それではいつもの調子でChatGPTに聞いていきます。

プロンプト 無事スクレイピングができました。 ただこれでは1レース分のみで1日に行われる全てのレースのデータをスクレイピングできていません。 指定した日に行われるレース数をnetkeiba.comからスクレイピングする方法と、forループで全てのレースの"着順", "馬名", "騎手", "単勝オッズ"を取得する方法、そして取得したデータにレースIDをつけて、一つのデータテーブルとしてまとめる方法を教えてください。

欲張って「何レース行われるのか」や「データテーブルを結合する方法」など聞いてしまいましたが大丈夫でしょうか。
GPT君の出力を確認してみます。

回答 おおー!👏 文字化け解決してスクレイピングできたんですね。次のステップは「1日分の全レースをまとめて取る」ですね。

流れはこうなります:


✅ ステップ

  1. 開催日を指定して、その日のレースID一覧を取得

    • netkeibaの「開催日ページ」にはその日の全レースのリンクがあるので、そこから race_id を拾う。
  2. レースIDを for ループで回して scrape_race() を実行

    • 各レースの DataFrame を取得する。
  3. レースIDを列として追加

    • 後でどのレースのデータか区別できるようにする。
  4. 全レースを結合して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_id01〜10 までループすればOK。
  • さらに複数日を取るなら date を変えてループさせるだけ。

👉 ここで質問ですが:
最初の実験では「1日分 × 1競馬場」で十分でしょうか?
それとも「1年分の全競馬場」を一気に取れる形に拡張したいですか?

期待通り、その日のrace_idを取得するget_race_ids関数と、さらに複数ある競馬場も考慮して全会場のデータを入手できるようにしてくれました。
pandasのconcatでデータをまとめてくれてもいるようです。
極めつけは長期間(1年分)の拡張版も提案してくれています!
なんと優秀な子… :cry:
それではまず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をブラウザに打ち込んで確認していきます。

image.png
URLを打ち込んだものの、一向に開催レース一覧が表示されません。
URLはどこから間違えているのでしょうか?
一つずつ確認していきます。
・"https://race.netkeiba.com/top/race_list.html"まで
image.png
今週のレース情報が表示されました。無事URLは機能しています。
・"https://race.netkeiba.com/top/race_list.html?kaisai_date={YYYYMMDD}"まで
image.png
これも無事URLが機能してます。
・"https://race.netkeiba.com/top/race_list.html?kaisai_date={YYYYMMDD}&kaisai_place={place_id}"まで
image.png
ここから少し様子が変です。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

image.png

一見場所ごとの結果が出力されたように見えますが、札幌のデータ(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)}")

📌 このコードのポイント

  1. カレンダーページから日付リンクを取得
    kaisai_date=YYYYMMDD を抽出。

  2. 各日付ページからレースリンクを抽出
    race_id=202409010611 のようなIDを正規表現で抜き出し。

  3. 月全体のレースIDをまとめて DataFrame 化
    → これを保存すればスクレイピングの元データに使える。


👉 このコードを改良して 1年分まとめて取得 したり、CSVに保存 したりもできます。

次のステップとして「取得した race_id を使って各レース結果をスクレイピングするコード」も作りましょうか?

ここでモジュール以下のコードをコピーして実行しようとしたのですが、正規表現を扱うためのreモジュールがないと勘違いし、もう一度以下の質問を投げかけます。
プロンプト 正規表現をする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を元にレース結果を特徴量としてスクレイピングしていきます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?