- はじめに
- 1. システム概要
- 2. システムアーキテクチャ
- 3. モジュール設計
- モジュール設計の特徴
- 4. データベース設計
- 5. ファイル・ディレクトリ構成
- 6. エラーハンドリング設計
- 7. パフォーマンス・負荷対策
- 8. 全コード
はじめに
Netkeiba.comのレース結果をデータベースへ保存するように改修しました。
また、今までは行き当たりばったりにコードを作成していました。今後のことを考えて、コードを作成前に設計し、その内容を設計書に落とし込むことにしました。競馬予測AIの各機能ごとに設計書を作成する予定です。
1. システム概要
1.1 システムの目的と概要
1.1.1 システム開発の背景
従来の競馬レース結果収集システムでは、以下のような運用上の課題が存在していました:
主要な問題点:
- 中断時のデータ消失: netkeiba.comのWeb閲覧制限に掛かった場合、プログラムを強制終了する必要があり、メモリ上に保存されていた取得済みレース結果がすべて消失していました
- 重複処理による非効率: 取得済みレース結果の記録機能がないため、再実行時に同じデータを再取得する必要がありました
- 長時間処理: 例えば2024年1月~12月の期間でレース結果取得を実行した際、中断後の再実行で完了までに長時間を要していました
- サイト負荷: 不要な重複アクセスによりnetkeiba.comに過度な負荷をかけるリスクがありました
1.1.2 改修による解決内容
本システムでは、上記課題を解決するために以下の機能を実装しました:
主要改善点:
- 月単位でのデータ永続化: 一か月単位でCSVファイルを作成し、取得したデータを即座に保存
- 処理状況の管理: 取得実施/未実施を詳細に管理し、処理済みURLを追跡
- スキップ機能: 取得済みレース結果は処理をスキップし、未処理分のみを対象
- データベース保存: レース結果情報をSQLiteデータベースに永続保存
- 再開可能な設計: 任意の時点で中断・再開が可能な堅牢な処理フロー
1.1.3 システムの目的
本システムは、JRA(日本中央競馬会)のレース結果を効率的かつ安全に収集し、分析用データとして整備することを目的とします。具体的には:
- 効率的なデータ収集: 重複処理を排除し、必要最小限のアクセスでデータを取得
- データの永続化: 取得したデータの安全な保存と管理
- 再開可能な処理: 予期しない中断からの確実な復旧
- 負荷軽減: 対象サイトへの適切な負荷分散とアクセス制御
1.2 対象サイト(netkeiba.com)
- 対象URL: https://db.netkeiba.com
- データソース: JRA競馬データベース
- 取得対象: 芝・ダート両方のレース結果
- アクセス方式: Selenium(検索)+ HTTP Request(詳細取得)
1.3 取得データの種類と形式
1.3.1 レース基本情報
- レース名、開催日、開催場所、ラウンド
- 距離、天候、馬場状態
- 年、月(分類用)
1.3.2 出走馬情報
- 馬名、父馬、母馬、性齢
- 斤量、騎手、タイム
- 単勝オッズ、人気順、馬体重
1.3.3 出力形式
- データベース: SQLite形式(race_results.db)
- CSVファイル: 月別・年間統合ファイル(Shift-JIS)
- 管理ファイル: JSON形式(処理状況記録)
1.4 システム構成要素
- Webスクレイピング: Selenium WebDriver + BeautifulSoup
- データ保存: SQLite データベース
- ファイル出力: CSV形式での結果出力
- 進捗管理: 処理状況の追跡と管理
2. システムアーキテクチャ
2.1 全体アーキテクチャ図
2.2 データフロー図
2.3 処理シーケンス図
3. モジュール設計
3.1 get_raceResults.py(メイン実行)
3.1.1 概要
- システム全体の起動・制御を担当するメインモジュール
- コマンドライン引数の解析と全体フロー制御
- 各管理モジュールの初期化と連携
- エラーハンドリングと例外処理
3.1.2 関数構成
本モジュールはクラスベースではなく、関数ベースで構成されています。
主要関数:
関数名 | 引数 | 戻り値 | 説明 |
---|---|---|---|
parse_arguments() |
なし | argparse.Namespace | コマンドライン引数の解析と検証 |
show_race_links_info(config, year, month) |
ConfigManager, int, int | bool | 指定年月のレースリンク情報表示 |
main() |
なし | int | メイン処理の実行とステータスコード返却 |
詳細仕様:
parse_arguments()
: argparseを使用してコマンドライン引数を解析し、year、start_month、end_month、batch_size、wait_minutes、config、show_linksオプションを処理show_race_links_info()
: –show_linksオプション指定時に、保存済みのJSONファイルからレースリンク情報を読み込み表示main()
: 全体的な実行フローを制御し、設定初期化→データベース初期化→スクレイピング実行→エラーハンドリングを順次実行
3.1.3 実行例
python get_raceResults.py --year 2024 --start_month 1 --end_month 12 --batch_size 40
3.2 config_manager.py(設定管理)
3.2.1 概要
- アプリケーション全体の設定を一元管理
- 設定ファイルの読み込みとデフォルト値の提供
- パス管理と自動生成機能
- 期間検証と年月ペア生成
3.2.2 主要クラス
ConfigManager
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
__init__(config_file_path=None) |
コンストラクタ | str(optional) | None | 設定管理の初期化、設定ファイル読み込み |
_load_config_file(config_file_path) |
プライベート | str | None | JSONファイルからの設定読み込み |
_ensure_data_directories() |
プライベート | なし | None | データディレクトリの存在確認と作成 |
get_db_path() |
パブリック | なし | str | データベースファイルの完全パス取得 |
get_csv_dir_for_year(year) |
パブリック | int/str | str | 指定年のCSVディレクトリパス取得 |
get_csv_path(year, month) |
パブリック | int/str, int/str | str | 月別CSVファイルパス取得 |
get_combined_csv_path(year) |
パブリック | int/str | str | 年間統合CSVファイルパス取得 |
validate_scraping_period(year, start_month, end_month) |
パブリック | int, int, int | bool | スクレイピング期間の妥当性検証 |
generate_year_month_pairs(year, start_month, end_month) |
パブリック | int, int, int | list | 期間内の年月ペア生成 |
3.2.3 設定項目
- URL設定: base_url, search_url
- 処理制御: batch_size, wait_minutes
- 待機時間: min_wait_seconds, max_wait_seconds
- ディレクトリ: data_dir
- ブラウザ: use_headless_browser
- ユーザーエージェント: user_agents配列
3.2.4 ディレクトリ構造自動生成
./data/raceresults/
├── race_results.db
├── 2023/
│ ├── 2023_1_raceresults.csv
│ ├── 2023_all_raceresults.csv
│ └── 2023_1_racelinks.json
└── 2024/
3.3 database_manager.py(データベース管理)
3.3.1 概要
- SQLiteデータベースの操作管理
- データの保存・取得・状況管理
- トランザクション制御とエラーハンドリング
- 処理済みURL管理による重複処理防止
3.3.2 主要クラス
DatabaseManager
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
__init__(db_path) |
コンストラクタ | str | None | データベース管理の初期化とテーブル作成 |
_ensure_db_directory() |
プライベート | なし | None | データベースディレクトリの確保 |
_get_connection() |
プライベート | なし | contextmanager | DB接続のコンテキストマネージャ |
_initialize_database() |
プライベート | なし | None | 必要なテーブル構造の作成 |
save_race_result(url, race_data, year, month) |
パブリック | str, DataFrame/Series, int, int/str | bool | レース結果のデータベース保存 |
_convert_horse_numeric_data(horse) |
プライベート | Series | tuple | 馬データの数値項目安全変換 |
record_failed_url(url) |
パブリック | str | None | 失敗したURLの記録 |
get_unprocessed_urls(url_list) |
パブリック | list | list | 未処理URLの抽出 |
get_processed_urls_status(url_list) |
パブリック | list | tuple | 処理済みURLの状態取得 |
get_monthly_race_results(year, month) |
パブリック | int, int/str | DataFrame | 指定月のレース結果取得 |
3.3.3 データベーススキーマ
races(レース基本情報)
CREATE TABLE races (
race_id INTEGER PRIMARY KEY AUTOINCREMENT,
race_date TEXT, race_name TEXT, location TEXT,
round TEXT, weather TEXT, ground TEXT, condition TEXT,
distance INTEGER, year INTEGER, month INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
race_horses(出走馬情報)
CREATE TABLE race_horses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
race_id INTEGER, horse_name TEXT, father TEXT, mother TEXT,
age TEXT, rider_weight REAL, rider TEXT, odds REAL,
popular INTEGER, horse_weight TEXT, time TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (race_id) REFERENCES races(race_id)
);
processed_urls(処理状況管理)
CREATE TABLE processed_urls (
url TEXT PRIMARY KEY, race_id INTEGER, status TEXT,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (race_id) REFERENCES races(race_id)
);
3.4 scraper_manager.py(統括管理)
3.4.1 概要
- スクレイピング処理全体の制御と進捗管理
- バッチ処理による負荷分散
- データ統合と出力管理
- 実行時間予測と状況表示
3.4.2 主要クラス
ScraperManager
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
__init__(config_manager, db_manager) |
コンストラクタ | ConfigManager, DatabaseManager | None | 統括管理の初期化、Web管理インスタンス作成 |
scrape_race_details(year, month) |
パブリック | int/str, int/str | tuple | 指定年月のレース詳細取得 |
_fetch_race_details(link_list, year, month) |
プライベート | list, int/str, int/str | list | バッチ処理でのレース詳細情報取得 |
_save_race_links_info(year, month, all_links, unprocessed_links) |
プライベート | int/str, int/str, list, list | None | レースリンク情報のJSON保存 |
_scrape_single_race(url) |
プライベート | str | DataFrame/str | 単一レースの詳細情報スクレイピング |
_extract_race_info(soup) |
プライベート | BeautifulSoup | dict | レース基本情報の抽出と辞書化 |
_get_horse_pedigree(soup) |
プライベート | BeautifulSoup | tuple | 出走馬の血統情報取得 |
_save_to_csv(year, month) |
プライベート | int/str, int/str | None | レース結果のCSV保存 |
print_race_links_summary(year, month, all_links, unprocessed_links) |
パブリック | int/str, int/str, list, list | None | レースリンク情報のサマリー表示 |
combine_race_result(year) |
パブリック | int | DataFrame | 年間レース結果の統合とCSV出力 |
run_scraping(year, start_month, end_month) |
パブリック | int, int, int | None | スクレイピング処理の全体実行 |
_predict_remaining_time(current_index, total_items, item_process_time) |
プライベート | int, int, float | None | 残り処理時間の予測と表示 |
_show_runtime_info() |
プライベート | なし | None | 実行時間情報の表示 |
3.4.3 処理フロー制御
- 月別処理ループ: 指定期間の各月を順次処理
- バッチ処理: 40URL単位での負荷分散処理
- 待機時間管理: サーバー負荷軽減のための適切な間隔制御
- 進捗表示: リアルタイム進捗と予想終了時刻表示
3.5 web_manager.py(Webアクセス管理)
3.5.1 概要
- Seleniumによるブラウザ自動操作
- HTTP通信とHTMLコンテンツ取得
- サイト負荷軽減とアクセス制御
- エラーハンドリングとリトライ機能
3.5.2 主要クラス
WebDriverHandler(ブラウザ操作)
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
__init__(config_manager) |
コンストラクタ | ConfigManager | None | ブラウザ操作の初期化 |
initialize_browser() |
パブリック | なし | webdriver.Chrome/None | Chromeブラウザの初期化と設定 |
wait_for_random_time(min_seconds=None, max_seconds=None) |
パブリック | int(optional), int(optional) | None | 指定範囲でのランダム待機 |
scroll_webpage(pixel) |
パブリック | int | None | Webページの縦方向スクロール |
search_race(year, month) |
パブリック | int/str, int/str | None | 指定年月のレース検索実行 |
_set_search_period(year, month) |
プライベート | str, str | None | 検索フォームの期間設定 |
_set_display_options(num) |
プライベート | str | None | 検索結果の表示件数設定 |
_submit_search() |
プライベート | なし | None | 検索フォームの送信実行 |
collect_race_links() |
パブリック | なし | list | 検索結果からレースリンク収集 |
_extract_links_from_page(link_list) |
プライベート | list | list | 現在ページからのリンク抽出 |
_get_absolute_url(relative_url) |
プライベート | str | str | 相対URLの絶対URL変換 |
_click_next_btn(count_click_next) |
プライベート | int | tuple | 次ページボタンのクリック処理 |
close_browser() |
パブリック | なし | None | ブラウザの終了とリソース解放 |
RequestManager(HTTP通信)
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
__init__(config_manager) |
コンストラクタ | ConfigManager | None | HTTP通信管理の初期化 |
get_html(url, max_retries=5, retry_delay=5) |
パブリック | str, int, int | BeautifulSoup/None | 指定URLからHTMLコンテンツ取得 |
retry_request(url, max_retries=3, retry_delay=1) |
パブリック | str, int, int | requests.Response/None | リトライ機能付きHTTPリクエスト |
3.5.3 負荷軽減機能
- ランダム待機: 2-5秒のランダムな待機時間
- User-Agentローテーション: 複数エージェントの循環使用
- 同時実行制御: FileLockによる重複実行防止
- 自然なブラウジング: スクロールやクリック操作の模倣
3.6 content_parsers.py(HTML解析)
3.6.1 概要
- BeautifulSoupを使用したDOM解析
- レース情報の構造化データ変換
- 血統情報の抽出と整理
- エラーハンドリングによる堅牢性確保
3.6.2 主要クラス
RaceInfoParser(レース基本情報解析)
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
get_race_name(soup) |
静的 | BeautifulSoup | str | レース名の抽出(h1タグから) |
get_race_round(soup) |
静的 | BeautifulSoup | str | ラウンド情報の抽出(東京11Rなど) |
get_race_date(soup) |
静的 | BeautifulSoup | str | 開催日の抽出(2023年12月24日形式) |
get_race_place(soup) |
静的 | BeautifulSoup | str | 開催場所の抽出(東京、中山など) |
get_race_distance(soup) |
静的 | BeautifulSoup | int | レース距離の抽出(メートル単位) |
get_race_weather(soup) |
静的 | BeautifulSoup | str | 天候情報の抽出(晴、曇、雨など) |
get_race_condition(soup) |
静的 | BeautifulSoup | tuple | 馬場状態の抽出(種類、状態のペア) |
RaceResultsParser(レース結果解析)
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
parse_race_results(soup) |
静的 | BeautifulSoup | DataFrame | レース結果テーブルの解析とDataFrame化 |
_extract_row_data(cols) |
静的 | list | dict | テーブル行からのデータ抽出 |
format_race_results(race_results_df, race_info, horse_pedigree) |
静的 | DataFrame, dict, tuple | DataFrame | 結果データの整形と情報統合 |
HorsePedigreeParser(血統情報解析)
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
get_horse_links(race_table_data, base_url) |
静的 | BeautifulSoup, str | list | 出走馬詳細ページリンクの抽出 |
get_horse_father_name(soup) |
静的 | BeautifulSoup | str | 血統テーブルから父馬名を抽出 |
get_horse_mother_name(soup) |
静的 | BeautifulSoup | str | 血統テーブルから母馬名を抽出 |
3.6.3 解析対象データ
- レース基本情報: 名前、日付、場所、距離、天候、馬場状態
- レース結果: 着順、馬名、騎手、タイム、オッズ、人気、馬体重
- 血統情報: 父馬、母馬の名前
3.7 prepare_raceResults.py(データ前処理)
3.7.1 概要
- 機械学習向けデータ前処理
- データクリーニングと型変換
- 特徴量エンジニアリング
- 分析用データの整形と保存
3.7.2 主要クラス
RaceDataProcessor
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
---|---|---|---|---|
__init__(base_folder_path, output_folder_path) |
コンストラクタ | str, str | None | 入出力パスの設定とカラム名定義 |
load_data() |
パブリック | なし | list | 年別フォルダからCSVファイル読み込み |
transform_data(race_results) |
静的 | DataFrame | DataFrame | タイム列の秒単位変換(分:秒→秒数) |
clean_data(race_result) |
静的 | DataFrame | DataFrame | データクリーニングと型変換 |
save_data(race_result) |
パブリック | DataFrame | None | 前処理済みデータのCSV保存 |
process() |
パブリック | なし | None | 前処理の全体実行フロー |
3.7.3 前処理内容
- 欠損値処理: NaN値を含む行の削除
- データ型変換:
- タイム: “1:45.3” → 105.3秒
- 馬体重: “480(+4)” → 480.0
- 日付: “2023年12月24日” → datetime型
- 無効データ除去: “計不”、”—“などの除去
- 数値化: 文字列データの適切な数値変換
3.7.4 出力データ形式
columns = ['time', 'horse', 'father', 'mother', 'age', 'rider_weight',
'rider', 'odds', 'popular', 'horse_weight', 'distance',
'weather', 'ground', 'condition', 'date', 'race_name',
'location', 'round']
モジュール設計の特徴
設計原則
単一責任の原則: 各クラスが明確な役割を持つ
開放閉鎖の原則: 拡張に開放、修正に閉鎖
依存性逆転: 抽象に依存、具象に依存しない
インターフェース分離: 必要な機能のみを公開
エラーハンドリング戦略
- 各メソッドレベル: try-except による例外キャッチ
- デフォルト値: エラー時の安全な初期値設定
- ログ出力: エラー内容の詳細記録
- 継続性: 個別失敗による全体停止の回避
パフォーマンス考慮
- 静的メソッド活用: 状態を持たない処理の効率化
- コンテキスト管理: リソースの確実な解放
- バッチ処理: 負荷分散による効率的処理
- キャッシュ活用: 重複処理の最小化
4. データベース設計
4.1 ER図
4.2 テーブル定義
4.2.1 races(レース基本情報)
カラム名 | 型 | 制約 | 説明 |
---|---|---|---|
race_id | INTEGER | PRIMARY KEY AUTOINCREMENT | レースID(主キー) |
race_date | TEXT | 開催日 | |
race_name | TEXT | レース名 | |
location | TEXT | 開催場所 | |
round | TEXT | ラウンド情報 | |
weather | TEXT | 天候 | |
ground | TEXT | 馬場種類(芝/ダート) | |
condition | TEXT | 馬場状態 | |
distance | INTEGER | 距離(メートル) | |
year | INTEGER | 開催年 | |
month | INTEGER | 開催月 | |
created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 作成日時 |
4.2.2 race_horses(出走馬情報)
カラム名 | 型 | 制約 | 説明 |
---|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT | ID(主キー) |
race_id | INTEGER | FOREIGN KEY | レースID |
horse_name | TEXT | 馬名 | |
father | TEXT | 父馬名 | |
mother | TEXT | 母馬名 | |
age | TEXT | 性齢 | |
rider_weight | REAL | 斤量 | |
rider | TEXT | 騎手名 | |
odds | REAL | 単勝オッズ | |
popular | INTEGER | 人気順位 | |
horse_weight | TEXT | 馬体重 | |
time | TEXT | タイム | |
created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 作成日時 |
4.2.3 processed_urls(処理状況管理)
カラム名 | 型 | 制約 | 説明 |
---|---|---|---|
url | TEXT | PRIMARY KEY | URL(主キー) |
race_id | INTEGER | FOREIGN KEY | 関連レースID |
status | TEXT | 処理状態(success/failed) | |
processed_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 処理日時 |
4.3 インデックス設計
-- 年月での検索用
CREATE INDEX idx_races_year_month ON races(year, month);
-- レース日付での検索用
CREATE INDEX idx_races_date ON races(race_date);
-- 処理状況での検索用
CREATE INDEX idx_processed_urls_status ON processed_urls(status);
4.4 データ整合性制約
- 外部キー制約: race_horses.race_id → races.race_id
- 外部キー制約: processed_urls.race_id → races.race_id
- NOT NULL制約: 主要なデータ項目に設定
- チェック制約: status列は’success’または’failed’のみ
5. ファイル・ディレクトリ構成
5.1 プロジェクト構造
horse_racing_scraper/
├── get_raceResults.py # メイン実行ファイル
├── config_manager.py # 設定管理
├── web_manager.py # Web操作管理
├── content_parsers.py # HTML解析
├── database_manager.py # データベース管理
├── scraper_manager.py # 統括管理
└── prepare_raceResults.py # データ前処理
data/ # データディレクトリ
└── raceresults/
├── race_results.db # SQLiteデータベース
├── 2023/ # 年別フォルダ
│ ├── 2023_1_raceresults.csv
│ ├── 2023_2_raceresults.csv
│ ├── ...
│ ├── 2023_all_raceresults.csv
│ ├── 2023_1_racelinks.json
│ └── ...
├── 2024/ # 年別フォルダ
└── prepared_raceresults/ # 前処理済みデータ
└── 2019_2023_prepared_raceresults.csv
5.2 データファイル構成
5.2.1 データベースファイル
- パス:
./data/raceresults/race_results.db
- 形式: SQLite3データベース
- エンコーディング: UTF-8
5.2.2 CSVファイル
- 月別:
./data/raceresults/[年]/[年]_[月]_raceresults.csv
- 年間統合:
./data/raceresults/[年]/[年]_all_raceresults.csv
- 前処理済み:
./data/raceresults/prepared_raceresults/[開始年]_[終了年]_prepared_raceresults.csv
- エンコーディング: Shift-JIS (cp932)
5.2.3 管理ファイル
- レースリンク情報:
./data/raceresults/[年]/[年]_[月]_racelinks.json
- エンコーディング: UTF-8
5.3 ログファイル構成
- 標準出力: コンソールへの進捗情報出力
- エラーログ: 標準エラー出力への例外情報
- 処理状況: データベース内の処理履歴
5.4 設定ファイル構成
- デフォルト設定: config_manager.py内のDEFAULT_CONFIG
- 外部設定ファイル: config.json(オプション)
- コマンドライン引数: 実行時パラメータ
6. エラーハンドリング設計
6.1 例外処理方針
6.1.1 基本方針
- 継続性重視: 個別の失敗で全体処理を停止しない
- 状態保存: 処理状況を確実に記録
- 自動復旧: 可能な限り自動的に回復
- 詳細ログ: エラー原因の特定可能な情報を記録
6.1.2 例外レベル
- 致命的エラー: システム全体停止
- 設定ファイル読み込み失敗
- データベース初期化失敗
- 必要ライブラリ不足
- 重要エラー: 処理継続、影響範囲限定
- ブラウザ起動失敗
- ネットワーク接続失敗
- HTMLパース失敗
- 軽微エラー: ログ記録、処理継続
- 個別URL取得失敗
- 一部データ欠損
- ファイル書き込み警告
6.2 リトライ機能
6.2.1 HTTP通信リトライ
def retry_request(url, max_retries=3, retry_delay=1):
# 最大3回まで異なるUser-Agentでリトライ
# 指数バックオフによる待機時間調整
6.2.2 ブラウザ操作リトライ
- 要素取得失敗時の再試行
- ページ読み込み完了待機
- JavaScript実行結果待機
6.2.3 データベース操作リトライ
- ロック待ちでのリトライ
- トランザクション競合時の再実行
6.3 失敗URL管理
6.3.1 失敗記録
def record_failed_url(url):
# processed_urlsテーブルにstatus='failed'で記録
# 再実行時は自動的にスキップ
6.3.2 再試行制御
- 失敗URLの自動スキップ
- 手動での再試行指定
- 失敗原因の分析機能
6.4 ログ出力仕様
6.4.1 出力レベル
- INFO: 正常な処理進捗
- WARNING: 注意が必要な状況
- ERROR: エラー発生時の詳細情報
6.4.2 出力内容
- タイムスタンプ
- 処理モジュール名
- エラー種別とメッセージ
- スタックトレース(ERROR時)
7. パフォーマンス・負荷対策
7.1 バッチ処理設計
7.1.1 バッチサイズ制御
- デフォルト: 40URL/バッチ
- 設定可能: config.jsonまたはコマンドライン引数
- 動的調整: エラー率に応じた自動調整
7.1.2 バッチ間待機
- デフォルト: 10分間の待機
- 目的: サイト負荷軽減
- 予告表示: 次回開始時刻の表示
7.2 待機時間制御
7.2.1 ランダム待機
- 範囲: 2-5秒(設定可能)
- 目的: 自然なアクセスパターン模倣
- 適用箇所: リクエスト間、ブラウザ操作間
7.2.2 固定待機
- ページ読み込み: 要素出現まで待機
- JavaScript実行: 処理完了まで待機
- エラー後: 段階的に待機時間延長
7.3 メモリ使用量管理
7.3.1 データ処理
- 即座保存: 取得データの即座データベース保存
- メモリ解放: 処理完了後の変数クリア
- ガベージコレクション: 明示的なメモリ回収
7.3.2 ブラウザリソース
- 適切なクローズ: 処理完了後の確実な終了
- リソース監視: メモリ使用量の定期確認
7.4 サイト負荷軽減策
7.4.1 アクセスパターン
- User-Agent: 複数エージェントのローテーション
- セッション管理: 適切なセッション維持
- 同時接続制御: 単一プロセスでの実行
7.4.2 アクセス頻度制御
- 段階的アクセス: 検索→リンク収集→詳細取得
- 重複排除: 処理済みURLの確実なスキップ
- 優先度制御: 失敗URLの後回し処理
8. 全コード
8.1 get_raceResults.py (メイン実行ファイル)
# ==============================================================================
# 競馬レース結果スクレイピングプログラム
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果をネット競馬から自動的に取得し、構造化されたデータとして保存するスクレイピングプログラム。
# 指定された期間のレース結果を効率的に収集し、後続の分析に使用可能な形式で整理します。
#
# プログラムの主な機能:
# 1. レース結果の自動収集
# - 指定した年月のレース結果ページへのアクセス
# - 進捗表示と残り時間の予測
# 2. データの構造化と保存
# - レース基本情報と出走馬情報の抽出
# - SQLiteデータベースへの格納
# - 月別・年間CSVファイルの作成
# 3. 状態管理と再開機能
# - 処理済みURLと未処理URLの管理
# - 失敗したスクレイピングの記録と再開
# - JSONファイルによる処理状況の保存
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
# pip install pandas numpy requests beautifulsoup4 selenium
# pip install tqdm filelock webdriver_manager
#
# 2. 設定値の確認:
# - データ保存先ディレクトリ: ./data/raceresults
# - 対象ウェブサイト: https://db.netkeiba.com
# - バッチサイズ: 40(一度に処理するURL数)
# - 待機時間: 10分(バッチ間の待機時間)
#
# 3. 実行方法:
# python get_raceResults.py [--year YEAR] [--start_month START] [--end_month END]
# [--batch_size BATCH] [--wait_minutes WAIT]
# [--config CONFIG] [--show_links]
#
# 4. 処理の流れ:
# 1) コマンドライン引数の解析と設定の初期化
# 2) 指定された年月の範囲でスクレイピングを実行
# 3) レース検索ページからレースリンクを収集
# 4) 各レースページから詳細情報を抽出
# 5) データベースに保存しCSVファイルに出力
# 6) 月別データを年間データとして統合
#
# ==============================================================================
# 出力データ
# ==============================================================================
# 1. データベースファイル:
# - パス: ./data/raceresults/race_results.db
# - テーブル:
# ├─ races: レース基本情報(レースID、開催日、レース名、開催場所、ラウンド、天候、馬場、状態、距離、年、月)
# ├─ race_horses: 出走馬情報(ID、レースID、馬名、父親、母親、性齢、斤量、騎手、オッズ、人気、馬体重、タイム)
# └─ processed_urls: 処理済みURL管理(URL、レースID、状態、処理日時)
#
# 2. CSVファイル:
# - 月別ファイル: ./data/raceresults/[年]/[年]_[月]_raceresults.csv
# 例: ./data/raceresults/2024/2024_1_raceresults.csv
# - 年間統合ファイル: ./data/raceresults/[年]/[年]_all_raceresults.csv
# 例: ./data/raceresults/2024/2024_all_raceresults.csv
# - カラム構成:
# タイム, 馬名, 父親, 母親, 性齢, 斤量, 騎手, 単勝, 人気, 馬体重,
# 距離, 天候, 馬場, 状態, 開催日, レース名, 開催場所, ラウンド
#
# 3. 進捗・管理ファイル:
# - レースリンク情報: ./data/raceresults/[年]/[年]_[月]_racelinks.json
# 内容: スクレイピング日時、取得レースリンク、未処理リンク一覧
# - ディレクトリ構造: ./data/raceresults/[年]/
# 各年月のデータを年別フォルダで管理
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 依存モジュール:
# - config_manager.py
# └─ 設定管理・パス生成・期間検証を担当
# ├─ 設定ファイルの読み込み・デフォルト値の提供
# ├─ 年月別ディレクトリ・ファイルパスの生成
# └─ スクレイピング期間の検証(未来日付のチェックなど)
#
# - web_manager.py
# └─ ウェブアクセス処理を担当
# ├─ Seleniumを使用したブラウザ操作
# └─ HTTP通信とHTMLの取得
#
# - content_parsers.py
# └─ HTML解析処理を担当
# ├─ レース基本情報の抽出
# ├─ レース結果テーブルの解析
# └─ 血統情報の抽出
#
# - database_manager.py
# └─ データベース操作を担当
# ├─ データベーステーブルの初期化
# ├─ レース結果の保存と取得
# └─ 処理済みURLの管理
#
# - scraper_manager.py
# └─ スクレイピング処理全体の制御を担当
# ├─ レース詳細ページの処理
# ├─ 進捗管理と待機時間の制御
# └─ CSV出力と年間データ統合
#
# 2. 後続プログラム:
# - prepare_raceResults.py
# └─ 収集したレース結果データの前処理
# ├─ 欠損値の処理
# ├─ タイム(分:秒)の秒数変換
# └─ 分析用データの整形と保存
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. 実行環境:
# - Python 3.6以上
# - ChromeDriver(自動インストール対応)
# - インターネット接続(安定した接続推奨)
#
# 2. 実行時の注意:
# - 長時間の実行が予想されるため、電源管理に注意
# - ネットワーク接続が安定していることを確認
# - サーバー負荷軽減のため適切な待機時間を設定
# - 処理に失敗したURLは自動的に記録され再実行時にスキップ
#
# 3. データ要件:
# - 通常一年分の処理で約10-15MBのデータベースファイルが生成
# - CSVファイルはShift-JIS(cp932)エンコーディングで保存
# - 合計で年間あたり20-30MB程度のディスク容量が必要
#
# 4. 再実行時のオプション:
# - --show_links オプションで既存のレースリンク情報のみを表示
# - 実行済み期間の再スクレイピングは未処理のURLのみ対象
#
import argparse
import sys
import traceback
from config_manager import ConfigManager
from database_manager import DatabaseManager
from scraper_manager import ScraperManager
def parse_arguments():
"""
コマンドライン引数の解析
:return: 解析されたコマンドライン引数のオブジェクト
:rtype: argparse.Namespace
"""
parser = argparse.ArgumentParser(description='JRAレース結果スクレイピングツール')
parser.add_argument('--year', type=int, help='取得したい年(例: 2023)')
parser.add_argument('--start_month', type=int, help='開始月(例: 1)')
parser.add_argument('--end_month', type=int, help='終了月(例: 12)')
parser.add_argument('--batch_size', type=int, help='一度に処理するURL数(例: 40)')
parser.add_argument('--wait_minutes', type=int, help='バッチ処理間の待機時間(分)(例: 10)')
parser.add_argument('--config', type=str, help='設定ファイルのパス(例: config.json)')
parser.add_argument('--show_links', action='store_true',
help='既存のレースリンク情報のみを表示(スクレイピングは実行しない)')
return parser.parse_args()
def show_race_links_info(config, year, month):
"""
指定された年月のレースリンク情報を表示する
保存済みのJSONファイルから情報を読み込み、画面に表示する。
:param ConfigManager config: 設定管理オブジェクト
:param int year: 対象年
:param int month: 対象月
:return: 表示が成功したかどうか
:rtype: bool
"""
import os
import json
# JSONファイルパスの設定
year_dir = config.get_csv_dir_for_year(year)
json_file_path = os.path.join(year_dir, f"{year}_{month}_racelinks.json")
if not os.path.exists(json_file_path):
print(f"{year}年{month}月のレースリンク情報が見つかりません")
return False
try:
# JSONファイルの読み込み
with open(json_file_path, 'r', encoding='utf-8') as f:
links_info = json.load(f)
# 情報表示
print(f"\n{year}年{month}月のレースリンク情報")
print(f"スクレイピング日時: {links_info.get('scraping_date', '不明')}")
print(f"取得したレースリンク数: {links_info.get('total_links', 0)}")
print(f"未処理のレースリンク数: {links_info.get('unprocessed_links_count', 0)}")
all_links = links_info.get('all_links', [])
unprocessed_links = links_info.get('unprocessed_links', [])
print("\n取得したレースリンク一覧:")
for idx, link in enumerate(all_links, 1):
print(f"{idx}. {link}")
if unprocessed_links:
print("\n未処理のレースリンク一覧:")
for idx, link in enumerate(unprocessed_links, 1):
print(f"{idx}. {link}")
else:
print("\n未処理のレースリンクはありません。すべてのリンクが処理されました。")
return True
except Exception as e:
print(f"リンク情報表示エラー: {e}")
return False
def main():
"""
メイン処理関数
コマンドライン引数の解析、設定の初期化、スクレイピングの実行を行う。
:return: 処理結果のステータスコード(0: 成功、1: 失敗)
:rtype: int
"""
try:
# コマンドライン引数の解析
args = parse_arguments()
# 設定管理の初期化
config = ConfigManager(args.config)
# コマンドライン引数で上書き
if args.batch_size:
config.config["batch_size"] = args.batch_size
if args.wait_minutes:
config.config["wait_minutes"] = args.wait_minutes
# デフォルト値の設定
year = args.year or 2025
start_month = args.start_month or 1
end_month = args.end_month or 12
# レースリンク情報のみ表示モード
if args.show_links:
# 年月ペアを生成
year_month_pairs = config.generate_year_month_pairs(year, start_month, end_month)
for year, month in year_month_pairs:
show_race_links_info(config, year, month)
return 0
# 値の検証
try:
config.validate_scraping_period(year, start_month, end_month)
except ValueError as e:
print(f"エラー: {e}")
return 1
# データベース管理の初期化
db_manager = DatabaseManager(config.get_db_path())
# スクレイピングマネージャーの初期化と実行
scraper = ScraperManager(config, db_manager)
scraper.run_scraping(year, start_month, end_month)
return 0
except Exception as e:
print(f"実行中にエラーが発生しました: {e}")
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())
8.2 config_manager.py (設定管理)
# ==============================================================================
# 競馬レース結果スクレイピング設定管理モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果をスクレイピングするアプリケーション全体の設定を一元管理するモジュール。
# 設定ファイルの読み込み、デフォルト値の提供、パス管理、入力値の検証など、スクレイピングの基盤となる設定管理機能を提供します。
#
# プログラムの主な機能:
# 1. 設定の読み込みと管理
# - JSONファイルからの設定の読み込み
# - デフォルト設定値の提供
# - コマンドライン引数による設定の上書き
# - 設定値の検証と整合性の確保
# 2. パス管理と自動生成
# - データベースファイルパスの生成
# - 年別・月別CSVファイルパスの生成
# - 必要なディレクトリ構造の自動作成
# 3. 入力検証と期間管理
# - 指定された年月が有効かの検証
# - 未来日付や無効な期間範囲のチェック
# - 年月ペアの生成とバリデーション
# 4. ユーザーエージェント管理
# - 複数のユーザーエージェントを提供
# - 自然なアクセスパターンの維持
#
# ==============================================================================
# 設定ファイル形式
# ==============================================================================
# 1. JSONフォーマットでの設定例:
# {
# "base_url": "https://db.netkeiba.com",
# "search_url": "https://db.netkeiba.com/?pid=race_search_detail",
# "batch_size": 40,
# "wait_minutes": 10,
# "min_wait_seconds": 2,
# "max_wait_seconds": 5,
# "data_dir": "./data/raceresults",
# "use_headless_browser": false,
# "user_agents": [
# "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
# "..."
# ]
# }
#
# 2. 主要設定項目の説明:
# - base_url: スクレイピング対象サイトのベースURL
# - search_url: レース検索ページのURL
# - batch_size: 一度に処理するURL数(サーバー負荷調整用)
# - wait_minutes: バッチ処理間の待機時間(分)
# - min_wait_seconds/max_wait_seconds: リクエスト間のランダム待機時間範囲
# - data_dir: データの保存先ディレクトリ
# - use_headless_browser: ヘッドレスブラウザを使用するかのフラグ
# - user_agents: 使用するユーザーエージェントのリスト
#
# ==============================================================================
# 出力データとディレクトリ
# ==============================================================================
# 1. ディレクトリ構造:
# - ベースディレクトリ: ./data/raceresults/ (デフォルト)
# ├─ データベースファイル: race_results.db
# └─ 年別フォルダ: ./[年]/
# ├─ 月別CSVファイル: [年]_[月]_raceresults.csv
# ├─ 年間統合CSVファイル: [年]_all_raceresults.csv
# └─ レースリンク情報: [年]_[月]_racelinks.json
#
# 2. 自動生成されるパス:
# - get_db_path(): データベースファイルの完全パス
# - get_csv_dir_for_year(): 年別CSVディレクトリの完全パス
# - get_csv_path(): 月別CSVファイルの完全パス
# - get_combined_csv_path(): 年間統合CSVファイルの完全パス
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 本モジュールを呼び出すプログラム:
# - get_raceResults.py (メイン実行モジュール)
# └─ コマンドライン引数の解析と設定の初期化
# - scraper_manager.py (スクレイピング統括モジュール)
# └─ スクレイピング設定の取得とパス管理
# - web_manager.py (ウェブアクセスモジュール)
# └─ ブラウザ設定とユーザーエージェント取得
# - database_manager.py (データベース管理モジュール)
# └─ データベースパスの取得
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. 設定ファイルの使用:
# - 設定ファイルは任意のパスに配置可能
# - 設定ファイルが存在しない場合は自動的にデフォルト値が使用される
# - コマンドライン引数で明示的に指定された値が最優先される
#
# 2. 期間検証の動作:
# - 未来の年月は指定できない (現在の日付より先の期間)
# - 開始月は終了月より後にできない (論理的整合性の確保)
# - システムの現在日付を基準に自動的に検証
#
# 3. ディレクトリ管理:
# - 必要なディレクトリは自動的に作成される
# - 既存のディレクトリは上書きされない (exist_ok=True)
# - パス生成は非OS依存 (os.path.joinを使用)
#
import os
import json
import datetime
class ConfigManager:
"""
アプリケーション全体の設定を管理するクラス
設定ファイルの読み込み、デフォルト値の提供、設定値の検証などを行う
"""
# デフォルト設定
DEFAULT_CONFIG = {
"base_url": "https://db.netkeiba.com",
"search_url": "https://db.netkeiba.com/?pid=race_search_detail",
"batch_size": 40,
"wait_minutes": 10,
"min_wait_seconds": 2,
"max_wait_seconds": 5,
"data_dir": "./data/raceresults",
"use_headless_browser": False,
"user_agents": [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.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 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15"
]
}
def __init__(self, config_file_path=None):
"""
ConfigManagerの初期化
:param config_file_path: 設定ファイルのパス。指定がない場合はデフォルト設定を使用する
:type config_file_path: str, optional
"""
self.current_date = datetime.date.today()
self.current_year = self.current_date.year
self.current_month = self.current_date.month
# 設定をデフォルト値で初期化
self.config = self.DEFAULT_CONFIG.copy()
# 設定ファイルが指定されている場合は読み込み
if config_file_path and os.path.exists(config_file_path):
self._load_config_file(config_file_path)
# データディレクトリを確保
self._ensure_data_directories()
def _load_config_file(self, config_file_path):
"""
設定ファイルを読み込む。
:param config_file_path: 設定ファイルのパス
:type config_file_path: str
"""
try:
with open(config_file_path, 'r', encoding='utf-8') as file:
user_config = json.load(file)
# デフォルト設定にユーザー設定を上書き
self.config.update(user_config)
except Exception as e:
print(f"設定ファイルの読み込みエラー: {e}")
print("デフォルト設定を使用します")
def _ensure_data_directories(self):
"""
データディレクトリの存在を確認し、なければ作成する
"""
os.makedirs(self.config["data_dir"], exist_ok=True)
def get_db_path(self):
"""
データベースファイルのパスを取得する。
:return: データベースファイルの完全パス
:rtype: str
"""
return os.path.join(self.config["data_dir"], "race_results.db")
def get_csv_dir_for_year(self, year):
"""
指定された年のCSVディレクトリを取得する。
ディレクトリが存在しない場合は作成する。
:param year: 対象年
:type year: int or str
:return: 年別CSVディレクトリの完全パス
:rtype: str
"""
year_dir = os.path.join(self.config["data_dir"], str(year))
os.makedirs(year_dir, exist_ok=True)
return year_dir
def get_csv_path(self, year, month):
"""
指定された年月のCSVファイルパスを取得する。
:param year: 対象年
:type year: int or str
:param month: 対象月
:type month: int or str
:return: 月別CSVファイルの完全パス
:rtype: str
"""
year_dir = self.get_csv_dir_for_year(year)
return os.path.join(year_dir, f"{year}_{month}_raceresults.csv")
def get_combined_csv_path(self, year):
"""
結合されたCSVファイルのパスを取得する。
:param year: 対象年
:type year: int or str
:return: 年間統合CSVファイルの完全パス
:rtype: str
"""
year_dir = self.get_csv_dir_for_year(year)
return os.path.join(year_dir, f"{year}_all_raceresults.csv")
def validate_scraping_period(self, year, start_month, end_month):
"""
スクレイピング期間の検証を行う。
未来の年月や不正な期間範囲をチェックし、問題があれば例外を発生させる。
:param year: 対象年
:type year: int
:param start_month: 開始月
:type start_month: int
:param end_month: 終了月
:type end_month: int
:return: 有効な期間であればTrue
:rtype: bool
:raises ValueError: 無効な期間の場合
"""
if year > self.current_year:
raise ValueError("指定された年が現在の年より未来です")
if start_month > end_month:
raise ValueError("開始月は終了月より後にできません")
if year == self.current_year and end_month > self.current_month:
raise ValueError("指定された終了月が現在の月より未来です")
return True
def generate_year_month_pairs(self, year, start_month, end_month):
"""
指定された期間の年月ペアを生成する。
:param year: 対象年
:type year: int
:param start_month: 開始月
:type start_month: int
:param end_month: 終了月
:type end_month: int
:return: (年, 月)のタプルリスト
:rtype: list
"""
self.validate_scraping_period(year, start_month, end_month)
return [(year, str(month)) for month in range(start_month, end_month + 1)]
8.3 web_manager.py (Web操作管理)
# ==============================================================================
# 競馬レース結果スクレイピングWebアクセス管理モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果を効率的かつ安全にスクレイピングするためのWebアクセス管理モジュール。
# Seleniumを使用したブラウザ操作と一般的なHTTPリクエストの両方をサポートし、サイトへの負荷を軽減しながら必要なデータを
# 収集するための機能を提供します。
#
# プログラムの主な機能:
# 1. ブラウザ自動操作(WebDriverHandler)
# - Seleniumを使用したブラウザの制御と自動操作
# - ChromeDriverの自動インストールと設定
# - レース検索条件の設定と実行
# - 検索結果ページからのレースリンク抽出
# 2. HTTP通信管理(RequestManager)
# - 複数ユーザーエージェントのローテーション
# - リトライ機能付きHTTPリクエスト処理
# - HTMLコンテンツの取得とBeautifulSoupへの変換
# - エラー処理とリカバリー
# 3. サイト負荷軽減対策
# - ランダム待機時間の実装
# - 自然なブラウジングパターンの模倣
# - スクロール操作で人間のような振る舞い
# - 複数プロセス同時実行の防止
#
# ==============================================================================
# クラスと機能の詳細
# ==============================================================================
# 1. WebDriverHandler:
# - ブラウザインスタンスの作成と設定
# └─ ChromeDriverの自動インストール
# └─ ヘッドレスモードのサポート
# └─ ロックファイルによる同時実行制御
# - レース検索機能
# └─ 競馬データベースサイトへのアクセス
# └─ 芝・ダート両方のレースを対象に設定
# └─ 年月指定での検索条件設定
# - レースリンク収集
# └─ 検索結果ページからのリンク抽出
# └─ ページネーション(次へボタン)の処理
# └─ 抽出URLの絶対パス変換
#
# 2. RequestManager:
# - HTMLコンテンツ取得
# └─ 指定URLからのHTMLコンテンツ取得
# └─ BeautifulSoupオブジェクトへの変換
# - リトライ機能付きリクエスト
# └─ 異なるユーザーエージェントでのリクエスト
# └─ エラー発生時の複数回リトライ
# └─ エラーメッセージのログ出力
#
# ==============================================================================
# 制御フローとプロセス
# ==============================================================================
# 1. ブラウザ操作の流れ:
# - ブラウザ初期化
# └─ search_race()でレース検索ページにアクセス
# └─ 検索条件を設定(年月、競馬場種別など)
# └─ 検索実行
# └─ collect_race_links()でレースリンクを収集
# └─ _extract_links_from_page()で1ページのリンク抽出
# └─ _click_next_btn()で次ページに移動(全ページ走査)
# └─ close_browser()でブラウザを終了
#
# 2. HTTP通信の流れ:
# - get_html()でHTMLコンテンツ取得リクエスト
# └─ retry_request()で通信実行(リトライあり)
# └─ ランダムなユーザーエージェントの選択
# └─ レスポンスコードのチェック
# └─ エラー時はリトライ(最大回数まで)
# └─ 成功時はBeautifulSoupオブジェクトを返却
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 本モジュールを呼び出すプログラム:
# - scraper_manager.py
# └─ WebDriverHandlerを使用したレース検索と結果収集
# └─ RequestManagerを使用した各レースの詳細情報取得
# - content_parsers.py
# └─ get_html()で取得したHTMLからのデータ抽出
#
# 2. 本モジュールが依存するプログラム:
# - config_manager.py
# └─ 設定値の取得(URLs、待機時間、ユーザーエージェントなど)
# - 外部ライブラリ:
# ├─ selenium: ブラウザ操作
# ├─ webdriver_manager: ChromeDriverの自動管理
# ├─ filelock: 同時実行制御
# ├─ requests: HTTPリクエスト
# └─ beautifulsoup4: HTML解析
#
# ==============================================================================
# 使用例
# ==============================================================================
# 1. WebDriverHandlerの使用:
# ```python
# # WebDriverHandlerのインスタンス作成
# webdriver = WebDriverHandler(config_manager)
#
# # ブラウザの初期化
# browser = webdriver.initialize_browser()
#
# # 2023年12月のレースを検索
# webdriver.search_race(2023, 12)
#
# # レースリンクの収集
# links = webdriver.collect_race_links()
# ```
#
# 2. RequestManagerの使用:
# ```python
# # RequestManagerのインスタンス作成
# request = RequestManager(config_manager)
#
# # HTMLコンテンツの取得
# soup = request.get_html(url)
#
# # リトライ設定を指定してリクエスト
# response = request.retry_request(url, max_retries=5, retry_delay=2)
# ```
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. ブラウザ操作に関する注意:
# - ChromeDriverのインストールは自動的に管理(手動インストール不要)
# - ヘッドレスモードは設定ファイルで制御(use_headless_browser)
# - 複数プロセスから同時実行されないようロックファイルで制御
# - ブラウザはリソースを消費するため、使用後に必ず閉じる
#
# 2. リクエスト管理の注意:
# - 過度な短時間リクエストはサイト側から制限される可能性あり
# - min_wait_seconds/max_wait_secondsの設定で適切な間隔を確保
# - 異常なエラー発生時はIPブロックの可能性を考慮
# - 大量のページを一度に取得する場合はバッチ処理を検討
#
# 3. 実行環境の注意:
# - Chrome/Chromiumブラウザがインストールされている必要あり
# - 十分なメモリ(特にヘッドレスでない場合)
# - 安定したインターネット接続
# - ファイアウォール設定で外部アクセスが許可されていること
#
import random
import time
import requests
import os
from filelock import FileLock, Timeout
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
from webdriver_manager.chrome import ChromeDriverManager
from bs4 import BeautifulSoup
class WebDriverHandler:
"""
Seleniumブラウザの操作を管理するクラス
ブラウザの初期化、操作、スクロール、待機などの機能を提供
"""
def __init__(self, config_manager):
"""
WebDriverHandlerの初期化
設定マネージャから必要な設定を取得し、ブラウザ操作のための初期設定を行う。
初期化時点ではブラウザは起動せず、initialize_browser()メソッドで起動する。
:param ConfigManager config_manager: 設定管理オブジェクト
:return: なし
"""
self.config = config_manager.config
self.browser = None
def initialize_browser(self):
"""
Chromeブラウザを初期化
ChromeDriverのインストールと設定を行い、ブラウザインスタンスを作成する。
複数プロセスでの同時実行を避けるためFileLockを使用する。
:return: 初期化されたブラウザインスタンス
:rtype: webdriver.Chrome または None(失敗時)
"""
# ロックファイルのパスを設定
lock_file_dir = os.path.join(self.config["data_dir"])
os.makedirs(lock_file_dir, exist_ok=True)
lock_file_path = os.path.join(lock_file_dir, 'chromedriver_install.lock')
# FileLockオブジェクトを作成
lock = FileLock(lock_file_path, timeout=120)
try:
# ロックを取得
with lock.acquire():
chrome_options = webdriver.ChromeOptions()
if self.config.get("use_headless_browser", False):
chrome_options.add_argument('--headless')
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
self.browser = webdriver.Chrome(
service=ChromeService(ChromeDriverManager().install()),
options=chrome_options
)
return self.browser
except Timeout:
print('ロックの取得に失敗しました。')
return None
def wait_for_random_time(self, min_seconds=None, max_seconds=None):
"""
ランダムな時間だけ待機
自然なブラウジングを模倣するため、指定された範囲内でランダムな秒数待機する。
引数が指定されない場合は設定ファイルの値を使用する。
:param int min_seconds: 最小待機時間(秒)(オプション)
:param int max_seconds: 最大待機時間(秒)(オプション)
"""
min_sec = min_seconds or self.config["min_wait_seconds"]
max_sec = max_seconds or self.config["max_wait_seconds"]
time.sleep(random.uniform(min_sec, max_sec))
def scroll_webpage(self, pixel):
"""
Webページを縦方向にスクロール
Webページを指定されたピクセル分だけ縦方向にスクロールする。
正の値で下方向、負の値で上方向にスクロールする。
:param int pixel: スクロールするピクセル数
"""
if self.browser:
self.browser.execute_script(f'window.scrollTo(0, {pixel});')
def search_race(self, year, month):
"""
レースを検索
競馬データベースWebページを開き、指定された年月のレース結果を検索する。
芝とダートの両方のレースを対象に検索を実行する。
:param int/str year: 検索する年
:param int/str month: 検索する月
"""
# 競馬データベースを開く
self.browser.get(self.config["search_url"])
self.browser.implicitly_wait(10)
self.wait_for_random_time()
# 検索条件を設定
# 競争種別で「芝」と「ダート」にチェック
self.browser.find_element(By.ID, value='check_track_1').click()
self.browser.find_element(By.ID, value='check_track_2').click()
# 期間を設定
self._set_search_period(str(year), str(month))
self.scroll_webpage(400)
# 表示件数を設定
self._set_display_options("100")
# self._set_display_options("20")
# 検索実行
self._submit_search()
def _set_search_period(self, year, month):
"""
検索期間を設定
検索フォームの開始年月と終了年月に指定された値を設定する。
:param str year: 検索する年
:param str month: 検索する月
"""
# 開始年月を設定
elem_start_year = self.browser.find_element(By.NAME, value='start_year')
Select(elem_start_year).select_by_value(year)
elem_start_month = self.browser.find_element(By.NAME, value='start_mon')
Select(elem_start_month).select_by_value(month)
# 終了年月を設定
elem_end_year = self.browser.find_element(By.NAME, value='end_year')
Select(elem_end_year).select_by_value(year)
elem_end_month = self.browser.find_element(By.NAME, value='end_mon')
Select(elem_end_month).select_by_value(month)
def _set_display_options(self, num):
"""
表示件数を設定
検索結果の1ページあたりの表示件数を設定する。
:param str num: 表示する項目数 ("20", "50", "100"のいずれか)
"""
elem_list = self.browser.find_element(By.NAME, value='list')
Select(elem_list).select_by_value(num)
def _submit_search(self):
"""
検索を実行
設定した条件で検索を実行する。検索実行前に自然なブラウジング動作を
模倣するための短い待機時間を設ける。
"""
elem_search = self.browser.find_element(By.CLASS_NAME, value='search_detail_submit')
self.wait_for_random_time()
elem_search.submit()
def collect_race_links(self):
"""
検索結果ページからレースリンクを収集
検索結果の全ページを順番にたどり、レース詳細ページへのリンクを収集する。
「次へ」ボタンがなくなるまで繰り返し処理を行う。
:return: 収集したレースURL一覧
:rtype: list
"""
link_list = []
no_click_next = 0
count_click_next = 0
# count = 0
while no_click_next == 0:
# count += 1
# if count > 1:
# self.close_browser()
# break
# リンクを取得
link_list = self._extract_links_from_page(link_list)
# 「次」ボタンがあればクリック
no_click_next, count_click_next = self._click_next_btn(count_click_next)
self.close_browser()
return link_list
def _extract_links_from_page(self, link_list):
"""
現在のページからレースリンクを抽出
現在表示されているページのHTMLからレース詳細ページへのリンクを抽出し、
既存のリンクリストに追加する。特定のキーワードを含むリンクは除外する。
:param list link_list: 既存のリンクリスト
:return: 更新されたリンクリスト
:rtype: list
"""
html_content = self.browser.page_source.encode('utf-8')
soup = BeautifulSoup(html_content, 'html.parser')
try:
table_data = soup.find(class_='nk_tb_common')
if not table_data:
return link_list
for link in table_data.find_all('a'):
href = link.get('href')
if href and 'race' in href and not any(word in href for word in ['horse', 'jockey', 'result', 'sum', 'list', 'movie']) and 'javascript' not in href:
absolute_url = self._get_absolute_url(href)
link_list.append(absolute_url)
return link_list
except Exception as e:
print(f"リンク抽出エラー: {e}")
return link_list
def _get_absolute_url(self, relative_url):
"""
相対URLを絶対URLに変換
相対URL(例:'/race/202201010101/')を絶対URL
(例:'https://db.netkeiba.com/race/202201010101/')に変換する。
:param str relative_url: 相対URL
:return: 絶対URL
:rtype: str
"""
import urllib.parse
return urllib.parse.urljoin(self.config["base_url"], relative_url)
def _click_next_btn(self, count_click_next):
"""
「次」ボタンをクリック
検索結果の次ページへ移動するため「次」ボタンをクリックする。
ボタンが存在しない場合は処理を終了するためのフラグを返す。
:param int count_click_next: これまでのクリック回数
:return: (次ページの有無フラグ, 更新されたクリック回数)
:rtype: tuple
"""
# 画面下部にスクロール
self.wait_for_random_time(1, 3)
self.scroll_webpage(2500)
self.wait_for_random_time(1, 3)
xpath = '//*[@id="contents_liquid"]/div[2]/ul[1]/li[14]/a/span/span'
try:
elem_next = self.browser.find_element(By.XPATH, value=xpath)
self.wait_for_random_time(2, 5)
elem_next.click()
no_click_next = 0
except Exception:
print('次のページはありません')
no_click_next = 1
count_click_next += 1
return no_click_next, count_click_next
def close_browser(self):
"""
ブラウザを閉じる
ブラウザが開いている場合にクローズし、リソースを解放する。
"""
if self.browser:
self.browser.quit()
self.browser = None
class RequestManager:
"""
HTTPリクエストを管理するクラス
ユーザーエージェントのローテーション、リトライ処理などを行う
"""
def __init__(self, config_manager):
"""
RequestManagerの初期化
設定マネージャから必要な設定を取得し、リクエスト処理の初期設定を行う。
:param ConfigManager config_manager: 設定管理オブジェクト
"""
self.config = config_manager.config
def get_html(self, url, max_retries=5, retry_delay=5):
"""
指定されたURLからHTMLを取得
指定URLからHTMLコンテンツを取得し、BeautifulSoupオブジェクトに変換する。
リクエスト失敗時は指定回数までリトライする。
:param str url: アクセスするURL
:param int max_retries: 最大リトライ回数(デフォルト:5)
:param int retry_delay: リトライ間隔(秒)(デフォルト:5)
:return: 取得したHTMLのBeautifulSoupオブジェクト、または失敗時はNone
:rtype: BeautifulSoup または None
"""
response = self.retry_request(url, max_retries, retry_delay)
if not response:
print(f"リクエスト失敗: {url}")
return None
return BeautifulSoup(response.content, 'lxml')
def retry_request(self, url, max_retries=3, retry_delay=1):
"""
リトライ機能付きHTTPリクエスト
指定URLにHTTPリクエストを送信する。失敗時は指定回数までリトライする。
毎回異なるユーザーエージェントを使用して自然なアクセスを模倣する。
:param str url: アクセスするURL
:param int max_retries: 最大リトライ回数(デフォルト:3)
:param int retry_delay: リトライ間隔(秒)(デフォルト:1)
:return: レスポンスオブジェクト、または失敗時はNone
:rtype: requests.Response または None
"""
headers = {
"User-Agent": random.choice(self.config["user_agents"]),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
}
retries = 0
while retries < max_retries:
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response
else:
print(f"ステータスコード {response.status_code}: {url}")
except requests.exceptions.RequestException as e:
print(f"リクエストエラー: {e}")
retries += 1
if retries < max_retries:
print(f"リトライ {retries}/{max_retries}...")
time.sleep(retry_delay)
return None
8.4 content_parsers.py (HTML解析)
# ==============================================================================
# 競馬レース結果HTML解析モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース情報をHTML形式から構造化データに変換する解析モジュール。
# レースの基本情報、結果テーブル、出走馬の血統情報をBeautifulSoupを使用して抽出し、
# 後続処理で利用可能なデータ形式に変換します。
#
# プログラムの主な機能:
# 1. HTML要素からのデータ抽出
# - BeautifulSoupを使用したDOM要素の検索と抽出
# - 正規表現によるテキスト処理と整形
# - エラーハンドリングによる堅牢なデータ取得
# 2. データの構造化と変換
# - pandas DataFrameへの情報整理
# - カラム名の標準化と型変換
# - データの整形と結合
# 3. 例外処理と信頼性確保
# - 各メソッド単位での例外キャッチ
# - 欠損データへの対応
# - エラー発生時のフォールバック処理
#
# ==============================================================================
# クラスと機能の詳細
# ==============================================================================
# 1. RaceInfoParser:
# - get_race_name(): レース名の抽出
# - get_race_round(): ラウンド情報の抽出 (例:「東京11R」)
# - get_race_date(): 開催日の抽出 (例:「2023年12月24日」)
# - get_race_place(): 開催場所の抽出 (例:「東京」「中山」)
# - get_race_distance(): レース距離(m)の抽出
# - get_race_weather(): 天候情報の抽出
# - get_race_condition(): 馬場状態の抽出 (芝/ダート、良/稍重など)
#
# 2. RaceResultsParser:
# - レース結果テーブルの解析
# └─ 着順、馬名、性齢、斤量、騎手、タイム、上り、単勝、人気、馬体重を抽出
# - テーブル1行からのデータ抽出
# - 抽出データの整形と結合
# └─ レース情報と血統情報を含めた最終的なデータフレーム作成
#
# 3. HorsePedigreeParser:
# - 出走馬の詳細ページリンク抽出
# - 父馬名の抽出
# - 母馬名の抽出
#
# ==============================================================================
# データ抽出フロー
# ==============================================================================
# 1. レース基本情報の抽出:
# - レースページのHTMLから見出し要素(h1)を検索してレース名を抽出
# - クラス名を指定して特定の要素からラウンド情報や開催情報を抽出
# - テキスト分割と正規表現処理による情報の整形
#
# 2. レース結果テーブルの解析:
# - テーブル要素(class="race_table_01")の検索
# - 行(tr)ごとに列(td/th)のテキストを抽出
# - 着順、除外、取消の判定と処理
# - 各列の情報を辞書形式でまとめてDataFrameに変換
#
# 3. 血統情報の取得:
# - レーステーブルから出走馬の詳細ページへのリンクを抽出
# - 各馬の詳細ページにアクセスして血統テーブルを検索
# - 父馬(b_ml)、母馬(b_fml)の情報をそれぞれ抽出
# - 全出走馬の父馬リスト、母馬リストを生成
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 本モジュールを呼び出すプログラム:
# - scraper_manager.py
# └─ スクレイピング処理全体の制御
# └─ 各レースページのHTMLから情報を抽出
# └─ データベースへの保存とCSV出力の準備
#
# 2. 本モジュールが依存するプログラム:
# - web_manager.pyの出力結果(BeautifulSoupオブジェクト)
# └─ RequestManagerで取得したHTMLコンテンツ
#
# ==============================================================================
# 使用例
# ==============================================================================
# 1. レース基本情報の取得:
# ```python
# from content_parsers import RaceInfoParser
#
# # HTMLからレース名を取得
# race_name = RaceInfoParser.get_race_name(soup)
#
# # 開催日を取得
# race_date = RaceInfoParser.get_race_date(soup)
#
# # レース距離を取得
# distance = RaceInfoParser.get_race_distance(soup)
# ```
#
# 2. レース結果の解析:
# ```python
# from content_parsers import RaceResultsParser
#
# # レース結果テーブルを解析
# results_df = RaceResultsParser.parse_race_results(soup)
#
# # 結果にレース情報と血統情報を統合
# formatted_results = RaceResultsParser.format_race_results(
# results_df, race_info, horse_pedigree
# )
# ```
#
# 3. 血統情報の取得:
# ```python
# from content_parsers import HorsePedigreeParser
#
# # 出走馬の詳細ページへのリンクを取得
# horse_links = HorsePedigreeParser.get_horse_links(race_table, base_url)
#
# # 父馬名を取得
# father_name = HorsePedigreeParser.get_horse_father_name(horse_soup)
# ```
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. HTML構造依存性:
# - 本モジュールはnetkeiba.comのHTML構造に依存しています
# - サイト側の変更によって抽出機能が動作しなくなる可能性があります
# - 定期的に抽出ロジックの検証が必要です
#
# 2. エラー処理:
# - 各メソッドはエラー発生時に空文字や空のDataFrameを返すよう設計
# - try-except構文で例外を捕捉し、プログラム全体が停止しないよう配慮
# - エラーメッセージはコンソールに出力されますが処理は継続されます
#
# 3. データ整合性:
# - 除外や取消の出走馬はresults_dfから除外されます
# - 血統情報が取得できない場合は空文字が設定されます
# - format_race_resultsでは'着順'と'上り'列は削除されます
#
import re
import pandas as pd
from bs4 import BeautifulSoup
class RaceInfoParser:
"""
レース基本情報の解析を担当するクラス
"""
@staticmethod
def get_race_name(soup):
"""
レース名を抽出
HTMLから競走のレース名(例:「○○賞」)を抽出する。
レース名は通常、ページの見出し(h1タグ)として表示されている。
:param BeautifulSoup soup: HTMLコンテンツ
:return: 抽出されたレース名
:rtype: str
"""
try:
race_name_element = soup.find_all('h1')
if len(race_name_element) > 1:
return race_name_element[1].text.strip()
except Exception as e:
print(f"レース名抽出エラー: {e}")
return ""
@staticmethod
def get_race_round(soup):
"""
ラウンド情報を抽出
HTMLからレースのラウンド情報(例:「東京11R」)を抽出する。
ラウンド情報は通常、racedata fcクラスのdlタグ内にある。
:param BeautifulSoup soup: HTMLコンテンツ
:return: 抽出されたラウンド情報
:rtype: str
"""
try:
race_round_element = soup.find('dl', attrs={'class': 'racedata fc'})
if race_round_element:
dt_elements = race_round_element.find_all('dt')
if dt_elements:
return dt_elements[0].text.replace('\n', '').strip()
except Exception as e:
print(f"ラウンド抽出エラー: {e}")
return ""
@staticmethod
def get_race_date(soup):
"""
開催日を抽出
HTMLからレースの開催日(例:「2023年12月24日」)を抽出する。
開催日は通常、smalltxtクラスのpタグ内の最初の項目として表示されている。
:param BeautifulSoup soup: HTMLコンテンツ
:return: 抽出された開催日
:rtype: str
"""
try:
race_base_info = soup.find('p', attrs={'class': 'smalltxt'})
if race_base_info:
text = race_base_info.text.replace(u'\xa0', u' ')
words = text.split(' ')
if words:
return words[0].strip()
except Exception as e:
print(f"開催日抽出エラー: {e}")
return ""
@staticmethod
def get_race_place(soup):
"""
開催場所を抽出
HTMLからレースの開催場所(例:「東京」「中山」)を抽出する。
開催場所は通常、smalltxtクラスのpタグ内の2番目の項目として表示されている。
:param BeautifulSoup soup: HTMLコンテンツ
:return: 抽出された開催場所
:rtype: str
"""
try:
race_base_info = soup.find('p', attrs={'class': 'smalltxt'})
if race_base_info:
text = race_base_info.text.replace(u'\xa0', u' ')
words = text.split(' ')
if len(words) > 1:
return words[1].strip()
except Exception as e:
print(f"開催場所抽出エラー: {e}")
return ""
@staticmethod
def get_race_distance(soup):
"""
レース距離を抽出
HTMLからレースの距離をメートル単位で抽出する。
距離は通常、diary_snap_cutタグ内に表示されており、
「芝1600m」や「ダ1200m」などの形式で書かれている。
:param BeautifulSoup soup: HTMLコンテンツ
:return: 抽出されたレース距離(メートル)
:rtype: int
"""
try:
race_info = soup.find('diary_snap_cut')
if race_info:
text = race_info.text.replace(u'\xa0', u' ').replace(u'\xa5', u' ')
words = text.split('/')
if words:
# 2周などの文字を削除し、数字のみを抽出
distance_text = re.sub(r'2周', '', words[0])
distance = int(re.sub(r'\D', '', distance_text))
return distance
except Exception as e:
print(f"距離抽出エラー: {e}")
return 0
@staticmethod
def get_race_weather(soup):
"""
天候情報を抽出
HTMLからレース開催時の天候情報(例:「晴」「曇」「雨」)を抽出する。
天候情報は通常、diary_snap_cutタグ内にスラッシュで区切られた形式で表示されている。
:param BeautifulSoup soup: HTMLコンテンツ
:return: 抽出された天候情報
:rtype: str
"""
try:
race_info = soup.find('diary_snap_cut')
if race_info:
text = race_info.text.replace(u'\xa0', u' ').replace(u'\xa5', u' ')
words = text.split('/')
if len(words) > 1:
weather_parts = words[1].split(':')
if len(weather_parts) > 1:
return weather_parts[1].strip()
except Exception as e:
print(f"天候抽出エラー: {e}")
return ""
@staticmethod
def get_race_condition(soup):
"""
馬場状態を抽出
HTMLからレース開催時の馬場状態(例:「芝:良」「ダート:稍重」)を抽出する。
馬場状態は通常、diary_snap_cutタグ内にスラッシュで区切られた形式で表示されている。
:param BeautifulSoup soup: HTMLコンテンツ
:return: 馬場種類と馬場状態のタプル
:rtype: tuple
"""
try:
race_info = soup.find('diary_snap_cut')
if race_info:
text = race_info.text.replace(u'\xa0', u' ').replace(u'\xa5', u' ')
words = text.split('/')
race_condition = words[2].split(':')
return race_condition[0], race_condition[1]
except Exception as e:
print(f"馬場状態抽出エラー: {e}")
return "", ""
class RaceResultsParser:
"""
レース結果の解析を担当するクラス
"""
@staticmethod
def parse_race_results(soup):
"""
レース結果テーブルを解析
HTMLのレース結果テーブルから、着順、馬名、騎手名などの情報を抽出し、
pandas DataFrameとして構造化する。除外や取消の出走馬は除外される。
:param BeautifulSoup soup: HTMLコンテンツ
:return: 出走馬情報のデータフレーム
:rtype: pd.DataFrame
"""
try:
# テーブルを抽出
table = soup.find('table', attrs={'class': 'race_table_01'})
if not table:
print("テーブルが見つかりません")
return pd.DataFrame()
# 出走馬情報を取得
rows = []
for tr in table.find_all('tr')[1:]: # ヘッダー行をスキップ
cols = tr.find_all(['td', 'th'])
if not cols:
continue
# 着順、除外、取消をチェック
order = cols[0].get_text().strip()
if '除外' in order or '取消' in order:
continue
# 各列のデータを抽出
row_data = RaceResultsParser._extract_row_data(cols)
rows.append(row_data)
return pd.DataFrame(rows)
except Exception as e:
print(f"レース結果解析エラー: {e}")
return pd.DataFrame()
@staticmethod
def _extract_row_data(cols):
"""
行データを抽出
テーブルの1行から必要なデータを抽出し、辞書形式で返す。
各列の意味は固定で、着順、馬名、性齢、斤量、騎手など決まった順序で並んでいる。
:param list cols: 行の列データ
:return: 抽出されたデータの辞書
:rtype: dict
"""
return {
'着順': cols[0].get_text().strip(),
'馬名': cols[3].get_text().strip(),
'性齢': cols[4].get_text().strip(),
'斤量': cols[5].get_text().strip(),
'騎手': cols[6].get_text().strip(),
'タイム': cols[7].get_text().strip(),
'上り': cols[11].get_text().strip() if len(cols) > 11 else '',
'単勝': cols[12].get_text().strip().replace(',', '') if len(cols) > 12 else '',
'人気': cols[13].get_text().strip() if len(cols) > 13 else '',
'馬体重': cols[14].get_text().strip() if len(cols) > 14 else ''
}
@staticmethod
def format_race_results(race_results_df, race_info, horse_pedigree):
"""
レース結果を整形
出走馬情報のデータフレームにレース基本情報と血統情報を追加し、
最終的な出力形式に整形する。このデータは分析や保存に使用される。
:param pd.DataFrame race_results_df: 出走馬情報のデータフレーム
:param dict race_info: レース基本情報
:param tuple horse_pedigree: 血統情報 (父馬リスト, 母馬リスト)
:return: 整形されたレース結果
:rtype: pd.DataFrame
"""
try:
# コピーを作成
formatted_results = race_results_df.copy()
# 必要な情報を追加
formatted_results['距離'] = race_info.get('距離', 0)
formatted_results['天候'] = race_info.get('天候', '')
formatted_results['馬場'] = race_info.get('馬場', '')
formatted_results['状態'] = race_info.get('状態', '')
formatted_results['開催日'] = race_info.get('開催日', '')
formatted_results['レース名'] = race_info.get('レース名', '')
formatted_results['開催場所'] = race_info.get('開催場所', '')
formatted_results['ラウンド'] = race_info.get('ラウンド', '')
# 血統情報を追加
male_horse_pedigree_list, female_horse_pedigree_list = horse_pedigree
formatted_results['父親'] = male_horse_pedigree_list
formatted_results['母親'] = female_horse_pedigree_list
# 使用しない列を削除
formatted_results.drop(['着順', '上り'], axis=1, inplace=True, errors='ignore')
# 列の順序を調整
column_order = [
'タイム', '馬名', '父親', '母親', '性齢', '斤量', '騎手',
'単勝', '人気', '馬体重', '距離', '天候', '馬場', '状態',
'開催日', 'レース名', '開催場所', 'ラウンド'
]
return formatted_results.reindex(columns=column_order)
except Exception as e:
print(f"レース結果整形エラー: {e}")
return pd.DataFrame()
class HorsePedigreeParser:
"""
馬の血統情報解析を担当するクラス
"""
@staticmethod
def get_horse_links(race_table_data, base_url):
"""
出走馬のリンクを取得
レース結果テーブルから、各出走馬の詳細ページへのリンクを抽出する。
これらのリンクは後で血統情報を取得するために使用される。
:param BeautifulSoup race_table_data: レーステーブル要素
:param str base_url: ベースURL
:return: 出走馬の詳細ページへのURLリスト
:rtype: list
"""
link_url = []
try:
href_list = race_table_data.find_all('a')
for href_link in href_list:
href = href_link.get('href')
if href and '/horse/' in href:
import urllib.parse
absolute_url = urllib.parse.urljoin(base_url, href)
link_url.append(absolute_url)
except Exception as e:
print(f"出走馬リンク取得エラー: {e}")
return link_url
@staticmethod
def get_horse_father_name(soup):
"""
父馬の名前を取得
馬の詳細ページから父馬(種牡馬)の名前を抽出する。
血統テーブルの最初の行にある父馬の名前を取得する。
:param BeautifulSoup soup: 血統情報ページのHTMLコンテンツ
:return: 父馬名
:rtype: str
"""
try:
blood_table = soup.find('table', class_='blood_table')
if not blood_table:
return ""
father_row = blood_table.find_all('tr')[0] # 1行目(父親の行)
father_cell = father_row.find('td', class_='b_ml')
if not father_cell:
return ""
father_link = father_cell.find('a')
if father_link:
return father_link.text.replace('\n', '').strip()
else:
return father_cell.text.replace('\n', '').strip()
except Exception as e:
print(f"父馬名取得エラー: {e}")
return ""
@staticmethod
def get_horse_mother_name(soup):
"""
母馬の名前を取得
馬の詳細ページから母馬の名前を抽出する。
血統テーブルの3行目にある母馬の名前を取得する。
:param BeautifulSoup soup: 血統情報ページのHTMLコンテンツ
:return: 母馬名
:rtype: str
"""
try:
blood_table = soup.find('table', class_='blood_table')
if not blood_table:
return ""
mother_row = blood_table.find_all('tr')[2] # 3行目(母親の行)
mother_cell = mother_row.find('td', class_='b_fml')
if not mother_cell:
return ""
mother_link = mother_cell.find('a')
if mother_link:
return mother_link.text.replace('\n', '').strip()
else:
return mother_cell.text.replace('\n', '').strip()
except Exception as e:
print(f"母馬名取得エラー: {e}")
return ""
8.5 database_manager.py (データベース管理)
# ==============================================================================
# 競馬レース結果データベース管理モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果をSQLiteデータベースに保存・管理するためのモジュール。
# スクレイピングで取得したレースデータを構造化して保存し、効率的なデータ検索や再開可能なスクレイピングを実現します。
# URLの処理状況の追跡機能も提供します。
#
# プログラムの主な機能:
# 1. データベース管理基盤
# - SQLiteデータベースへの接続とコンテキスト管理
# - テーブル構造の初期化と確認
# - トランザクション管理とエラーハンドリング
# 2. データの保存と取得
# - レース結果のデータベースへの保存
# - 月別レース結果のDataFrame形式での取得
# - 数値データの安全な変換と型チェック
# 3. URL処理状況の管理
# - 処理済み/未処理URLの管理
# - 失敗したURLの記録と追跡
# - バッチ処理のための効率的なURL状態チェック
#
# ==============================================================================
# データベーススキーマ
# ==============================================================================
# 1. テーブル構造:
# - races(レース基本情報)
# ├─ race_id: INTEGER PRIMARY KEY - 主キー
# ├─ race_date: TEXT - レース開催日
# ├─ race_name: TEXT - レース名
# ├─ location: TEXT - 開催場所
# ├─ round: TEXT - ラウンド情報
# ├─ weather: TEXT - 天候
# ├─ ground: TEXT - 馬場(芝/ダート)
# ├─ condition: TEXT - 馬場状態
# ├─ distance: INTEGER - 距離(m)
# ├─ year: INTEGER - 開催年
# ├─ month: INTEGER - 開催月
# └─ created_at: TIMESTAMP - 作成日時
#
# - race_horses(出走馬情報)
# ├─ id: INTEGER PRIMARY KEY - 主キー
# ├─ race_id: INTEGER - レースID(外部キー)
# ├─ horse_name: TEXT - 馬名
# ├─ father: TEXT - 父馬名
# ├─ mother: TEXT - 母馬名
# ├─ age: TEXT - 性齢
# ├─ rider_weight: REAL - 斤量
# ├─ rider: TEXT - 騎手名
# ├─ odds: REAL - 単勝オッズ
# ├─ popular: INTEGER - 人気順
# ├─ horse_weight: TEXT - 馬体重
# ├─ time: TEXT - タイム
# └─ created_at: TIMESTAMP - 作成日時
#
# - processed_urls(処理済みURL管理)
# ├─ url: TEXT PRIMARY KEY - URL(主キー)
# ├─ race_id: INTEGER - 関連するレースID
# ├─ status: TEXT - 処理状態('success'/'failed')
# └─ processed_at: TIMESTAMP - 処理日時
#
# ==============================================================================
# メソッドと機能の詳細
# ==============================================================================
# 1. データベース基盤メソッド:
# - __init__(): データベースマネージャーの初期化
# - _ensure_db_directory(): データベースディレクトリの確保
# - _get_connection(): SQLite接続のコンテキストマネージャ
# - _initialize_database(): 必要なテーブル構造の作成
#
# 2. データ操作メソッド:
# - save_race_result(): レース結果の保存
# └─ レース情報と出走馬情報を適切なテーブルに保存
# - _convert_horse_numeric_data(): 馬情報の数値変換
# - get_monthly_race_results(): 月別レース結果の取得
# └─ 指定された年月のすべてのレースをDataFrameとして取得
#
# 3. URL管理メソッド:
# - record_failed_url(): 失敗したURLの記録
# - get_unprocessed_urls(): 未処理URLの取得
# └─ 指定されたURLリストから未処理のもののみを抽出
# - get_processed_urls_status(): 処理状況の取得
# └─ 成功したURLと失敗したURLを分類して返却
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 本モジュールを呼び出すプログラム:
# - scraper_manager.py
# └─ スクレイピング処理の統括モジュール
# └─ レース結果の保存とURL管理情報の活用
# - get_raceResults.py
# └─ メインプログラム
# └─ データベースマネージャーの初期化
#
# 2. 本モジュールが依存するプログラム:
# - 外部ライブラリ:
# ├─ sqlite3: SQLiteデータベース操作
# ├─ pandas: DataFrameによるデータ操作
# ├─ os: ファイルパス操作
# └─ contextlib: コンテキストマネージャ
#
# ==============================================================================
# 使用例
# ==============================================================================
# 1. データベースマネージャーの初期化:
# ```python
# from database_manager import DatabaseManager
#
# # データベースマネージャーの初期化
# db_manager = DatabaseManager("./data/raceresults/race_results.db")
# ```
#
# 2. レース結果の保存:
# ```python
# # レース結果をデータベースに保存
# success = db_manager.save_race_result(
# "https://db.netkeiba.com/race/202301010101/",
# race_data_df, # pandas DataFrame
# 2023, # 年
# 1 # 月
# )
# ```
#
# 3. URL処理状況の管理:
# ```python
# # 未処理URLの取得
# urls_to_process = ["https://db.netkeiba.com/race/202301010101/",
# "https://db.netkeiba.com/race/202301010102/"]
# unprocessed_urls = db_manager.get_unprocessed_urls(urls_to_process)
#
# # 失敗したURLの記録
# db_manager.record_failed_url("https://db.netkeiba.com/race/202301010103/")
#
# # 処理状況の取得
# processed, failed = db_manager.get_processed_urls_status(urls_to_process)
# ```
#
# 4. 月別データの取得:
# ```python
# # 2024年3月のレース結果を取得
# march_results = db_manager.get_monthly_race_results(2024, 3)
#
# # DataFrameとして処理
# print(f"取得したレース数: {len(march_results)}")
# march_results.to_csv("2024_3_results.csv", index=False)
# ```
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. データベース操作:
# - トランザクション処理により整合性を確保
# - 自動的にロールバックとコネクション管理を実行
# - 複数プロセスからの同時アクセスには注意が必要
#
# 2. データ型と変換:
# - 数値データは安全に変換され、変換不能な場合はNoneが設定
# - タイムスタンプはSQLiteのCURRENT_TIMESTAMPで自動設定
# - テキストデータのエンコーディングはUTF-8を使用
#
# 3. エラーハンドリング:
# - すべてのデータベース操作は例外処理で保護
# - エラーが発生した場合はコンソールにメッセージを出力
# - データベース初期化エラーはアプリケーション起動時に検出可能
#
import sqlite3
import pandas as pd
import os
from contextlib import contextmanager
class DatabaseManager:
"""
レース結果データベースを管理するクラス
SQLiteデータベースへの接続、テーブル作成、データの保存と取得を担当。
処理済みURLの管理も行い、効率的なデータ収集を支援する。
"""
def __init__(self, db_path):
"""
DatabaseManagerの初期化
指定されたパスにSQLiteデータベースファイルを作成または接続し、
必要なテーブルが存在することを確認する。
:param str db_path: データベースファイルのパス
"""
self.db_path = db_path
self._ensure_db_directory()
self._initialize_database()
def _ensure_db_directory(self):
"""
データベースディレクトリが存在することを確認
データベースファイルのディレクトリが存在しない場合は自動的に作成する。
"""
db_dir = os.path.dirname(self.db_path)
os.makedirs(db_dir, exist_ok=True)
@contextmanager
def _get_connection(self):
"""
データベース接続のコンテキストマネージャ
自動的に接続を開いて閉じるためのコンテキストマネージャを提供する。
エラーが発生した場合は自動的にロールバックを行う。
:yield: sqlite3.Connection オブジェクト
:raises sqlite3.Error: SQLiteデータベース操作中にエラーが発生した場合
"""
conn = None
try:
conn = sqlite3.connect(self.db_path)
yield conn
except sqlite3.Error as e:
if conn:
conn.rollback()
raise e
finally:
if conn:
conn.close()
def _initialize_database(self):
"""
必要なテーブルを作成する
races(レース情報)、race_horses(出走馬情報)、processed_urls(処理済みURL)の
3つのテーブルが存在することを確認し、なければ作成する。
:raises sqlite3.Error: データベース初期化中にエラーが発生した場合
"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
# レース情報テーブル
cursor.execute('''
CREATE TABLE IF NOT EXISTS races (
race_id INTEGER PRIMARY KEY AUTOINCREMENT,
race_date TEXT,
race_name TEXT,
location TEXT,
round TEXT,
weather TEXT,
ground TEXT,
condition TEXT,
distance INTEGER,
year INTEGER,
month INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 出走馬情報テーブル
cursor.execute('''
CREATE TABLE IF NOT EXISTS race_horses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
race_id INTEGER,
horse_name TEXT,
father TEXT,
mother TEXT,
age TEXT,
rider_weight REAL,
rider TEXT,
odds REAL,
popular INTEGER,
horse_weight TEXT,
time TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (race_id) REFERENCES races(race_id)
)
''')
# 処理済みURLの管理テーブル
cursor.execute('''
CREATE TABLE IF NOT EXISTS processed_urls (
url TEXT PRIMARY KEY,
race_id INTEGER,
status TEXT,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (race_id) REFERENCES races(race_id)
)
''')
conn.commit()
except sqlite3.Error as e:
print(f"データベース初期化エラー: {e}")
def save_race_result(self, url, race_data, year, month):
"""
レース結果をデータベースに保存
レース基本情報をracesテーブルに、出走馬情報をrace_horsesテーブルに保存する。
また、処理済みURLとしてURLを記録する。
:param str url: レース結果のURL
:param pd.DataFrame/pd.Series race_data: レース結果のデータ
:param int year: レースの年
:param int/str month: レースの月
:return: 保存が成功したかどうか
:rtype: bool
"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
# DataFrameかSeriesかを判定して処理
if isinstance(race_data, pd.DataFrame):
first_row = race_data.iloc[0]
else:
first_row = race_data
# 数値型の安全な変換
try:
distance = int(first_row['距離'])
except (ValueError, TypeError):
distance = 0
# レース情報を保存
cursor.execute('''
INSERT INTO races
(race_date, race_name, location, round, weather, ground,
condition, distance, year, month)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
first_row['開催日'],
first_row['レース名'],
first_row['開催場所'],
first_row['ラウンド'],
first_row['天候'],
first_row['馬場'],
first_row['状態'],
distance,
year,
month
))
race_id = cursor.lastrowid
# 出走馬情報を保存
if isinstance(race_data, pd.DataFrame):
horse_data = race_data
else:
horse_data = pd.DataFrame([race_data])
for _, horse in horse_data.iterrows():
# 数値データの安全な変換
odds, rider_weight, popular = self._convert_horse_numeric_data(horse)
cursor.execute('''
INSERT INTO race_horses
(race_id, horse_name, father, mother, age, rider_weight,
rider, odds, popular, horse_weight, time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
race_id,
horse['馬名'],
horse['父親'],
horse['母親'],
horse['性齢'],
rider_weight,
horse['騎手'],
odds,
popular,
horse['馬体重'],
horse['タイム']
))
# 処理済みURLを記録
cursor.execute('''
INSERT OR REPLACE INTO processed_urls (url, race_id, status)
VALUES (?, ?, 'success')
''', (url, race_id))
conn.commit()
return True
except Exception as e:
print(f"データ保存エラー: {e}")
return False
def _convert_horse_numeric_data(self, horse):
"""
馬のデータから数値項目を安全に変換
単勝オッズ、騎手の斤量、人気順の数値に変換する。
変換できない場合はNoneを返す。
:param pd.Series horse: 馬のデータ行
:return: 変換後の数値データのタプル (odds, rider_weight, popular)
:rtype: tuple
"""
try:
odds = float(horse['単勝']) if pd.notna(horse['単勝']) and horse['単勝'] != '' else None
except (ValueError, TypeError):
odds = None
try:
rider_weight = float(horse['斤量']) if pd.notna(horse['斤量']) and horse['斤量'] != '' else None
except (ValueError, TypeError):
rider_weight = None
try:
popular = int(horse['人気']) if pd.notna(horse['人気']) and horse['人気'] != '' else None
except (ValueError, TypeError):
popular = None
return odds, rider_weight, popular
def record_failed_url(self, url):
"""
失敗したURLを記録
スクレイピングに失敗したURLをデータベースに記録する。
これにより再実行時に同じURLでのエラーを回避する。
:param str url: 失敗したURL
"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO processed_urls (url, status)
VALUES (?, 'failed')
''', (url,))
conn.commit()
except sqlite3.Error as e:
print(f"失敗URL記録エラー: {e}")
def get_unprocessed_urls(self, url_list):
"""
未処理のURLを取得
指定されたURLリストの中から、まだ処理されていないURLを抽出する。
これにより重複処理を防止し、効率的なスクレイピングを実現する。
:param list url_list: チェックするURLのリスト
:return: 未処理のURLのリスト
:rtype: list
"""
if not url_list:
return []
try:
with self._get_connection() as conn:
cursor = conn.cursor()
placeholders = ','.join(['?'] * len(url_list))
query = f'''
SELECT url FROM processed_urls
WHERE url IN ({placeholders})
'''
cursor.execute(query, url_list)
processed_urls = set(row[0] for row in cursor.fetchall())
return [url for url in url_list if url not in processed_urls]
except sqlite3.Error as e:
print(f"未処理URL取得エラー: {e}")
return url_list
def get_processed_urls_status(self, url_list):
"""
処理済みURLの状態を取得
指定されたURLリストの中で、処理に成功したURLと失敗したURLを抽出する。
:param list url_list: チェックするURLのリスト
:return: (成功したURLのリスト, 失敗したURLのリスト)のタプル
:rtype: tuple
"""
if not url_list:
return [], []
try:
with self._get_connection() as conn:
cursor = conn.cursor()
placeholders = ','.join(['?'] * len(url_list))
# 成功したURLを取得
query_success = f'''
SELECT url FROM processed_urls
WHERE url IN ({placeholders}) AND status = 'success'
'''
cursor.execute(query_success, url_list)
success_urls = [row[0] for row in cursor.fetchall()]
# 失敗したURLを取得
query_failed = f'''
SELECT url FROM processed_urls
WHERE url IN ({placeholders}) AND status = 'failed'
'''
cursor.execute(query_failed, url_list)
failed_urls = [row[0] for row in cursor.fetchall()]
return success_urls, failed_urls
except sqlite3.Error as e:
print(f"URL状態取得エラー: {e}")
return [], []
def get_monthly_race_results(self, year, month):
"""
指定された年月のレース結果を取得
データベースから指定された年月のすべてのレース結果を取得し、
DataFrameとして返す。レース日付順とラウンド順にソートされる。
:param int year: 取得対象の年
:param int/str month: 取得対象の月
:return: レース結果のDataFrame
:rtype: pd.DataFrame
"""
query = '''
SELECT
rh.time as タイム,
rh.horse_name as 馬名,
rh.father as 父親,
rh.mother as 母親,
rh.age as 性齢,
rh.rider_weight as 斤量,
rh.rider as 騎手,
rh.odds as 単勝,
rh.popular as 人気,
rh.horse_weight as 馬体重,
r.distance as 距離,
r.weather as 天候,
r.ground as 馬場,
r.condition as 状態,
r.race_date as 開催日,
r.race_name as レース名,
r.location as 開催場所,
r.round as ラウンド
FROM races r
JOIN race_horses rh ON r.race_id = rh.race_id
WHERE r.year = ? AND r.month = ?
ORDER BY r.race_date, r.round
'''
try:
with self._get_connection() as conn:
df = pd.read_sql_query(query, conn, params=(year, month))
print(f"取得したレコード数: {len(df)} ({year}/{month})")
return df
except Exception as e:
print(f"月別データ取得エラー: {e}")
return pd.DataFrame()
8.6 scraper_manager.py (統括管理)
# ==============================================================================
# 競馬レース結果スクレイピング統括管理モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果をスクレイピングする全プロセスを管理・制御するモジュール。年月指定でのデータ収集から、
# 詳細情報の抽出、データベースへの保存、CSVファイル出力までの一連の流れを統括します。
# 進捗管理や実行時間予測、エラーハンドリングなどの機能も提供します。
#
# プログラムの主な機能:
# 1. スクレイピング処理の統括管理
# - 年月指定でのレース結果収集
# - バッチ処理による負荷分散
# - 詳細レース情報の取得と保存
# - CSV出力とデータ統合
# 2. 進捗・状態管理
# - 詳細な進捗状況の表示
# - 残り時間の予測と完了時刻の計算
# - 処理済み/未処理URLの管理
# - エラーハンドリングと再開可能な設計
# 3. データ整形と出力
# - 月別CSVファイルの作成
# - 年間統合CSVの生成
# - レースリンク情報のJSON保存
# - データフォーマットの標準化
#
# ==============================================================================
# スクレイピングプロセスの流れ
# ==============================================================================
# 1. 初期化と準備:
# - 設定管理とデータベース管理のインスタンス受け取り
# - Webドライバと要求管理のインスタンス作成
# - 処理開始時間の記録と年月ペアの生成
#
# 2. 月別データ収集処理:
# - 各月ごとの処理実行
# ├─ ブラウザ起動とネット競馬サイトへのアクセス
# ├─ 検索条件設定(年月・競馬場種別指定)
# ├─ 検索実行と結果ページからのレースリンク収集
# ├─ 未処理URLの特定とバッチ処理の準備
# ├─ 各レースページへのアクセスと情報抽出
# │ ├─ レース基本情報の取得(レース名、日付、場所など)
# │ ├─ レース結果テーブルの解析(着順、馬名、タイムなど)
# │ └─ 出走馬の血統情報の取得(父馬、母馬)
# ├─ 抽出データのデータベースへの保存
# ├─ 月別CSVファイルの作成
# ├─ レースリンク情報のJSON保存
# └─ 進捗状況と予想終了時刻の更新
#
# 3. バッチ処理による負荷軽減:
# - 指定数のURLごとにバッチとして処理
# ├─ 各バッチの進捗表示(tqdmプログレスバー)
# ├─ 個別URL処理の成功/失敗の追跡
# ├─ バッチ間の待機時間確保(設定可能)
# └─ 次バッチの開始時刻予告
#
# 4. データ統合と完了処理:
# - 全月の処理完了後の年間データ統合
# ├─ 月別CSVファイルの読み込みと結合
# ├─ 年間統合CSVファイルの作成
# ├─ 実行時間情報の表示
# └─ 処理完了の通知
#
# ==============================================================================
# メソッドと機能の詳細
# ==============================================================================
# 1. スクレイピング実行と管理:
# - run_scraping(): スクレイピング処理全体の実行
# - scrape_race_details(): 指定年月のレース詳細取得
# - _fetch_race_details(): バッチ処理でのURL処理
# - _scrape_single_race(): 単一レースのスクレイピング
# - _save_race_links_info(): レースリンク情報の保存
#
# 2. データ抽出と整形:
# - _extract_race_info(): レース基本情報の抽出
# - _get_horse_pedigree(): 出走馬の血統情報の取得
# - _save_to_csv(): レース結果のCSV保存
# - combine_race_result(): 年間レース結果の統合
#
# 3. 進捗管理と表示:
# - print_race_links_summary(): リンク情報のサマリー出力
# - _predict_remaining_time(): 残り時間の予測
# - _show_runtime_info(): 実行時間情報の表示
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 本モジュールが利用するモジュール:
# - config_manager.py
# └─ 設定情報の提供と年月検証
# - database_manager.py
# └─ データベース操作とURL管理
# - web_manager.py
# ├─ WebDriverHandler: ブラウザ操作
# └─ RequestManager: HTTP通信
# - content_parsers.py
# ├─ RaceInfoParser: レース基本情報の解析
# ├─ RaceResultsParser: レース結果の解析
# └─ HorsePedigreeParser: 血統情報の解析
#
# 2. 本モジュールを呼び出すプログラム:
# - get_raceResults.py
# └─ メイン実行モジュール
# └─ コマンドライン引数の解析と全体フローの起動
#
# ==============================================================================
# 出力データとファイル
# ==============================================================================
# 1. データベース保存:
# - races: レース基本情報テーブル
# - race_horses: 出走馬情報テーブル
# - processed_urls: 処理済みURL管理テーブル
#
# 2. ファイル出力:
# - 月別CSVファイル: ./data/raceresults/[年]/[年]_[月]_raceresults.csv
# └─ 指定月のすべてのレース結果を統合
# - 年間統合CSVファイル: ./data/raceresults/[年]/[年]_all_raceresults.csv
# └─ 1年分のデータを1つのファイルにまとめる
# - レースリンク情報JSON: ./data/raceresults/[年]/[年]_[月]_racelinks.json
# └─ 取得したリンク一覧と未処理リンク情報
#
# ==============================================================================
# 使用例
# ==============================================================================
# 1. 基本的な使用方法:
# ```python
# from config_manager import ConfigManager
# from database_manager import DatabaseManager
# from scraper_manager import ScraperManager
#
# # 設定とデータベース管理の初期化
# config = ConfigManager("config.json")
# db = DatabaseManager(config.get_db_path())
#
# # スクレイパーマネージャーの初期化
# scraper = ScraperManager(config, db)
#
# # 2023年の1月から12月までのデータを取得
# scraper.run_scraping(2023, 1, 12)
# ```
#
# 2. 特定月のみ処理:
# ```python
# # 2024年5月のみのデータを取得
# scraper.run_scraping(2024, 5, 5)
# ```
#
# 3. 年間データのみ統合:
# ```python
# # 2023年の月別データを年間データに統合
# scraper.combine_race_result(2023)
# ```
#
# ==============================================================================
# 注意事項と運用上の留意点
# ==============================================================================
# 1. リソース管理:
# - 長時間実行を前提とした設計(数時間~数日)
# - 電源管理に注意(バッテリー駆動の場合は特に注意)
# - 安定したネットワーク接続が必要
# - 断続的な処理のため、コンピュータがスリープしないよう設定
#
# 2. エラーハンドリング:
# - 個別のレース取得に失敗しても全体処理は継続
# - 失敗したURLは記録され再実行時に自動的にスキップ
# - 予期せぬエラーでもできるだけ回復する設計
# - 中断後の再開が可能(処理済みURLは再処理しない)
#
# 3. サイト負荷対策:
# - バッチサイズは適切に設定(デフォルト: 40件)
# - バッチ間の待機時間を確保(デフォルト: 10分)
# - リクエスト間のランダム待機時間を導入
# - サイトの利用規約を確認し、過度の負荷をかけない配慮
#
import pandas as pd
import time
import datetime
import os
import json
from tqdm import tqdm
from web_manager import WebDriverHandler, RequestManager
from content_parsers import RaceInfoParser, RaceResultsParser, HorsePedigreeParser
class ScraperManager:
"""
スクレイピングプロセス全体を管理するクラス
年月ごとのレース結果収集、詳細情報のスクレイピング、データの保存を制御
"""
def __init__(self, config_manager, db_manager):
"""
ScraperManagerの初期化
設定管理とデータベース管理のインスタンスを受け取り、
Webドライバと要求管理のインスタンスを初期化する。
:param ConfigManager config_manager: 設定管理オブジェクト
:param DatabaseManager db_manager: データベース管理オブジェクト
:return: なし
"""
self.config = config_manager
self.db = db_manager
self.webdriver = WebDriverHandler(config_manager)
self.request = RequestManager(config_manager)
# 実行時間管理
self.start_time = time.time()
def scrape_race_details(self, year, month):
"""
指定された年月のレース詳細をスクレイピング
検索条件を設定してレース検索を実行し、結果ページから
各レースの詳細情報を抽出してデータベースに保存する。
:param int/str year: スクレイピング対象の年
:param int/str month: スクレイピング対象の月
:return: 処理が成功したかどうか、全リンクリスト、未処理リンクリストのタプル
:rtype: tuple(bool, list, list)
"""
print(f"\n{year}年{month}月の情報取得を開始します")
try:
# ブラウザの初期化
browser = self.webdriver.initialize_browser()
if not browser:
print("ブラウザの初期化に失敗しました")
return False, [], []
# 検索実行
self.webdriver.search_race(year, month)
# レースリンクの収集
link_list = self.webdriver.collect_race_links()
print(f"取得したレースリンク数: {len(link_list)}")
if not link_list:
print(f"{year}年{month}月のレースデータが見つかりませんでした")
return False, [], []
# 詳細情報の取得と未処理リンクの取得
unprocessed_links = self._fetch_race_details(link_list, year, month)
# CSVファイルへの保存
self._save_to_csv(year, month)
# レースリンク情報の保存
self._save_race_links_info(year, month, link_list, unprocessed_links)
return True, link_list, unprocessed_links
except Exception as e:
print(f"スクレイピング中にエラーが発生しました: {e}")
return False, [], []
def _fetch_race_details(self, link_list, year, month):
"""
レース詳細情報の取得
URLリストから未処理のURLを抽出し、バッチ処理によりレース詳細情報を
スクレイピングしてデータベースに保存する。バッチ間には待機時間を設ける。
:param list link_list: レースリンクのリスト
:param int/str year: 対象年
:param int/str month: 対象月
:return: 処理後も未処理のままのURLリスト
:rtype: list
"""
batch_size = self.config.config["batch_size"]
wait_minutes = self.config.config["wait_minutes"]
# 未処理のURLのみを取得
unprocessed_urls = self.db.get_unprocessed_urls(link_list)
total_batches = (len(unprocessed_urls) + batch_size - 1) // batch_size
print(f"未処理のレース数: {len(unprocessed_urls)} / {len(link_list)}")
# 処理後に未処理として残るURL(処理失敗したものなど)を追跡
remaining_unprocessed = unprocessed_urls.copy()
# バッチ処理
for i in range(0, len(unprocessed_urls), batch_size):
batch_urls = unprocessed_urls[i:i + batch_size]
batch_num = i // batch_size + 1
print(f'{year}年{month}月 URLバッチ {batch_num}/{total_batches} の処理を開始します')
# バッチ内のURLを処理
for url in tqdm(batch_urls):
if not url.split('/')[-2].isdecimal():
continue
race_data = self._scrape_single_race(url)
if isinstance(race_data, str):
print(f"レース情報の取得に失敗しました: {url}")
self.db.record_failed_url(url)
# 失敗したURLは追跡リストに残す
elif not race_data.empty:
self.db.save_race_result(url, race_data, year, month)
# 成功したURLを追跡リストから削除
if url in remaining_unprocessed:
remaining_unprocessed.remove(url)
# バッチ処理完了後の待機
if i + batch_size < len(unprocessed_urls):
next_batch_time = datetime.datetime.now() + datetime.timedelta(minutes=wait_minutes)
print(f'\n情報取得先の負荷軽減のため{wait_minutes}分間待機します...')
print(f'次回の取得開始時刻: {next_batch_time.strftime("%Y-%m-%d %H:%M:%S")}')
time.sleep(wait_minutes * 60)
return remaining_unprocessed
def _save_race_links_info(self, year, month, all_links, unprocessed_links):
"""
レースリンク情報の保存
取得した全レースリンクと未処理のリンクをJSONファイルとして保存する。
:param int/str year: 対象年
:param int/str month: 対象月
:param list all_links: 全レースリンクのリスト
:param list unprocessed_links: 未処理のリンクのリスト
"""
# 年別ディレクトリの確保
year_dir = self.config.get_csv_dir_for_year(year)
# JSONファイルパスの設定
json_file_path = os.path.join(year_dir, f"{year}_{month}_racelinks.json")
# リンク情報の辞書作成
links_info = {
"year": year,
"month": month,
"scraping_date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"total_links": len(all_links),
"unprocessed_links_count": len(unprocessed_links),
"all_links": all_links,
"unprocessed_links": unprocessed_links
}
# JSON形式で保存
try:
with open(json_file_path, 'w', encoding='utf-8') as f:
json.dump(links_info, f, ensure_ascii=False, indent=2)
print(f"レースリンク情報を {json_file_path} に保存しました")
except Exception as e:
print(f"リンク情報保存エラー: {e}")
def _scrape_single_race(self, url):
"""
単一レースの詳細情報をスクレイピング
指定されたURLからレース詳細ページのHTMLを取得し、
レース情報、結果、血統情報を抽出して整形したデータを返す。
:param str url: レース詳細ページのURL
:return: 整形されたレース結果、またはエラー発生時はエラーメッセージ
:rtype: pd.DataFrame/str
"""
try:
# レースページのHTMLを取得
soup = self.request.get_html(url)
if not soup:
return "HTMLの取得に失敗しました"
# レース結果テーブルを解析
race_results = RaceResultsParser.parse_race_results(soup)
if race_results.empty:
return "レース結果の解析に失敗しました"
# レース基本情報の取得
race_info = self._extract_race_info(soup)
# 血統情報の取得
horse_pedigree = self._get_horse_pedigree(soup)
# 結果の整形
formatted_results = RaceResultsParser.format_race_results(
race_results, race_info, horse_pedigree
)
return formatted_results
except Exception as e:
print(f"レース {url} のスクレイピング中にエラー発生: {e}")
return f"エラー: {str(e)}"
def _extract_race_info(self, soup):
"""
レース基本情報を抽出
BeautifulSoupオブジェクトからレース名、開催日、場所などの
基本情報を抽出し、辞書形式で返す。
:param BeautifulSoup soup: レースページのHTMLコンテンツ
:return: レース情報の辞書
:rtype: dict
"""
race_name = RaceInfoParser.get_race_name(soup)
race_date = RaceInfoParser.get_race_date(soup)
race_place = RaceInfoParser.get_race_place(soup)
race_round = RaceInfoParser.get_race_round(soup)
distance = RaceInfoParser.get_race_distance(soup)
weather = RaceInfoParser.get_race_weather(soup)
ground, condition = RaceInfoParser.get_race_condition(soup)
return {
'レース名': race_name,
'開催日': race_date,
'開催場所': race_place,
'ラウンド': race_round,
'距離': distance,
'天候': weather,
'馬場': ground,
'状態': condition
}
def _get_horse_pedigree(self, soup):
"""
出走馬の血統情報を取得
レースページから各出走馬の詳細ページへのリンクを抽出し、
それぞれのページにアクセスして父馬と母馬の情報を取得する。
:param BeautifulSoup soup: レースページのHTMLコンテンツ
:return: 父馬リストと母馬リストのタプル
:rtype: tuple
"""
race_table_data = soup.find(class_='race_table_01 nk_tb_common')
if not race_table_data:
return ([], [])
male_horse_pedigree_list = []
female_horse_pedigree_list = []
# 出走馬のリンクを取得
link_url = HorsePedigreeParser.get_horse_links(race_table_data, self.config.config["base_url"])
# 各馬の血統情報を取得
for link in link_url:
soup_blood_table = self.request.get_html(link)
if soup_blood_table:
male_horse_pedigree_list.append(HorsePedigreeParser.get_horse_father_name(soup_blood_table))
female_horse_pedigree_list.append(HorsePedigreeParser.get_horse_mother_name(soup_blood_table))
else:
male_horse_pedigree_list.append("")
female_horse_pedigree_list.append("")
return male_horse_pedigree_list, female_horse_pedigree_list
def _save_to_csv(self, year, month):
"""
レース結果をCSVファイルとして保存
データベースから指定年月のレース結果を取得し、
CSVファイルとして保存する。
:param int/str year: 対象年
:param int/str month: 対象月
"""
print(f'{year}年{month}月のレース結果のCSVファイルを作成します')
# データベースからデータを取得
race_results = self.db.get_monthly_race_results(year, month)
if race_results.empty:
print(f"{year}年{month}月のデータが見つかりません")
return
# ファイルパスの設定
file_path = self.config.get_csv_path(year, month)
# CSVファイルとして保存
try:
race_results.to_csv(file_path, encoding='shift-jis', index=False, errors="ignore")
print(f"{file_path} にデータを保存しました")
except Exception as e:
print(f"CSVファイル保存エラー: {e}")
def print_race_links_summary(self, year, month, all_links, unprocessed_links):
"""
レースリンク情報のサマリーを出力
取得した全リンクと未処理リンクの件数を出力し、
両方のリストを表示する。
:param int/str year: 対象年
:param int/str month: 対象月
:param list all_links: 全レースリンクのリスト
:param list unprocessed_links: 未処理のリンクのリスト
"""
print(f"\n{year}年{month}月のレースリンク情報")
print(f"取得したレースリンク数: {len(all_links)}")
print(f"未処理のレースリンク数: {len(unprocessed_links)}")
print("\n取得したレースリンク一覧:")
for idx, link in enumerate(all_links, 1):
print(f"{idx}. {link}")
if unprocessed_links:
print("\n未処理のレースリンク一覧:")
for idx, link in enumerate(unprocessed_links, 1):
print(f"{idx}. {link}")
else:
print("\n未処理のレースリンクはありません。すべてのリンクが処理されました。")
def combine_race_result(self, year):
"""
年間レース結果の統合
指定年の月別CSVファイルを読み込み、統合したデータフレームを作成して
年間統合CSVファイルとして保存する。
:param int year: 統合する年
:return: 統合されたデータフレーム
:rtype: pd.DataFrame
"""
print(f"{year}年のレース結果を統合します")
# 年間データを保存するディレクトリ
year_dir = self.config.get_csv_dir_for_year(year)
# 統合データフレーム
combined_data = pd.DataFrame()
# CSVファイルの一覧を取得
if os.path.exists(year_dir):
# 月別CSVファイルを取得(_all_raceresults.csvを除外)
csv_files = [f for f in os.listdir(year_dir)
if f.endswith('_raceresults.csv') and not f.endswith('all_raceresults.csv')]
# 月でソート(降順)
csv_files.sort(key=lambda f: int(f.split('_')[1]) if f.split('_')[1].isdigit() else 0, reverse=True)
# 各ファイルを読み込んで結合
for csv_file in csv_files:
file_path = os.path.join(year_dir, csv_file)
try:
df_read_csv = pd.read_csv(
file_path,
encoding='shift-jis',
names=['タイム', '馬名', '父親', '母親', '性齢', '斤量', '騎手',
'単勝', '人気', '馬体重', '距離', '天候', '馬場', '状態',
'開催日', 'レース名', '開催場所', 'ラウンド'],
header=None
)
combined_data = pd.concat([combined_data, df_read_csv], ignore_index=True)
month = csv_file.split('_')[1]
print(f'{year}年{month}月のデータを結合しました')
except Exception as e:
print(f'ファイル {file_path} の読み込みエラー: {e}')
# 統合データを保存
if not combined_data.empty:
combined_path = self.config.get_combined_csv_path(year)
combined_data.to_csv(combined_path, encoding='shift-jis', header=False, index=False, errors='ignore')
print(f'レース結果を結合し、{combined_path}に保存しました')
return combined_data
def run_scraping(self, year, start_month, end_month):
"""
スクレイピング処理の実行
指定された年と月の範囲に対してスクレイピングを実行し、
月ごとのデータ取得と年間データの統合を行う。
:param int year: 対象年
:param int start_month: 開始月
:param int end_month: 終了月
"""
try:
# 年月ペアを生成
year_month_pairs = self.config.generate_year_month_pairs(year, start_month, end_month)
total_months = len(year_month_pairs)
print(f"処理対象期間: {year}年{start_month}月~{year}年{end_month}月 ({total_months}ヶ月)")
print(f"処理開始時刻: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 各月のデータを取得
for i, (year, month) in enumerate(year_month_pairs):
month_start_time = time.time()
# 月別データの取得
print(f"\n{year}年{month}月のデータ取得を開始します ({i + 1}/{total_months}月目)")
success, all_links, unprocessed_links = self.scrape_race_details(year, month)
# レースリンク情報のサマリーを出力
if success:
self.print_race_links_summary(year, month, all_links, unprocessed_links)
# 処理時間の計算
month_process_time = time.time() - month_start_time
# 残り処理時間の予測
self._predict_remaining_time(i, total_months, month_process_time)
# 最後の月でなければ待機
if i < len(year_month_pairs) - 1:
wait_minutes = self.config.config["wait_minutes"]
next_month_time = datetime.datetime.now() + datetime.timedelta(minutes=wait_minutes)
print(f'\n次の月の処理までの間、{wait_minutes}分間待機します...')
print(f'次の月の取得開始時刻: {next_month_time.strftime("%Y-%m-%d %H:%M:%S")}')
time.sleep(wait_minutes * 60)
# 年間データの統合
self.combine_race_result(year)
# 実行時間の表示
self._show_runtime_info()
except Exception as e:
print(f"スクレイピング実行中にエラーが発生しました: {e}")
def _predict_remaining_time(self, current_index, total_items, item_process_time):
"""
残り処理時間を予測
現在の進捗状況に基づいて、残りの処理時間と予想終了時刻を計算する。
:param int current_index: 現在の処理インデックス
:param int total_items: 総処理数
:param float item_process_time: 1項目あたりの処理時間
"""
remaining_items = total_items - (current_index + 1)
wait_minutes = self.config.config["wait_minutes"]
if current_index == 0:
# 初回の予測
estimated_total_time = item_process_time * total_items + (wait_minutes * 60 * (total_items - 1))
estimated_end_time = datetime.datetime.now() + datetime.timedelta(seconds=estimated_total_time)
print(f"\n予想終了時刻: {estimated_end_time.strftime('%Y-%m-%d %H:%M:%S')}")
else:
# 更新された予測
remaining_time = (item_process_time * remaining_items) + (wait_minutes * 60 * (remaining_items - 1))
updated_end_time = datetime.datetime.now() + datetime.timedelta(seconds=remaining_time)
print(f"更新された予想終了時刻: {updated_end_time.strftime('%Y-%m-%d %H:%M:%S')}")
def _show_runtime_info(self):
"""
実行時間情報の表示
スクレイピング処理全体の開始時刻、終了時刻、実行時間を表示する。
"""
run_time = time.time() - self.start_time
print(f'\n処理開始時刻: {datetime.datetime.fromtimestamp(self.start_time).strftime("%Y-%m-%d %H:%M:%S")}')
print(f'処理終了時刻: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
print('実行時間: {:.2f}秒 ({:.2f}分)'.format(run_time, run_time / 60))
8.7 prepare_raceResults.py (データ前処理)
# ==============================================================================
# 競馬レース結果データ前処理モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果データを機械学習や統計分析に適した形式に
# 整形するための前処理モジュール。スクレイピングで取得した生データから欠損値や不正確なデータを除去し、
# 数値型への変換や日付の標準化など分析に必要な一連の前処理を行います。
#
# プログラムの主な機能:
# 1. データの読み込みと検証
# - 複数年のレース結果CSVファイルの読み込み
# - ヘッダー行の有無の自動検出と処理
# - エンコーディング(Shift-JIS/cp932)の管理
# 2. データクリーニング
# - 欠損値を含む行の削除
# - 無効なデータ(「計不」など)の除去
# - カラムの型変換(文字列→数値、日付など)
# 3. 特徴量エンジニアリング
# - タイム表記(分:秒)を秒単位に変換
# - 馬体重の数値化(括弧内の増減表記の除去)
# - 日付のdatetime型への変換
#
# ==============================================================================
# 実行手順と前提条件
# ==============================================================================
# 1. 必要なライブラリのインストール:
# ```
# pip install pandas
# ```
#
# 2. 入力データの準備:
# - get_raceResults.pyを実行してレース結果データを取得
# - 各年のデータを年間統合ファイルで用意
# - 入力ファイルパス:
# ```
# ./data/raceresults/[年]/[年]_all_raceresults.csv
# ```
# - 準備例:
# ./data/raceresults/2019/2019_all_raceresults.csv
# ./data/raceresults/2020/2020_all_raceresults.csv
# ./data/raceresults/2021/2021_all_raceresults.csv
# ./data/raceresults/2022/2022_all_raceresults.csv
# ./data/raceresults/2023/2023_all_raceresults.csv
#
# 3. プログラムの実行:
# ```
# python prepare_raceResults.py
# ```
#
# ==============================================================================
# データ処理の詳細
# ==============================================================================
# 1. データ読み込みプロセス:
# - 年別フォルダをグロブで検索
# - 各年フォルダ内の[年]_all_raceresults.csvファイルを検出
# - ファイル先頭部分を読み込みヘッダー行の有無を確認
# - ヘッダー行がある場合は削除してカラム名を統一
#
# 2. データクリーニング処理:
# - 欠損値(NaN)を含む行を削除
# - 「計不」を含む馬体重データを持つ行を削除
# - 単勝オッズに「,」や「---」を含む行を削除
# - 日付を「%Y年%m月%d日」形式からdatetime型に変換
#
# 3. データ変換処理:
# - タイム列(「分:秒」形式)を秒単位の浮動小数点数に変換
# 例: "1:45.3" → 105.3秒
# - 馬体重から括弧内の増減表記を削除し数値のみを抽出
# 例: "480(+4)" → 480.0
# - 単勝オッズを文字列から浮動小数点数に変換
#
# ==============================================================================
# メソッドと機能の詳細
# ==============================================================================
# 1. RaceDataProcessor クラス:
# - __init__(): 入出力パスの設定とカラム名の定義
# - load_data(): 年別フォルダからCSVファイルを読み込み
# └─ ヘッダー検出とカラム名の標準化
# - transform_data(): タイム列を秒単位に変換
# └─ 「分:秒」形式を小数点表記の秒数に変換
# - clean_data(): データのクリーニング処理
# └─ 欠損値、無効データの除去と型変換
# - save_data(): 処理結果をCSVファイルとして保存
# - process(): メイン処理フローの実行
# └─ 読み込み→クリーニング→変換→保存の一連の流れ
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 本モジュールが依存するプログラム:
# - get_raceResults.py の出力結果
# └─ スクレイピングで取得した年間レース結果CSV
#
# 2. 本モジュールの出力を利用するプログラム:
# - 機械学習モデル構築プログラム
# └─ 前処理済みデータを学習・予測に使用
# - 統計分析プログラム
# └─ 加工済みデータを使用した分析・可視化
#
# ==============================================================================
# 出力データとファイル
# ==============================================================================
# 1. 前処理済みデータファイル:
# - 出力パス: ./data/raceresults/prepared_raceresults/
# - ファイル名: [開始年]_[終了年]_prepared_raceresults.csv
# 例: 2019_2023_prepared_raceresults.csv
#
# 2. データフォーマット:
# - エンコーディング: Shift-JIS (cp932)
# - 主要カラム:
# ├─ time: タイム(秒単位の浮動小数点数)
# ├─ horse: 馬名
# ├─ father: 父馬名
# ├─ mother: 母馬名
# ├─ age: 性齢
# ├─ rider_weight: 斤量(浮動小数点数)
# ├─ rider: 騎手名
# ├─ odds: 単勝オッズ(浮動小数点数)
# ├─ popular: 人気順位(整数)
# ├─ horse_weight: 馬体重(浮動小数点数)
# ├─ distance: レース距離(整数)
# ├─ weather: 天候
# ├─ ground: 馬場(芝/ダート)
# ├─ condition: 馬場状態
# ├─ date: レース日(datetime型)
# ├─ race_name: レース名
# ├─ location: 開催場所
# └─ round: ラウンド情報
#
# ==============================================================================
# 使用例
# ==============================================================================
# 1. 基本的な実行:
# ```python
# from prepare_raceResults import RaceDataProcessor
#
# # 入出力パスを指定して処理を実行
# processor = RaceDataProcessor(
# './data/raceresults',
# './data/raceresults/prepared_raceresults'
# )
# processor.process()
# ```
#
# 2. プログラムの直接実行:
# ```
# # コマンドラインから実行
# python prepare_raceResults.py
# ```
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. データ要件:
# - 入力CSVファイルはShift-JIS(cp932)エンコードである必要あり
# - ヘッダー行の有無は自動検出するが、カラム順序は固定
# - 複数年のデータは結合時に最新のデータが優先される
#
# 2. 処理上の注意:
# - 欠損値を含む行は全て削除されるため、データ量が減少する
# - 「計不」の馬体重データや無効なオッズを含む行も削除
# - 処理途中のエラーはコンソールに出力されるが処理は続行
#
# 3. 出力と保存:
# - 出力フォルダが存在しない場合は自動的に作成
# - 既存の同名ファイルは上書きされる
# - 処理の進行状況と実行時間はコンソールに表示
#
import pandas as pd
import os
import time
import glob
class RaceDataProcessor:
def __init__(self, base_folder_path, output_folder_path):
"""
入力フォルダパスと出力フォルダパスを設定し、レース結果のカラム名を定義する。
:param str base_folder_path: レース結果が保存されている基本フォルダのパス。
:param str output_folder_path: 加工後のレース結果を保存するフォルダのパス。
"""
self.base_folder_path = base_folder_path
self.output_folder_path = output_folder_path
self.columns = ['time', 'horse', 'father', 'mother', 'age', 'rider_weight', 'rider',
'odds', 'popular', 'horse_weight', 'distance', 'weather', 'ground',
'condition', 'date', 'race_name', 'location', 'round']
def load_data(self):
"""
指定されたフォルダ内の年別フォルダから全てのall_raceresults.csvファイルを読み込む。
:return: 読み込んだ各ファイルのDataFrameオブジェクトを要素とするリスト
:rtype: list
"""
race_results = []
# 全ての年フォルダを検索
year_folders = glob.glob(os.path.join(self.base_folder_path, '*'))
for year_folder in year_folders:
if os.path.isdir(year_folder):
# 各年フォルダ内の*_all_raceresults.csvファイルを探す
all_results_file = os.path.join(year_folder, f"{os.path.basename(year_folder)}_all_raceresults.csv")
if os.path.exists(all_results_file):
try:
# 最初の5行をチェックしてヘッダー行があるか確認
with open(all_results_file, 'r', encoding='cp932') as f:
first_lines = [f.readline() for _ in range(5)]
# ヘッダー行があるか確認
has_header = any('タイム' in line or '馬名' in line or '馬体重' in line for line in first_lines)
# ファイル全体を読み込み
df = pd.read_csv(all_results_file, header=None, names=self.columns, encoding='cp932')
# ヘッダー行を削除
if has_header:
print(f"{all_results_file} にヘッダー行が見つかりました。削除します。")
header_rows = df[
df['time'].astype(str).str.contains('タイム', na=False) |
df['horse'].astype(str).str.contains('馬名', na=False) |
df['horse_weight'].astype(str).str.contains('馬体重', na=False)
].index
df = df.drop(header_rows)
race_results.append(df)
print(f"ファイルを読み込みました: {all_results_file}")
except Exception as e:
print(f"ファイル読み込みエラー {all_results_file}: {str(e)}")
if not race_results:
print("警告: 読み込めるファイルが見つかりませんでした")
return race_results
@staticmethod
def transform_data(race_results):
"""
DataFrame内の'time'列のデータを変換する。
:param pd.DataFrame race_results: 変換を行うレース結果のDataFrame
:return: 'time'列のデータが秒単位に変換されたDataFrame
:rtype: pd.DataFrame
"""
race_results['time'] = race_results['time'].apply(
lambda x: float(x.split(':')[0]) * 60 + float(x.split(':')[1]))
return race_results
def save_data(self, race_result):
"""
レース結果を含むDataFrameをCSVファイルとして保存する。
:param pd.DataFrame race_result: 保存するレース結果のDataFrame
"""
start_year = race_result['date'].dt.year.min()
end_year = race_result['date'].dt.year.max()
csv_name = f"{start_year}_{end_year}_prepared_raceresults.csv"
csv_path = os.path.join(self.output_folder_path, csv_name)
race_result.to_csv(csv_path, encoding='cp932', index=False)
print(f"{csv_path}を作成しました。")
def process(self):
"""
レース結果データの処理を実行する。
"""
race_results = self.load_data()
if not race_results:
print("処理するデータがありません。")
return
combined_race_results = pd.DataFrame()
for i in range(len(race_results), 0, -1):
combined_race_results = pd.concat([combined_race_results, race_results[i - 1]], axis=0)
cleaned_race_results = self.clean_data(combined_race_results)
transformed_race_results = self.transform_data(cleaned_race_results)
self.save_data(transformed_race_results)
@staticmethod
def clean_data(race_result):
"""
レース結果データをクリーニングする。
:param pd.DataFrame race_result: クリーニングするレース結果のDataFrame
:return: クリーニング済みのDataFrame
:rtype: pd.DataFrame
"""
# 空を含む行を削除する
race_result.dropna(inplace=True)
# 'horse_wight'列に'計不'を含む行を削除し、float型に変換する
race_result = race_result[race_result['horse_weight'] != '計不']
race_result.loc[:, 'horse_weight'] = race_result['horse_weight'].str.split('(').str[0].astype(float)
# 'odds'列に','あるいは'---'を含む行を削除し、float型に変換する
race_result = race_result[~race_result['odds'].str.contains(',', na=False)]
race_result = race_result[race_result['odds'] != '---']
race_result['odds'] = race_result['odds'].astype(float)
# 'date'列をdatetime型に変換
race_result.insert(15, 'date_1', pd.to_datetime(race_result['date'], format='%Y年%m月%d日', errors='coerce'))
race_result.drop('date', axis=1, inplace=True)
race_result.rename(columns={'date_1': 'date'}, inplace=True)
# indexをリセットする
race_result.reset_index(drop=True, inplace=True)
return race_result
if __name__ == "__main__":
start_time = time.time()
base_folder_path = './data/raceresults'
output_folder_path = './data/raceresults/prepared_raceresults'
# 出力先フォルダの存在有無を確認し、存在しない場合はフォルダを作成する
if not os.path.exists(output_folder_path):
os.makedirs(output_folder_path)
processor = RaceDataProcessor(base_folder_path, output_folder_path)
processor.process()
print(f"実行時間:{time.time() - start_time:.1f}秒")
以上です。
コメント