プログラミング初心者がChatGPTを使って競馬予想AIを作ることで、生成AIとプログラミングについて学んでいく企画の第7回です。
前回の記事はこちら
これまでnetkeiba.comから一定期間のrace_idを取得し、その値を元にレース結果のページをまとめたrace_result_tableを作成しました。
このテーブルを特徴量として使えたら良いのですが、出場する各馬がレースでどのような成績を残すか予測するには、やはり馬ごとの過去の成績が必要です。
netkeiba.comにはこの馬ごとの成績がまとまったページが有り、今回はこのページのスクレイピング→horse_result_tableの作成を行っていきましょう。
馬ごとの成績ページのスクレイピング
方針としては以下の通りです。
- race_result_tableからhorse_idを取得
- 全horse_idを1つのcsvにまとめて管理
- horse_idを元にhttps://db.netkeiba.com/horse/{horse_id}/をHTMLごとbinファイルで保存
- binファイルをBeautifulSoupで解析
- horse_result_tableとして保存
これらの手順を一つ一つクリアにしていきましょう。
horse_idからHTMLを取得
始めに前回作成したrace_result_tableからhorse_id列を抜き出し、csvに保存しておきましょう。
df = pd.read_pickle(result_table_path)
horse_ids_df = df["horse_id"].unique()
pd.Series(horse_ids_df).to_csv(
r"C:\Users\yasak\Desktop\mykeibaAI_ver1p0\data\horse_id_list.csv",
index=False,
header=False
)
抜き出したhorse_idを用いてHTMLを取得します。
race_id, race_resultを取得したときと同様、requestモジュールを使います。
# CSVの読み込み
df = pd.read_csv(csv_path, header=None)
horse_ids = df[0].astype(str).tolist()
user_agents = [ # netkeiba.comはアクセス元によってページ構成が変わる→PCブラウザに統一
# Windows Chrome 系
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edg/121.0.2277.83 Safari/537.36",
# macOS Chrome 系
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
]
for horse_id in tqdm(horse_ids, total=len(horse_ids)):
url = f"https://db.netkeiba.com/horse/{horse_id}"
# ファイル保存パス
save_path = os.path.join(save_dir, f"{horse_id}.bin")
if os.path.exists(save_path):
continue # 既にそのhorse_idが取得済みならスキップ
else:
try:
res = requests.get(url, headers={"User-Agent": random.choice(user_agents)}, timeout=10)
res.raise_for_status() # エラーがあれば例外を発生
# HTMLをバイナリで保存
with open(save_path, "wb") as f:
f.write(res.content)
# アクセス間隔を少し空ける(サーバー負荷対策)
time.sleep(random.uniform(0.8, 2.0))
except Exception as e:
print(f"Error fetching {horse_id}: {e}")
HTMLからhorse_result_tableを取得
続いてBeautifulSoupを用いてHTMLを解析します。
ここで抽出するtableですが、https://db.netkeiba.com/horse/ を見ると下部に「競走成績」のtableがあり、このテーブルから「最近のレース成績」や「馬の得意コース」を特徴量として加えていきましょう。
developmentモードで確認すると、このテーブルのタグはdb_h_race_resultsとわかります。

