モジュール設計
scraper/race_calendar.py
当日のJRAレーススケジュールをnetkeiba.comから取得する。
データクラス
python
@dataclass
class RaceInfo:
race_id: str # "202609010101" (12桁)
venue_code: str # "09" (会場コード)
venue_name: str # "阪神"
race_num: int # 1〜12
post_time: str # "10:05"
race_name: str # "3歳未勝利"主要関数
| 関数 | 引数 | 戻り値 | 説明 |
|---|---|---|---|
fetch_today_races | date_str: str | list[RaceInfo] | 指定日の全JRAレース一覧を取得 |
取得URL
https://race.netkeiba.com/top/race_list_sub.html?kaisai_date=YYYYMMDD会場コード一覧
| コード | 会場 | コード | 会場 |
|---|---|---|---|
| 01 | 札幌 | 06 | 中山 |
| 02 | 函館 | 07 | 中京 |
| 03 | 福島 | 08 | 京都 |
| 04 | 新潟 | 09 | 阪神 |
| 05 | 東京 | 10 | 小倉 |
scraper/odds_fetcher.py
netkeiba.comのJSON APIから単勝オッズを取得する。最重要モジュール。
データクラス
python
@dataclass
class OddsData:
horse_number: int # 馬番
odds_win: float # 単勝オッズ
popularity: int # 人気順位主要関数
| 関数 | 引数 | 戻り値 | 説明 |
|---|---|---|---|
fetch_win_odds | race_id: str | dict[int, OddsData] | 単勝オッズを取得(馬番→OddsData) |
取得URL
https://race.netkeiba.com/api/api_get_jra_odds.html?race_id=XXXX&type=1&action=updateポイント
- JSON APIのため Playwright不要、requestsのみで取得可能
- JSONP形式のレスポンスにも対応(関数名を自動除去)
- 取消馬(オッズ0)は自動スキップ
scraper/horse_info.py
出馬表から馬番→馬名・騎手名マッピングを取得する。
データクラス
python
@dataclass
class HorseEntry:
number: int # 馬番
name: str # 馬名
jockey: str # 騎手名主要関数
| 関数 | 引数 | 戻り値 | 説明 |
|---|---|---|---|
fetch_horse_entries | race_id: str | dict[int, HorseEntry] | 出馬表を取得(キャッシュ付き) |
clear_cache | なし | None | キャッシュクリア |
キャッシュ戦略
race_id単位でメモリキャッシュ- 出馬表は開催中変わらないため、1レース1回の取得で十分
analyzer/snapshot_store.py
オッズスナップショットの保存と取得を管理する。
データクラス
python
@dataclass
class OddsSnapshot:
race_id: str
timestamp: datetime
odds: dict[int, OddsData] # 馬番 → OddsDataSnapshotStore クラス
| メソッド | 説明 |
|---|---|
save(snapshot) | SQLite + in-memory に保存 |
get_latest(race_id) | 直近スナップショット取得 |
get_previous(race_id) | 1つ前のスナップショット取得 |
get_history(race_id, limit) | SQLiteから履歴取得 |
get_snapshot_count(race_id) | スナップショット数を返す |
SQLiteスキーマ
sql
CREATE TABLE snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
race_id TEXT NOT NULL,
horse_number INTEGER NOT NULL,
odds_win REAL,
popularity INTEGER DEFAULT 0,
fetched_at TEXT NOT NULL -- ISO 8601
);analyzer/change_detector.py
前回と今回のスナップショットを比較し、急騰・急落を判定するコアロジック。
データクラス
python
@dataclass
class OddsChange:
horse_number: int
horse_name: str
jockey: str
old_odds: float
new_odds: float
pct_change: float # 正=オッズ上昇, 負=オッズ低下
direction: str # "急騰" / "急落" / ""
direction_arrow: str # "↑" / "↓" / "-"
is_alert: bool # Slack通知対象か主要関数
| 関数 | 説明 |
|---|---|
detect_changes(prev, curr, horses, ...) | 変動検知(全馬分返却、abs降順ソート) |
filter_significant(changes, threshold) | 閾値以上のみフィルタ |
split_by_direction(changes) | 急騰/急落に分離 |
判定ロジック
変動率 = (新オッズ - 旧オッズ) / 旧オッズ × 100
オッズ低下 (変動率 < 0) → 買い集中 → ↑ 急騰 (緑)
オッズ上昇 (変動率 > 0) → 買い減少 → ↓ 急落 (赤)notifier/terminal_display.py
richライブラリを使ったカラー付きターミナルダッシュボード。
主要関数
| 関数 | 説明 |
|---|---|
render_dashboard(...) | 全レースダッシュボードを表示 |
render_race_odds(...) | 1レース分のテーブル生成 |
print_startup_info(...) | 起動情報表示 |
表示例
┌──────────────────────────────────────────────────┐
│ 🏇 競馬オッズ急変動モニター 2026-03-14 15:33:12 │
╰──────────────────────────────────────────────────╯
全レース 急変動ランキング TOP20
┏━━━━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃ レース ┃ 馬番 ┃ 馬名 ┃ 前回 ┃ 今回 ┃ 変動% ┃ 方向 ┃
┡━━━━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ 阪神1R │ 11 │ ○○○ │180.3 │258.4 │ +43.3% │ ↓ 急落 │
│ 阪神1R │ 05 │ △△△ │ 44.2 │ 33.7 │ -23.8% │ ↑ 急騰 │
└─────────┴──────┴────────┴──────┴──────┴────────┴────────┘notifier/slack_notifier.py
Slack Incoming Webhookによるアラート通知。
SlackNotifier クラス
| メソッド | 説明 |
|---|---|
notify(race, changes) | 急変動アラートを送信(クールダウン付き) |
send_test() | テスト通知送信 |
クールダウン機能
- 同一レース・同一馬番への再通知を一定時間(デフォルト5分)ブロック
- Slackスパム防止
メッセージ形式
Slack Block Kit を使用:
🏇 オッズ急変動検知
阪神 1R コーラルS 15:30発走
📈 急騰(支持上昇 = 買い集中)
• 05番 メイショウハリオ: 44.2 → 33.7 (-23.8%)