スクレイピングをしてテーブルが抽出できるか確認してみましょう。
with open(bin_path, "rb") as f:
html_text = f.read().decode("EUC-JP", errors="ignore")
soup = BeautifulSoup(html_text, "html.parser")
# --- レース成績表の抽出 ---
result_table = soup.find("table", class_="db_h_race_results")
print(result_table)
None
実行結果はNone…
すなわちテーブルが見つかっていません。
この原因を探っていきましょう。
動的ページと静的ページ
試しにbinファイルの中身を"db_h_race_results"で検索したところヒットしません。
これは正しくHTMLが取得できていないように感じます。
お手上げなのでChatGPTに聞いてみましょう。
↓↓↓ 以下ChatGPTの回答 ↓↓↓
めちゃくちゃ良い質問です。
そして──はい、「同じ netkeiba.com 内でも、一部のページは静的HTMLで取得でき、他は動的に生成される」というケースは実際にあります。
ブラウザやUser-Agentを固定しても、サーバーの仕組みの違いによって挙動が変わるのです。
✅ 結論(先にまとめ)
-
race_resultページは 静的HTML(サーバー側で生成済み)
→requestsでHTMLを取得すれば<table>がそのまま含まれている -
horse(特にhttps://db.netkeiba.com/horse/<horse_id>/)ページは 動的HTML(JSで埋め込み)
→ 初期HTMLには<table>がなく、JSが呼び出して生成する
🔍 実際の構造の違い(重要)
🟩 例:レース結果ページ
https://db.netkeiba.com/race/202309040910/
このページはHTMLを直接開くとすぐに:
<table class="race_table_01 nk_tb_common">
<tr>...</tr>
</table>
という静的な表があります。
つまり requests.get(url) の結果に直接 <table> が含まれている。
🟥 一方で:馬の個別ページ
https://db.netkeiba.com/horse/2011106610/
このページはHTMLを開くと:
<div id="contents_liquid">
<div id="race_result_all">
<script>RaceResult.load_table('/horse/result/2011106610/')</script>
</div>
</div>
のように、テーブル自体は空で、JavaScriptが別URL(/horse/result/...)を読み込む構造になっています。
💡 つまり…
あなたが最初に言っていた:
race_resultのページは取れるのに horse_resultは取れないのはなぜ?
の答えはこうです👇
✅ 「horse_result」はトップページ(/horse/)ではJSで生成されるが、実際のHTMLは別URL(/horse/result/)に存在するから。
🧭 対処法(Selenium不要)
よって、次のように「裏URL」を直接叩けばOKです:
import requests
from bs4 import BeautifulSoup
horse_id = "2011106610"
url = f"https://db.netkeiba.com/horse/result/{horse_id}/"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
}
res = requests.get(url, headers=headers)
res.encoding = "EUC-JP"
soup = BeautifulSoup(res.text, "html.parser")
table = soup.find("table", class_="nk_tb_common")
print(table is not None) # → True になる
これなら Selenium も不要で、完全なHTMLテーブルを静的に取得できます。
🧩 まとめ(重要ポイント)
| ページ種別 | URL例 | JS後生成? | 静的HTML取得可? | 対応策 |
|---|---|---|---|---|
| レース結果 | /race/202309040910/ |
❌ | ✅ | requests.get() |
| 馬トップ | /horse/2011106610/ |
✅ | ❌ |
/horse/result/◯◯◯◯/ に変更 |
| 馬成績(中身) | /horse/result/2011106610/ |
❌ | ✅ |
requests.get() でOK |
希望があればこの /horse/result/<id>/ を自動で取得して pickle にまとめる関数(get_horse_results(horse_id))も作れます。
作りますか?
これまでうまくいったのになぜ今回スクレイピングができないのか、それはページが動的か静的かに起因します。
動的静的という言葉の違いは以下のQiitaが参考になります。
この中の言葉を一部引用しますと、「静的サイトは既に作られているお弁当を注文する」、「動的サイトは注文を受けてシェフが料理を作る」といった違いがあります。
今回HTMLの取得に用いたrequestモジュールは静的なWebサイトの取得しかできません。
race_resultのページは一度レースが行われるとその結果が変わることはないため、静的サイトで作られているようですが、horse_resultページは馬が出走する度にデータベースの更新が入るため、動的ページで作る必要がありそうです。
動的ページを回避したスクレイピング
本来動的ページをスクレイピングするには、ChatGPTの回答通りSeleniumというモジュールを使わなければなりません。
しかしChatGPTの回答によると、どうやら取得したい馬ごとの成績結果のテーブルは別のURLから取得しており、そのURLからHTMLを取得すれば、Seleniumを使った動的サイトのスクレイピングも必要なさそうです。
調教師や馬主といった基本データはrace_result_tableから取得できますし、特徴量として重要とされる血統データはさらに別のURLから取得できそうです。
すなわちこれからは変更後のURLをスクレイピングしていくことにします。
次回、取得したHTMLを用いて特徴量の抽出&前処理を行っていきます。

