- はじめに
- 1. システム概要
- 2. システムアーキテクチャ
- 3. モジュール設計
- 4. データベース設計
- 5. ファイル・ディレクトリ構成
- 6. 実行環境と運用(マルチプラットフォーム対応)
- 全コード
はじめに
Netkeiba.comの出走馬の血統情報を表示している箇所が、JavaScript/DOM表示に変更されました。そのため、従来のrequestsによるHTTPリクエストでは血統情報を取得できなくなりました。
JavaScript/DOM表示に対応するために、Seleniumによる血統情報を取得するように変更しました。
変更に合わせて、いくつかの改善も実施しています。
<主な変更点>
- 血統情報の取得方法をSeleniumに変更
- 取得する血統情報を2世代に増やす(自身の父母-自身の父母の父母)
- 出走馬の馬主・生産者やレースのコース方向、発走時刻などを取得
- ARM(RaspberryPI)に対応
一部の情報取得をSeleniumに変更したことで、今まで以上に情報取得に掛かる時間が増えました。
また、Netkeiba.comに対してのリクエスト要求も増えています。Netkeiba.comへの負荷が高まらないよう、プログラム実行時のパラメータには注意する必要があります。
1. システム概要
1.1 システムの目的と概要
1.1.1 システム開発の背景
従来の競馬レース結果収集システムでは、以下のような運用上の課題が存在していました。
<主要な問題点>
- 中断時のデータ消失
netkeiba.comのWeb閲覧制限に掛かった場合、プログラムを強制終了する必要があり、メモリ上に保存されていた取得済みレース結果がすべて消失 - 重複処理による非効率
取得済みレース結果の記録機能がないため、再実行時に同じデータを再取得する必要 - 血統情報の体裁変更による情報取得不可
netkeiba.comがJavaScript/DOM表示に変更されたため、従来のHTTPリクエストでは血統情報が取得不可能 - アーキテクチャ制限
特定環境でのみ動作し、ARM/x86_64の両対応が困難 - サイト負荷
不要な重複アクセスによりnetkeiba.comに過度な負荷をかけるリスク
1.1.2 改修による解決内容
本システムでは、上記課題を解決するために以下の機能を実装しました。
<主要改善点>
- Seleniumベース血統情報取得
- netkeiba.comの血統情報がJavaScript/DOM表示に変更されたため、requestsでは情報取得が不可能
- Seleniumを使用することで、JavaScriptによる動的表示に対応
- 血統テーブルの読み込み完了を待機してから情報を抽出
- 詳細データ取得
- 基本レース情報(コース方向・発走時刻含む)
- 詳細血統情報(2世代:父、母、 父の父、父の母、母の父、母の母)
- 馬詳細情報(馬主、生年月日、生産者、産地)
- 出走馬情報(枠番、馬番、調教師情報)
- マルチアーキテクチャ対応
- プラットフォーム自動検出機能
- ChromeDriver自動管理(x86_64)とシステム利用(ARM)
- 環境固有の最適化設定
- ハイブリッド処理
- レース基本情報: requests(高速取得)
- 血統・馬詳細情報: Selenium(JavaScript対応による確実取得)
- 強化された状態管理
- 動的テーブル管理による後方互換性
- FileLockによる同時実行制御
- バッチ処理による負荷分散
- 多層エラーハンドリング
- 各レベルでのリトライ機能と回復処理
1.1.3 netkeiba.com仕様変更への対応
<血統情報表示の変更>
- 変更前
静的HTML表示(requestsで取得可能) - 変更後
JavaScript/DOM表示(動的生成) - 対応策
Seleniumによる血統テーブル読み込み待機と情報抽出
1.1.4 システムの目的
本システムは、JRA(日本中央競馬会)のレース結果を効率的かつ安全に収集し、分析用データとして整備することを目的とします。
- JavaScript対応データ収集
動的表示される血統情報の確実な取得 - マルチプラットフォーム対応
ARM/x86_64環境での安定動作 - 効率的処理
ハイブリッド処理とバッチ処理による最適化 - データ永続化
テーブル構造での安全な保存と管理 - 再開可能な処理
予期しない中断からの確実な復旧
1.2 対象サイトと取得方式
- 対象URL
https://db.netkeiba.com - データソース
JRA競馬データベース - 取得対象
芝・ダート両方のレース結果 - アクセス方式
Selenium + HTTP Request - 血統情報取得
Selenium必須(JavaScript/DOM表示対応)
1.3 取得データの種類と形式
1.3.1 取得データの種類(31項目)
- time – タイム
- horse – 馬名
- frame_number – 枠番
- horse_number – 馬番
- father – 父親
- mother – 母親
- father_father – 父の父
- father_mother – 父の母
- mother_father – 母の父
- mother_mother – 母の母
- age – 性齢
- rider_weight – 斤量
- rider – 騎手
- trainer – 調教師
- owner – 馬主
- birth_date – 生年月日
- breeder – 生産者
- birthplace – 産地
- odds – 単勝
- popular – 人気
- horse_weight – 馬体重
- distance – 距離
- direction – コース方向
- start_time – 発走時刻
- weather – 天候
- ground – 馬場
- condition – 状態
- date – 開催日
- race_name – レース名
- location – 開催場所
- round – ラウンド
1.3.2 出力形式
- データベース
SQLite形式(race_results.db)- 動的テーブル管理対応 - CSVファイル
月別・年間統合ファイル(Shift-JIS、31カラム構成) - 管理ファイル
JSON形式(処理状況・リンク情報記録)
1.4 システム構成要素
- Webスクレイピング
Selenium + BeautifulSoup + requests - 血統情報取得
Selenium(JavaScript/DOM表示対応)
データ保存
SQLite データベース - ファイル出力
CSV形式での詳細結果出力
進捗管理
処理状況追跡と管理 - アーキテクチャ対応
ARM/x86_64自動検出と最適化
2. システムアーキテクチャ
2.1 全体アーキテクチャ図
2.2 血統情報取得の仕様変更対応
2.2.1 netkeiba.com仕様変更の詳細
<変更内容>
- 血統情報表示がJavaScript/DOMベースに変更
- 静的HTMLからの血統情報取得が不可能
- ページ読み込み後にJavaScriptで血統テーブルを動的生成
<技術的影響>
変更前: HTTP Request → 静的HTML → BeautifulSoup解析
変更後: Selenium → JavaScript実行待機 → DOM解析 → BeautifulSoup解析
2.2.2 対応方針
<Seleniumによる血統情報取得>
- ブラウザでページにアクセス
- blood_tableクラスの読み込み完了を待機
- JavaScriptの実行完了を確認
- DOM構造から血統情報を抽出
2.3 データフロー図
3. モジュール設計
3.1 get_raceResults.py(メイン実行)
3.1.1 概要
システム全体の起動・制御を担当するメインモジュール。コマンドライン引数の解析、全体フロー制御、各管理モジュールの初期化と連携、エラーハンドリングと例外処理を提供します。
3.1.2 主要関数
関数名 | 引数 | 戻り値 | 説明 |
parse_arguments() | なし | arugparse.Namespace | コマンドライン引数の解析と検証(年、月、バッチサイズ、設定ファイル等) |
show_race_links_info() | ConfigManager, int, int | bool | 指定年月のレースリンク情報表示(–show_linksオプション) |
main() | なし | int | メイン処理の実行とステータスコード返却 |
3.2 config_manager.py(設定管理・マルチアーキテクチャ対応)
3.2.1 概要
アプリケーション全体の設定を一元管理。マルチアーキテクチャ対応、設定ファイルの読み込み、パス管理と自動生成機能、期間検証と年月ペア生成を提供します。
3.2.2 主要クラス:ConfigManager
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
__init__(config_file_path=None) | コンストラクタ | str(optional) | None | 設定管理の初期化、マルチアーキテクチャ対応設定 |
get_db_path() | パブリック | なし | str | データベースファイルの完全パス取得 |
get_csv_path(year, month) | パブリック | int/str, int/str | str | 31カラム対応月別CSVファイルパス取得 |
get_combined_csv_path(year) | パブリック | int/str | str | 年間統合CSVファイルパス取得 |
validate_scraping_period() | パブリック | int, int, int | bool | スクレイピング期間の妥当性検証 |
generate_year_month_pairs() | パブリック | int, int, int | list | 期間内の年月ペア生成 |
3.3 database_manager.py(データベース管理・動的スキーマ)
3.3.1 概要
SQLiteデータベースの操作管理。動的テーブル管理による後方互換性、詳細データの保存・取得、処理済みURL管理による重複処理防止を提供します。
3.3.2 主要クラス:DatabaseManager
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
__init__(db_path) | コンストラクタ | str | None | データベース管理の初期化と動的テーブル作成 |
_add_race_columns_if_not_exist() | プライベート | cursor | None | racesテーブルの動的カラム追加 |
_add_horse_columns_if_not_exist() | プライベート | cursor | None | race_horsesテーブルの動的カラム追加 |
save_race_result() | パブリック | str, DataFrame, int, int, list | bool | レース結果のデータベース保存 |
get_monthly_race_results() | パブリック | int, int/str | DataFrame | 指定月の31カラム結果取得 |
get_unprocessed_urls() | パブリック | list | list | 未処理URLの抽出 |
3.4 scraper_manager.py(統括管理・ハイブリッド処理)
3.4.1 概要
スクレイピング処理全体の制御と進捗管理。ハイブリッド処理(Selenium + requests)、血統情報のSelenium必須対応、バッチ処理による負荷分散、データ統合と出力管理、実行時間予測と状況表示を提供します。
3.4.2 主要クラス:ScraperManager
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
__init__() | コンストラクタ | ConfigManager, DatabaseManager | None | ハイブリッド処理の初期化 |
scrape_race_details() | パブリック | int/str, int/str | tuple | 指定年月のレース詳細取得 |
_scrape_single_race_extended() | プライベート | str | tuple | 単一レースの情報取得 |
_get_horse_pedigree_extended() | プライベート | BeautifulSoup | tuple | Seleniumによる2世代血統情報取得★ |
_get_horse_details_extended() | プライベート | BeautifulSoup,list | list | Seleniumによる馬詳細情報取得★ |
_extract_race_info() | プライベート | BeautifulSoup | dict | レース基本情報の抽出 |
combine_race_result() | パブリック | int | DataFrame | 年間レース結果の統合(31カラム) |
run_scraping() | パブリック | int, int, int | None | ハイブリッドスクレイピング処理の全体実行 |
★ = JavaScript/DOM表示対応のためSelenium必須
3.4.3 ハイブリッド処理フロー
<処理分担の明確化>
- 基本情報取得
requestsでレース基本情報・結果テーブル - 血統・詳細情報取得
Selenium(JavaScript/DOM対応)で血統情報・馬詳細情報 - データ統合
基本情報 + 血統・詳細情報 → 31カラム構成 - バッチ処理
40URL単位(デフォルト)での負荷分散処理 - 待機制御
バッチ間10分待機(デフォルト)による負荷軽減
3.5 web_manager.py(Webアクセス管理・ARM/x86_64対応)
3.5.1 概要
Seleniumによるブラウザ操作とHTTP通信の管理。ARM/x86_64アーキテクチャの自動対応、血統情報取得でのJavaScript/DOM対応、FileLockによる同時実行制御、ハイブリッド処理の実装、強化されたエラーハンドリングとリトライ機能を提供します。
3.5.2 主要クラス:WebDriverHandler(ARM/x86_64対応・血統情報JavaScript対応)
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
__init__() | コンストラクタ | ConfigManager | None | マルチアーキテクチャ対応ブラウザ管理の初期化 |
initialize_browser() | パブリック | なし | webdriver.Chrome/None | ARM/x86_64自動検出によるChromeブラウザ初期化 |
search_race() | パブリック | int/str, int/str | None | リトライ機能付きレース検索実行 |
collect_race_links() | パブリック | なし | list | 全ページ対応レースリンク収集 |
get_html_selenium() | パブリック | str, int | BeautifulSoup | Selenium経由HTMLコンテンツ取得★ |
get_horse_pedigree_batch() | パブリック | list, int | tuple | 血統情報のバッチ処理取得★ |
get_horse_details_batch() | パブリック | list, int | list | 馬詳細情報のバッチ処理取得★ |
★ = JavaScript/DOM表示対応
3.5.3 主要クラス:RequestManager(セッション管理)
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
__init__() | コンストラクタ |
ConfigManager | None | セッション管理HTTPクライアントの初期化 |
get_html() | パブリック | str, int, int | BeautifulSoup/None | リトライ機能付きHTMLコンテンツ取得 |
retry_request() | パブリック | str, int, int | requests.Response/None | リトライ処理 |
3.6 content_parsers.py(HTML解析・血統情報JavaScript対応)
3.6.1 概要
BeautifulSoupを使用したDOM解析。レース情報の構造化データ変換、JavaScript/DOM表示対応による6世代血統情報の抽出と整理、馬詳細情報(馬主・生産者等)の取得、エラーハンドリングによる堅牢性確保を提供します。
3.6.2 主要クラス:HorsePedigreeParser(血統情報解析・JavaScript/DOM対応)
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
get_horse_links() | 静的 | BeautifulSoup, str | list | 出走馬詳細ページリンク抽出 |
get_horse_detailed_pedigree() | 静的 | BeautifulSoup | tuple | JavaScript/DOM対応6世代血統情報抽出★ |
get_horse_father_name() | 静的 | BeautifulSoup | str | JavaScript/DOM対応父馬名抽出★ |
get_horse_mother_name() | 静的 | BeautifulSoup | str | JavaScript/DOM対応母馬名抽出★ |
★ = JavaScript/DOM表示対応(Seleniumで取得したHTMLから解析)
3.7 prepare_raceResults.py(データ前処理・31カラム対応)
3.7.1 概要
機械学習向けデータ前処理。31カラムデータのクリーニング、特徴量エンジニアリング、分析用データの整形と保存を提供します。
3.7.2 主要クラス:RaceDataProcessor
メソッド名 | 種別 | 引数 | 戻り値 | 説明 |
__init__() | コンストラクタ | str, str | None | 31カラム対応データ処理の初期化 |
パブリック | なし | list | 年別フォルダからCSVファイル読み込み | |
静的 | DataFrame | DataFrame | データの数値変換(枠番・馬番・発走時刻等) | |
パブリック | DataFrame | DataFrame | データクリーニングと型変換 | |
パブリック | DataFrame | None | 31カラム前処理済みデータのCSV保存 | |
パブリック | なし | None | データ前処理の全体実行フロー |
3.7.3 カラム構成(31カラム)
columns = [
'time', 'horse', 'frame_number', 'horse_number', 'father', 'mother',
'father_father', 'father_mother', 'mother_father', 'mother_mother',
'age', 'rider_weight', 'rider', 'trainer', 'owner', 'birth_date',
'breeder', 'birthplace', 'odds', 'popular', 'horse_weight', 'distance',
'direction', 'start_time', 'weather', 'ground', 'condition',
'date', 'race_name', 'location', 'round'
]
4. データベース設計
4.1 ER図
4.2 血統情報カラムのJavaScript/DOM対応
<血統情報カラム(Selenium必須)>
- father: 父馬名
- mother: 母馬名
- father_father: 父の父
- father_mother: 父の母
- mother_father: 母の父
- mother_mother: 母の母
netkeiba.comの血統情報がJavaScript/DOM表示に変更されたため、これらの情報はSeleniumでJavaScript実行後のDOM構造から取得する必要があります。
5. ファイル・ディレクトリ構成
5.1 プロジェクト構造
horse_racing_scraper/
├── get_raceResults.py # メイン実行ファイル
├── config_manager.py # 設定管理(マルチアーキテクチャ対応)
├── web_manager.py # Web操作管理(ARM/x86_64対応)
│ ├── WebDriverHandler # Selenium処理(血統・詳細情報)
│ └── RequestManager # HTTP処理(基本情報)
├── content_parsers.py # HTML解析(31カラムデータ対応)
│ ├── RaceInfoParser # レース基本情報(コース方向・発走時刻含む)
│ ├── RaceResultsParser # レース結果(枠番・馬番・調教師含む)
│ ├── HorsePedigreeParser # 2世代血統情報
│ └── HorseDetailParser # 馬詳細情報(馬主・生産者等)
├── database_manager.py # データベース管理(動的スキーマ)
├── scraper_manager.py # 統括管理(ハイブリッド処理)
├── prepare_raceResults.py # データ前処理(31カラム対応)
├── config.json # 設定ファイル(オプション)
├── requirements.txt # 依存パッケージ
├── README.md # 使用方法
└── data/ # データディレクトリ
└── raceresults/
├── race_results.db # SQLiteデータベース(動的スキーマ)
├── 2023/ # 年別フォルダ
│ ├── 2023_1_raceresults.csv # 31カラム月別データ
│ ├── 2023_2_raceresults.csv
│ ├── ...
│ ├── 2023_all_raceresults.csv # 31カラム年間統合データ
│ ├── 2023_1_racelinks.json # レースリンク管理
│ └── ...
├── 2024/ # 年別フォルダ
└── prepared_raceresults/ # 前処理済みデータ
└── 2019_2023_prepared_raceresults_extended.csv
5.2 データファイル構成
5.2.1 データベースファイル(動的スキーマ対応)
- パス
./data/raceresults/race_results.db - 形式
SQLite3データベース - エンコーディング
UTF-8 - 特徴
既存テーブルへの自動カラム追加機能
5.2.2 CSVファイル(31カラム構成)
- 月別
./data/raceresults/[年]/[年]_[月]_raceresults.csv
年間統合
./data/raceresults/[年]/[年]_all_raceresults.csv
前処理済み
./data/raceresults/prepared_raceresults/[開始年]_[終了年]_prepared_raceresults_extended.csv - エンコーディング
Shift-JIS (cp932) - 構成
31カラム(基本情報 + 詳細血統 + 馬詳細情報)
6. 実行環境と運用(マルチプラットフォーム対応)
6.1 血統情報取得の要件
6.1.1 JavaScript/DOM対応要件
- 必須ツール
Selenium WebDriver - 対象ブラウザ
Chrome/Chromium
必要機能
JavaScript実行環境
待機機能
DOM要素読み込み完了待機
6.2 環境要件
6.2.1 ARM環境(Raspberry Pi等)
# システムパッケージのインストール
sudo apt update
sudo apt install chromium chromium-driver
# Pythonパッケージのインストール
pip install -r requirements_keiba_ai.txt
6.2.2 x86_64環境(Windows/Mac/Linux)
# Google Chromeのインストール
# 自動ChromeDriver管理対応
# Pythonパッケージのインストール
pip install -r requirements_keiba_ai.txt
6.3 実行例
6.3.1 基本実行(血統情報含む)
# 2024年全年データ取得(血統情報はSeleniumで取得)
# 一度に取得するレース情報数をbatch_sizeで設定
# レース情報取得後の、次回実行までの待ち時間をwait_minutesで設定
python get_raceResults.py --year 2024 --start_month 1 --end_month 12 --batch_size 10 --wait_minutes 15
# 血統情報取得のため処理時間が従来より長くなります
# 1レースあたり15-20秒程度(血統・馬詳細情報含む)
6.4 出力データ構成(31カラム)
6.4.1 CSVカラム構成
タイム, 馬名, 枠番, 馬番, 父親, 母親, 父の父, 父の母, 母の父, 母の母,
性齢, 斤量, 騎手, 調教師, 馬主, 生年月日, 生産者, 産地,
単勝, 人気, 馬体重, 距離, コース方向, 発走時刻, 天候, 馬場, 状態,
開催日, レース名, 開催場所, ラウンド
全コード
get_raceResults.py (メイン実行ファイル)
# ==============================================================================
# 競馬レース結果スクレイピングプログラム
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果をネット競馬から自動的に取得し、構造化されたデータとして保存するスクレイピングプログラム。
# 指定された期間のレース結果を効率的に収集し、レース基本情報、出走馬詳細情報、2世代血統情報、馬の基本情報を含む
# 分析に適したデータ形式で整理します。ARM/x86_64アーキテクチャの自動対応とバッチ処理による安定性を実現しています。
#
# プログラムの主な機能:
# 1. レース結果の自動収集
# - 指定した年月のレース結果ページへのアクセス
# - リトライ機能による安定した検索実行
# - 進捗表示と残り時間の予測
# - バッチ処理による効率的なデータ取得
# 2. 詳細データの構造化と保存
# - レース基本情報(コース方向、発走時刻を含む)の抽出
# - 出走馬情報(枠番、馬番、調教師を含む)の取得
# - 2世代血統情報(父の父、父の母、母の父、母の母)の収集
# - 馬基本情報(馬主、生年月日、生産者、産地)の取得
# - SQLiteデータベースへの格納と動的テーブル管理
# - 月別・年間CSVファイルの作成
# 3. 状態管理と再開機能
# - 処理済みURLと未処理URLの管理
# - 失敗したスクレイピングの記録と再開
# - JSONファイルによる処理状況の保存
# - 中断・再開可能な設計による堅牢性
# 4. 実行環境対応
# - ARM/x86_64アーキテクチャの自動検出
# - プラットフォーム固有のChromeDriver管理
# - FileLockによる重複実行防止
# - 安定したブラウザ操作とHTTP通信
#
# ==============================================================================
# 実行手順
# ==============================================================================
# 1. 必要なライブラリのインストール:
# pip install pandas numpy requests beautifulsoup4 selenium
# pip install tqdm filelock webdriver_manager lxml
#
# 2. 環境固有の準備:
# 【ARM環境(Raspberry Pi等)】
# sudo apt update
# sudo apt install chromium chromium-driver
#
# 【x86_64環境】
# Google Chrome のインストール(自動ChromeDriver管理)
#
# 3. 設定値の確認:
# - データ保存先ディレクトリ: ./data/raceresults
# - 対象ウェブサイト: https://db.netkeiba.com
# - バッチサイズ: 40(一度に処理するURL数)
# - 待機時間: 10分(バッチ間の待機時間)
#
# 4. 実行方法:
# python get_raceResults.py [--year YEAR] [--start_month START] [--end_month END]
# [--batch_size BATCH] [--wait_minutes WAIT]
# [--config CONFIG] [--show_links]
#
# 5. 処理の流れ:
# 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を使用したブラウザ操作(ARM/x86_64対応)
# ├─ リトライ機能付きレース検索とリンク収集
# ├─ バッチ処理による血統情報・馬詳細情報の取得
# └─ HTTP通信とHTMLの取得(セッション管理)
#
# - content_parsers.py
# └─ HTML解析処理を担当
# ├─ レース基本情報の抽出(コース方向、発走時刻を含む)
# ├─ レース結果テーブルの解析(枠番、馬番、調教師を含む)
# ├─ 3世代血統情報の抽出(父の父、父の母、母の父、母の母)
# └─ 馬詳細情報の抽出(馬主、生年月日、生産者、産地)
#
# - database_manager.py
# └─ データベース操作を担当
# ├─ 動的テーブル初期化とカラム追加機能
# ├─ 詳細レース結果の保存と取得
# ├─ 処理済みURLの管理
# └─ 月別データの効率的な検索
#
# - scraper_manager.py
# └─ スクレイピング処理全体の制御を担当
# ├─ ハイブリッド処理(Selenium + requests)による効率化
# ├─ バッチ処理と進捗管理
# ├─ 待機時間制御による負荷分散
# ├─ 詳細血統情報・馬基本情報の統合処理
# └─ CSV出力と年間データ統合
#
# 2. 後続プログラム:
# - prepare_raceResults.py
# └─ 収集したレース結果データの前処理
# ├─ 欠損値の処理
# ├─ タイム(分:秒)の秒数変換
# └─ 分析用データの整形と保存
#
# ==============================================================================
# データ取得の詳細
# ==============================================================================
# 1. レース基本情報:
# - レース名、開催日、開催場所、ラウンド情報
# - 距離、コース方向(右/左)、発走時刻
# - 天候、馬場状態(芝/ダート、良/稍重など)
#
# 2. 出走馬結果情報:
# - 着順、枠番、馬番、馬名、性齢
# - 斤量、騎手、調教師、タイム
# - 単勝オッズ、人気順位、馬体重
#
# 3. 3世代血統情報:
# - 父馬、母馬(従来の2世代)
# - 父の父、父の母、母の父、母の母(3世代詳細)
# - 血統テーブルからの確実な情報抽出
#
# 4. 馬基本情報:
# - 馬主、生年月日、生産者、産地
# - 馬プロフィールページからの詳細取得
# - エラー時の適切なフォールバック処理
#
# ==============================================================================
# 処理効率化と安定性
# ==============================================================================
# 1. アーキテクチャ対応:
# - ARM(Raspberry Pi等)とx86_64の自動検出
# - プラットフォーム固有のChromeDriver管理
# - ヘッドレスモードによるリソース効率化
#
# 2. バッチ処理による最適化:
# - 指定サイズでのURL分割処理
# - バッチ間の適切な待機時間確保
# - 個別失敗時の全体処理継続
#
# 3. リトライ機能と堅牢性:
# - ネットワークエラー時の自動再試行
# - ブラウザ操作失敗時の再初期化
# - レート制限への適切な対応
#
# 4. 効率的なデータ処理:
# - ハイブリッド処理(Selenium + requests)
# - セッション管理による通信最適化
# - 重複処理の確実な回避
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. 実行環境:
# - Python 3.10以上
# - Chrome/Chromiumブラウザ(環境に応じた自動選択)
# - ChromeDriver(ARM環境では手動インストール、x86_64では自動管理)
# - インターネット接続(安定した接続推奨)
#
# 2. 実行時の注意:
# - 長時間の実行が予想されるため、電源管理に注意
# - 詳細情報取得により従来より処理時間が長くなる
# - ネットワーク接続が安定していることを確認
# - サーバー負荷軽減のため適切な待機時間を設定
# - 処理に失敗したURLは自動的に記録され再実行時にスキップ
#
# 3. データ要件:
# - 通常一年分の処理で約15-25MBのデータベースファイルが生成
# - 詳細情報を含むため従来より大きなファイルサイズ
# - CSVファイルはShift-JIS(cp932)エンコーディングで保存
# - 合計で年間あたり30-50MB程度のディスク容量が必要
#
# 4. 再実行時のオプション:
# - --show_links オプションで既存のレースリンク情報のみを表示
# - 実行済み期間の再スクレイピングは未処理のURLのみ対象
# - データベースの動的カラム追加により旧データとの互換性維持
#
# 5. パフォーマンス最適化:
# - ARM環境では特にヘッドレスモードの使用を推奨
# - バッチサイズは環境に応じて調整可能
# - 血統・馬詳細情報の取得は時間がかかるため適切な待機時間を設定
# - FileLockにより複数プロセス同時実行を自動的に防止
import argparse
import sys
import traceback
from config_manager import ConfigManager
from database_manager import DatabaseManager
from scraper_manager import ScraperManager
def parse_arguments():
"""
コマンドライン引数を解析し、スクレイピング実行時のパラメータを取得する。
argparseライブラリを使用してコマンドライン引数をパースし、年、開始月、終了月、
バッチサイズ、待機時間、設定ファイルパス、リンク表示オプションなどの
実行パラメータを解析する。各引数にはヘルプメッセージが設定されており、
--helpオプションで使用方法を確認できる。
:return: 解析されたコマンドライン引数のNamespaceオブジェクト。
year, start_month, end_month, batch_size, wait_minutes, config, show_linksの属性を含む
: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ファイル([年]_[月]_racelinks.json)から情報を読み込み、
スクレイピング日時、取得したレースリンク数、未処理リンク数、全リンク一覧、
未処理リンク一覧をコンソールに見やすい形式で表示する。JSONファイルが
存在しない場合はエラーメッセージを表示し、処理を終了する。
:param config: 設定管理オブジェクト。ファイルパス生成に使用される
:type config: ConfigManager
:param year: 表示対象の年。JSONファイル名の生成とディレクトリ特定に使用
:type year: int
:param month: 表示対象の月。JSONファイル名の生成に使用
:type month: int
:return: 情報表示が成功した場合はTrue、JSONファイルが見つからない場合や
読み込みエラーが発生した場合はFalse
: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():
"""
プログラム全体のメイン処理を実行し、スクレイピングプロセスを制御する。
コマンドライン引数の解析から設定の初期化、データベースマネージャーの初期化、
スクレイピングマネージャーの実行まで一連の処理フローを管理する。
レースリンク表示モードとスクレイピング実行モードの分岐処理を行い、
エラー発生時は適切なスタックトレースと共にエラーメッセージを出力する。
処理の流れ:
1. コマンドライン引数の解析とデフォルト値の設定
2. 設定管理オブジェクトの初期化と引数による上書き
3. スクレイピング期間の妥当性検証
4. リンク表示モードまたはスクレイピング実行モードの実行
5. データベース初期化とスクレイピングマネージャーによる処理実行
: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())
config_manager.py (設定管理)
# ==============================================================================
# 競馬レース結果スクレイピング設定管理モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果をスクレイピングするアプリケーション全体の設定を一元管理するモジュール。
# 設定ファイルの読み込み、デフォルト値の提供、パス管理、入力値の検証など、スクレイピングの基盤となる設定管理機能を提供します。
# 詳細血統情報、馬基本情報、ARM/x86_64アーキテクチャ対応、バッチ処理など、システム全体の機能向上をサポートします。
#
# プログラムの主な機能:
# 1. 設定の読み込みと管理
# - JSONファイルからの設定の読み込み
# - デフォルト設定値の提供
# - コマンドライン引数による設定の上書き
# - 設定値の検証と整合性の確保
# 2. パス管理と自動生成
# - データベースファイルパスの生成
# - 年別・月別CSVファイルパスの生成
# - 必要なディレクトリ構造の自動作成
# - 詳細データ対応のファイル構造管理
# 3. 入力検証と期間管理
# - 指定された年月が有効かの検証
# - 未来日付や無効な期間範囲のチェック
# - 年月ペアの生成とバリデーション
# 4. システム設定の統合管理
# - 複数のユーザーエージェントを提供
# - バッチ処理サイズと待機時間の管理
# - ARM/x86_64環境に対応したブラウザ設定
# - ハイブリッド処理(Selenium + requests)の設定管理
#
# ==============================================================================
# 設定ファイル形式と詳細データ対応
# ==============================================================================
# 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: データの保存先ディレクトリ(詳細データベース・CSV対応)
# - use_headless_browser: ヘッドレスブラウザを使用するかのフラグ(ARM環境対応)
# - 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ファイルの完全パス(詳細情報統合)
#
# 3. 管理対象データの種類:
# - レース基本情報: レース名、開催日、場所、距離、コース方向、発走時刻、天候、馬場状態
# - 出走馬詳細情報: 馬名、枠番、馬番、騎手、調教師、オッズ、人気、馬体重、タイム
# - 血統詳細情報: 父、母、父の父、父の母、母の父、母の母(3世代血統)
# - 馬基本情報: 馬主、生年月日、生産者、産地
#
# ==============================================================================
# プログラム間の関係と機能統合
# ==============================================================================
# 1. 本モジュールを呼び出すプログラム:
# - get_raceResults.py (メイン実行モジュール)
# └─ コマンドライン引数の解析と設定の初期化
# └─ ARM/x86_64環境対応の設定管理
# - scraper_manager.py (スクレイピング統括モジュール)
# └─ スクレイピング設定の取得とパス管理
# └─ 詳細血統情報・馬基本情報取得の設定管理
# - web_manager.py (ウェブアクセスモジュール)
# └─ ブラウザ設定とユーザーエージェント取得
# └─ ARM/x86_64アーキテクチャ対応設定
# └─ バッチ処理設定の提供
# - database_manager.py (データベース管理モジュール)
# └─ データベースパスの取得
# └─ 詳細テーブル構造対応の設定提供
#
# 2. システム機能の統合サポート:
# - ハイブリッド処理: Selenium(血統・馬詳細取得) + requests(基本情報取得)の設定管理
# - バッチ処理: 血統情報・馬詳細情報の効率的取得のための設定提供
# - マルチアーキテクチャ: ARM/x86_64環境での適切な設定値提供
# - 動的テーブル管理: データベース構造の柔軟な管理のための設定
#
# ==============================================================================
# 注意事項と運用指針
# ==============================================================================
# 1. 設定ファイルの使用:
# - 設定ファイルは任意のパスに配置可能
# - 設定ファイルが存在しない場合は自動的にデフォルト値が使用される
# - コマンドライン引数で明示的に指定された値が最優先される
# - 詳細データ取得に適したデフォルト値を提供
#
# 2. 期間検証の動作:
# - 未来の年月は指定できない (現在の日付より先の期間)
# - 開始月は終了月より後にできない (論理的整合性の確保)
# - システムの現在日付を基準に自動的に検証
# - 詳細データ取得処理時間を考慮した期間指定推奨
#
# 3. ディレクトリ管理:
# - 必要なディレクトリは自動的に作成される
# - 既存のディレクトリは上書きされない (exist_ok=True)
# - パス生成は非OS依存 (os.path.joinを使用)
# - 詳細データ対応のファイル構造を自動生成
#
# 4. パフォーマンス最適化設定:
# - バッチサイズ: 詳細情報取得処理に適した値(デフォルト40)
# - 待機時間: サーバー負荷軽減とデータ品質確保のバランス
# - ユーザーエージェント: 複数エージェントによる自然なアクセスパターン
# - ヘッドレスモード: ARM環境での効率的なブラウザ操作
#
# 5. データ品質管理サポート:
# - 設定値の妥当性検証による安定したデータ取得
# - 適切なファイルパス生成による確実なデータ保存
# - 期間検証による不正なデータ取得の防止
# - 環境対応設定による安定したシステム動作
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):
"""
指定されたパスの設定ファイルを読み込み、デフォルト設定を上書きする。
JSONファイルから設定情報を読み込み、デフォルト設定値に対してユーザー指定の
設定値をマージする。ファイルが存在しないか読み込みエラーが発生した場合は
エラーメッセージを出力し、デフォルト設定を継続使用する。
:param config_file_path: 読み込み対象の設定ファイルの完全パス
:type config_file_path: str
:return: なし
:rtype: None
"""
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):
"""
データ保存用ディレクトリの存在を確認し、存在しない場合は自動的に作成する。
設定ファイルで指定されたdata_dirパスに基づいてディレクトリ構造を確保し、
後続のデータベースファイルやCSVファイルの保存処理が確実に実行できるよう
準備する。既存ディレクトリの場合は何も行わない。
:return: なし
:rtype: None
"""
os.makedirs(self.config["data_dir"], exist_ok=True)
def get_db_path(self):
"""
SQLiteデータベースファイルの完全パスを生成して返す。
設定で指定されたデータディレクトリパスとデータベースファイル名
(race_results.db)を結合し、データベース管理モジュールで使用される
完全パスを提供する。レース基本情報、出走馬詳細情報、血統情報、
処理済みURL管理テーブルを含むデータベースファイルへのアクセスパス。
:return: データベースファイルの完全パス
:rtype: str
"""
return os.path.join(self.config["data_dir"], "race_results.db")
def get_csv_dir_for_year(self, year):
"""
指定年の年別CSVディレクトリパスを取得し、必要に応じてディレクトリを作成する。
データベースディレクトリ配下に年別サブディレクトリを作成し、月別CSVファイル、
年間統合CSVファイル、レースリンク管理JSONファイルの保存場所を提供する。
ディレクトリが存在しない場合は自動的に作成される。
:param year: 対象年(4桁の西暦年または文字列)
: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ファイルの完全パスを生成して返す。
年別ディレクトリ内に保存される月別レース結果CSVファイルのパスを生成し、
ファイル名は「[年]_[月]_raceresults.csv」形式で構成される。データベースから
取得した詳細レース情報(血統情報、馬基本情報を含む)の月別保存先として使用される。
:param year: 対象年(4桁の西暦年または文字列)
:type year: int or str
:param month: 対象月(1-12の数値または文字列)
: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ファイルの完全パスを生成して返す。
年別ディレクトリ内に保存される年間レース結果統合CSVファイルのパスを生成し、
ファイル名は「[年]_all_raceresults.csv」形式で構成される。月別CSVファイルを
統合した年間データとして使用され、データ分析や機械学習の入力データとしても活用される。
:param year: 対象年(4桁の西暦年または文字列)
: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: 検証対象の年(4桁の西暦年)
:type year: int
:param start_month: 検証対象の開始月(1-12)
:type start_month: int
:param end_month: 検証対象の終了月(1-12)
: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: 対象年(4桁の西暦年)
:type year: int
:param start_month: 処理開始月(1-12)
:type start_month: int
:param end_month: 処理終了月(1-12)
:type end_month: int
:return: (年, 月文字列)のタプルで構成されたリスト
:rtype: list
:raises ValueError: validate_scraping_period()で検証エラーが発生した場合
"""
self.validate_scraping_period(year, start_month, end_month)
return [(year, str(month)) for month in range(start_month, end_month + 1)]
web_manager.py (Web操作管理)
# ==============================================================================
# 競馬レース結果スクレイピングWebアクセス管理モジュール(拡張版)
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果を効率的かつ安全にスクレイピングするためのWebアクセス管理モジュール。
# Seleniumを使用したブラウザ操作と一般的なHTTPリクエストの両方をサポートし、サイトへの負荷を軽減しながら必要なデータを
# 収集するための機能を提供します。拡張版では、ARM/x86_64アーキテクチャの自動対応、バッチ処理機能、
# 強化されたエラーハンドリングなどが追加されています。
#
# プログラムの主な機能:
# 1. ブラウザ自動操作(WebDriverHandler)
# - Seleniumを使用したブラウザの制御と自動操作
# - ARM/x86_64アーキテクチャの自動検出とChromeDriver管理
# - レース検索条件の設定と実行(リトライ機能付き)
# - 検索結果ページからのレースリンク抽出
# - 血統・馬詳細情報のバッチ処理機能
# 2. HTTP通信管理(RequestManager)
# - セッション管理による効率的な通信
# - 複数ユーザーエージェントのローテーション
# - 強化されたリトライ機能付きHTTPリクエスト処理
# - HTMLコンテンツの取得とBeautifulSoupへの変換
# - レート制限対応とエラー処理
# 3. サイト負荷軽減対策
# - ランダム待機時間の実装
# - 自然なブラウジングパターンの模倣
# - スクロール操作で人間のような振る舞い
# - バッチ処理による効率的なデータ取得
# - 複数プロセス同時実行の防止(FileLock)
#
# ==============================================================================
# クラスと機能の詳細
# ==============================================================================
# 1. WebDriverHandler:
# - ブラウザインスタンスの作成と設定
# └─ ARM/x86_64アーキテクチャの自動検出
# └─ プラットフォーム固有のChromeDriver管理
# └─ ヘッドレスモードのサポート
# └─ FileLockによる同時実行制御
# - レース検索機能(リトライ機能付き)
# └─ 競馬データベースサイトへのアクセス
# └─ 芝・ダート両方のレースを対象に設定
# └─ 年月指定での検索条件設定
# └─ タイムアウト・エラー時の自動リトライ
# - レースリンク収集
# └─ 検索結果ページからのリンク抽出
# └─ ページネーション(次へボタン)の処理
# └─ 抽出URLの絶対パス変換
# - HTMLコンテンツ取得(Selenium)
# └─ get_html_selenium(): ページ読み込み完了待機
# └─ 血統テーブル読み込み待機
# └─ JavaScriptの実行完了確認
# - バッチ処理機能(新機能)
# └─ get_horse_pedigree_batch(): 血統情報の効率的取得
# └─ get_horse_details_batch(): 馬詳細情報の一括取得
# └─ バッチサイズ指定による負荷分散
#
# 2. RequestManager:
# - セッション管理
# └─ requests.Sessionによる効率的な通信
# └─ Keep-Aliveによる接続の再利用
# └─ 適切なHTTPヘッダーの設定
# - HTMLコンテンツ取得
# └─ 指定URLからのHTMLコンテンツ取得
# └─ BeautifulSoupオブジェクトへの変換
# └─ lxmlパーサーの使用による高速化
# - リトライ機能付きリクエスト
# └─ 異なるユーザーエージェントでのリクエスト
# └─ レート制限(429)エラーの特別処理
# └─ タイムアウト・接続エラーの処理
# └─ 指数バックオフによる再試行間隔調整
# - セッションの適切な終了処理
#
# ==============================================================================
# 制御フローとプロセス
# ==============================================================================
# 1. ブラウザ操作の流れ:
# - ブラウザ初期化(アーキテクチャ自動検出)
# └─ search_race()でレース検索ページにアクセス(リトライ付き)
# └─ 検索条件を設定(年月、競馬場種別など)
# └─ 検索実行(エラー時自動リトライ)
# └─ collect_race_links()でレースリンクを収集
# └─ _extract_links_from_page()で1ページのリンク抽出
# └─ _click_next_btn()で次ページに移動(全ページ走査)
# └─ close_browser()でブラウザを終了
#
# 2. バッチ処理の流れ:
# - get_horse_pedigree_batch()で血統情報を効率取得
# └─ 指定バッチサイズで馬URLを分割
# └─ 各バッチ内でget_html_selenium()を実行
# └─ 血統情報の抽出と構造化
# └─ バッチ間の適切な待機時間確保
# └─ 全バッチ完了まで反復
#
# 3. 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の自動管理(x86_64)
# ├─ filelock: 同時実行制御
# ├─ requests: HTTPリクエスト
# ├─ beautifulsoup4: HTML解析
# └─ platform, shutil: アーキテクチャ検出とバイナリ管理
#
# ==============================================================================
# アーキテクチャ対応
# ==============================================================================
# 1. ARM アーキテクチャ対応:
# - Raspberry Pi などの ARM デバイスでの動作サポート
# - システム標準の chromedriver を使用
# - chromium-browser の自動検出
# - ARM 固有の最適化設定
#
# 2. x86_64 アーキテクチャ対応:
# - webdriver_manager による自動 ChromeDriver 管理
# - Google Chrome の自動検出
# - 従来の動作を維持
#
# 3. プラットフォーム自動検出:
# - platform.machine() による CPU アーキテクチャ判定
# - shutil.which() による実行可能ファイル検索
# - 環境に応じたドライバー・ブラウザ選択
#
# ==============================================================================
# 使用例
# ==============================================================================
# 1. WebDriverHandlerの使用:
# ```python
# # WebDriverHandlerのインスタンス作成
# webdriver = WebDriverHandler(config_manager)
#
# # ブラウザの初期化(アーキテクチャ自動対応)
# browser = webdriver.initialize_browser()
#
# # 2023年12月のレースを検索(リトライ機能付き)
# webdriver.search_race(2023, 12)
#
# # レースリンクの収集
# links = webdriver.collect_race_links()
#
# # 血統情報の効率的取得(バッチ処理)
# horse_urls = [...] # 馬詳細ページのURLリスト
# father_list, mother_list, detailed_pedigree = webdriver.get_horse_pedigree_batch(horse_urls, batch_size=10)
#
# # 馬詳細情報の効率的取得(バッチ処理)
# horse_details = webdriver.get_horse_details_batch(horse_urls, batch_size=10)
# ```
#
# 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)
#
# # セッションの適切な終了
# request.close_session()
# ```
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. ブラウザ操作に関する注意:
# - ARM アーキテクチャでは事前に chromium と chromedriver のインストールが必要
# - ヘッドレスモードが推奨(特に ARM デバイスでは描画コスト削減)
# - 複数プロセスから同時実行されないようFileLockで制御
# - ブラウザはリソースを消費するため、使用後に必ず閉じる
# - リトライ機能により、一時的なネットワーク問題に対応
#
# 2. リクエスト管理の注意:
# - セッション管理により効率的な通信を実現
# - レート制限(429エラー)に対する特別処理を実装
# - 過度な短時間リクエストはサイト側から制限される可能性あり
# - バッチ処理により負荷分散を図る
# - 大量のページを一度に取得する場合はバッチサイズを調整
#
# 3. 実行環境の注意:
# - Chrome/Chromiumブラウザがインストールされている必要あり
# - ARM デバイスでは chromedriver のパッケージ導入が必要
# - 十分なメモリ(特にヘッドレスでない場合)
# - 安定したインターネット接続
# - ファイアウォール設定で外部アクセスが許可されていること
# - FileLock による排他制御でマルチプロセス実行を回避
#
# 4. パフォーマンス最適化:
# - バッチ処理により個別アクセス回数を削減
# - セッション管理による接続の再利用
# - 適切な待機時間による負荷分散
# - エラー時の指数バックオフによる効率的リトライ
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 selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from webdriver_manager.chrome import ChromeDriverManager
from bs4 import BeautifulSoup
import platform, shutil
class WebDriverHandler:
"""
Seleniumブラウザの操作を管理するクラス(拡張版)
"""
def __init__(self, config_manager):
"""
WebDriverHandlerの初期化
"""
self.config = config_manager.config
self.browser = None
self.wait = None
# 拡張機能用の設定
self.page_load_timeout = 30
self.element_wait_timeout = 10
self.max_retry_count = 3
# WebDriverHandler.initialize_browser() を一部置き換え
def initialize_browser(self):
"""
Chromeブラウザを初期化し、スクレイピング用に設定する。
ARM/x86_64アーキテクチャを自動検出してChromeDriverを適切に設定し、
FileLockによる同時実行制御、ヘッドレスモード、セキュリティオプションなどを
適用したWebDriverインスタンスを作成する。
:return: 初期化されたChromeWebDriverインスタンス。初期化に失敗した場合はNone
:rtype: webdriver.Chrome or 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')
lock = FileLock(lock_file_path, timeout=120)
try:
# with lock.acquire(): ではなく with lock: が正しい
with lock:
chrome_options = webdriver.ChromeOptions()
# ヘッドレス推奨(Pi では描画コスト削減)
chrome_options.add_argument('--headless=new') # この行をコメントアウトするchromeが描画される
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
# Chromium の実体パスを解決
binary = (shutil.which("chromium")
or shutil.which("chromium-browser")
or shutil.which("google-chrome"))
if binary:
chrome_options.binary_location = binary
# ARM かどうかで分岐
arch = platform.machine().lower()
if any(k in arch for k in ['aarch64', 'armv7', 'armv8', 'armhf', 'arm64', 'arm']):
driver_path = shutil.which("chromedriver") or "/usr/bin/chromedriver"
service = ChromeService(executable_path=driver_path)
else:
# x86_64 では従来通り webdriver_manager を使用
service = ChromeService(ChromeDriverManager().install())
self.browser = webdriver.Chrome(service=service, options=chrome_options)
self.browser.set_page_load_timeout(120)
self.browser.implicitly_wait(30)
self.wait = WebDriverWait(self.browser, 30)
return self.browser
except Timeout:
print('ロックの取得に失敗しました。')
return None
except Exception as e:
print(f'ブラウザ初期化エラー: {e}')
return None
def wait_for_random_time(self, min_seconds=None, max_seconds=None):
"""
指定された範囲内でランダムな時間だけ待機する。
自然なアクセスパターンを模倣し、サイト負荷軽減に貢献する。
引数が未指定の場合は設定ファイルのデフォルト値を使用する。
:param min_seconds: 待機時間の最小値(秒)。未指定時は設定ファイルの値を使用
:type min_seconds: int or float, optional
:param max_seconds: 待機時間の最大値(秒)。未指定時は設定ファイルの値を使用
:type max_seconds: int or float, optional
"""
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ページを指定されたピクセル数だけ縦方向にスクロールする。
ブラウザが初期化されている場合のみ実行される。
自然なブラウジング動作を模倣するために使用される。
:param pixel: スクロールする縦方向のピクセル数
:type pixel: int
"""
if self.browser:
self.browser.execute_script(f'window.scrollTo(0, {pixel});')
def get_html_selenium(self, url, timeout=10):
"""
SeleniumでWebページにアクセスしHTMLコンテンツを取得する。
ページ読み込み完了待機、血統テーブル読み込み待機、リトライ機能を含む。
血統ページの場合は血統テーブルの読み込みまで待機し、
取得したHTMLをBeautifulSoupオブジェクトに変換して返す。
:param url: 取得対象のWebページURL
:type url: str
:param timeout: タイムアウト時間(秒)。現在は使用されていない
:type timeout: int, optional
:return: 取得したHTMLのBeautifulSoupオブジェクト。取得失敗時はNone
:rtype: BeautifulSoup or None
"""
if not self.browser:
print("ブラウザが初期化されていません")
return None
retry_count = 0
while retry_count < self.max_retry_count:
try:
self.browser.get(url)
# ページの読み込み完了を待つ
self.wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
# 血統テーブルの読み込みを待つ(血統ページの場合)
if '/horse/' in url:
try:
self.wait.until(
EC.presence_of_element_located((By.CLASS_NAME, "blood_table"))
)
except TimeoutException:
print(f"血統テーブルが見つかりません: {url}")
# 血統テーブルがない場合でも基本情報は取得を試行
pass
# 少し待機してJavaScriptの実行を確実にする
self.wait_for_random_time(1, 2)
# HTMLを取得してBeautifulSoupに変換
html_content = self.browser.page_source
return BeautifulSoup(html_content, 'html.parser')
except TimeoutException:
retry_count += 1
print(f"タイムアウトエラー (試行 {retry_count}/{self.max_retry_count}): {url}")
if retry_count < self.max_retry_count:
print(f"リトライします...")
self.wait_for_random_time(2, 5)
else:
print(f"最大リトライ回数に達しました: {url}")
return None
except Exception as e:
print(f"HTML取得エラー: {e}")
return None
return None
def get_horse_pedigree_batch(self, horse_urls, batch_size=10):
"""
血統情報を効率的にバッチ処理で取得する。
指定されたバッチサイズで馬URLを分割し、各馬の詳細ページにアクセスして
父、母、父の父、父の母、母の父、母の母の血統情報を取得する。
バッチ間に適切な待機時間を設けて負荷分散を行う。
:param horse_urls: 馬の詳細ページURLのリスト
:type horse_urls: list
:param batch_size: バッチサイズ(一度に処理するURL数)
:type batch_size: int
:return: (父馬名リスト, 母馬名リスト, 詳細血統情報リスト)のタプル
:rtype: tuple
"""
if not self.browser:
print("ブラウザが初期化されていません")
return [], [], []
father_list = []
mother_list = []
detailed_pedigree_list = []
total_urls = len(horse_urls)
print(f"血統情報を{batch_size}件ずつバッチ処理で取得します(合計: {total_urls}件)")
for i in range(0, total_urls, batch_size):
batch_urls = horse_urls[i:i + batch_size]
batch_num = i // batch_size + 1
total_batches = (total_urls + batch_size - 1) // batch_size
print(f"血統情報バッチ {batch_num}/{total_batches} を処理中...")
for url in batch_urls:
try:
# 馬の詳細ページにアクセス
horse_soup = self.get_html_selenium(url)
if horse_soup:
# 詳細血統情報を取得
from content_parsers import HorsePedigreeParser
father, mother, father_father, father_mother, mother_father, mother_mother = \
HorsePedigreeParser.get_horse_detailed_pedigree(horse_soup)
father_list.append(father)
mother_list.append(mother)
detailed_pedigree_list.append({
'father_father': father_father,
'father_mother': father_mother,
'mother_father': mother_father,
'mother_mother': mother_mother
})
print(f" 取得完了: {father} x {mother}")
else:
# HTMLが取得できない場合は空文字を追加
father_list.append("")
mother_list.append("")
detailed_pedigree_list.append({
'father_father': '',
'father_mother': '',
'mother_father': '',
'mother_mother': ''
})
print(f" 取得失敗: {url}")
# 次のURLアクセス前の待機
self.wait_for_random_time(1, 2)
except Exception as e:
print(f"血統情報取得エラー ({url}): {e}")
# エラー時は空文字を追加
father_list.append("")
mother_list.append("")
detailed_pedigree_list.append({
'father_father': '',
'father_mother': '',
'mother_father': '',
'mother_mother': ''
})
# バッチ間の待機
if i + batch_size < total_urls:
print(f" バッチ間待機中...")
self.wait_for_random_time(3, 6)
print(f"血統情報取得完了: {len(father_list)}件")
return father_list, mother_list, detailed_pedigree_list
def get_horse_details_batch(self, horse_urls, batch_size=10):
"""
馬詳細情報を効率的にバッチ処理で取得する。
指定されたバッチサイズで馬URLを分割し、各馬の詳細ページにアクセスして
馬主、生年月日、生産者、産地、調教師の情報を取得する。
バッチ間に適切な待機時間を設けて負荷分散を行う。
:param horse_urls: 馬の詳細ページURLのリスト
:type horse_urls: list
:param batch_size: バッチサイズ(一度に処理するURL数)
:type batch_size: int
:return: 馬詳細情報のリスト
:rtype: list
"""
if not self.browser:
print("ブラウザが初期化されていません")
return []
horse_details = []
total_urls = len(horse_urls)
print(f"馬詳細情報を{batch_size}件ずつバッチ処理で取得します(合計: {total_urls}件)")
for i in range(0, total_urls, batch_size):
batch_urls = horse_urls[i:i + batch_size]
batch_num = i // batch_size + 1
total_batches = (total_urls + batch_size - 1) // batch_size
print(f"馬詳細情報バッチ {batch_num}/{total_batches} を処理中...")
for url in batch_urls:
try:
# 馬の詳細ページにアクセス
horse_soup = self.get_html_selenium(url)
if horse_soup:
# 馬詳細情報を取得
from content_parsers import HorseDetailParser
details = {
'owner': HorseDetailParser.get_horse_owner(horse_soup),
'birth_date': HorseDetailParser.get_horse_birth_date(horse_soup),
'breeder': HorseDetailParser.get_horse_breeder(horse_soup),
'birthplace': HorseDetailParser.get_horse_birthplace(horse_soup),
'trainer': HorseDetailParser.get_horse_trainer(horse_soup)
}
horse_details.append(details)
print(f" 取得完了: {details.get('owner', 'Unknown')}")
else:
# HTMLが取得できない場合は空の辞書を追加
horse_details.append({})
print(f" 取得失敗: {url}")
# 次のURLアクセス前の待機
self.wait_for_random_time(1, 2)
except Exception as e:
print(f"馬詳細情報取得エラー ({url}): {e}")
horse_details.append({})
# バッチ間の待機
if i + batch_size < total_urls:
print(f" バッチ間待機中...")
self.wait_for_random_time(3, 6)
print(f"馬詳細情報取得完了: {len(horse_details)}件")
return horse_details
def search_race(self, year, month):
"""
指定された年月のレースを検索する。
リトライ機能付きで、タイムアウトやエラー発生時は最大3回まで自動的に
ブラウザを再初期化して検索を再実行する。検索条件として芝・ダート両方を
対象とし、検索結果の表示件数を20件に設定する。
:param year: 検索対象年
:type year: int or str
:param month: 検索対象月
:type month: int or str
"""
max_retries = 3
retry_count = 0
while retry_count < max_retries:
try:
print(f"検索ページにアクセス中... (試行 {retry_count + 1}/{max_retries})")
# 競馬データベースを開く
self.browser.get(self.config["search_url"])
# ページが完全に読み込まれるまで待機(より詳細な待機)
print("ページの読み込み完了を待機中...")
# 複数の要素の読み込みを確認
self.wait.until(EC.presence_of_element_located((By.TAG_NAME, 'body')))
print("bodyタグの読み込み完了")
# JavaScriptの実行完了を待つ
self.browser.execute_script("return document.readyState") == "complete"
print("JavaScriptの実行完了")
# 特定の要素の読み込みを待つ
try:
self.wait.until(EC.element_to_be_clickable((By.ID, 'check_track_1')))
print("検索フォーム要素の読み込み完了")
except TimeoutException:
print("検索フォーム要素の読み込みタイムアウト - ページ構造を確認します")
# ページのHTMLを確認
page_source = self.browser.page_source
if len(page_source) < 1000:
print("ページの内容が不完全です。リトライします。")
raise TimeoutException("ページ内容不完全")
else:
print("ページは読み込まれていますが、要素が見つかりません")
# 要素の代替検索を試行
elements = self.browser.find_elements(By.CSS_SELECTOR, "input[type='checkbox']")
if len(elements) == 0:
raise TimeoutException("チェックボックス要素が見つかりません")
self.wait_for_random_time(2, 5) # 追加の待機時間
# 検索条件を設定
print("検索条件を設定中...")
# 競争種別で「芝」と「ダート」にチェック
try:
track1_element = self.browser.find_element(By.ID, 'check_track_1')
if not track1_element.is_selected():
track1_element.click()
print("芝レースを選択")
track2_element = self.browser.find_element(By.ID, 'check_track_2')
if not track2_element.is_selected():
track2_element.click()
print("ダートレースを選択")
except NoSuchElementException:
print("トラック種別の選択要素が見つかりません")
raise
# 期間を設定
self._set_search_period(str(year), str(month))
self.scroll_webpage(400)
print(f"検索期間を設定: {year}年{month}月")
# 表示件数を設定
self._set_display_options("100")
print("表示件数を100件に設定")
# 検索実行
print("検索を実行中...")
self._submit_search()
# 検索結果ページの読み込みを待つ
self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'nk_tb_common')))
print("検索完了")
return # 成功した場合はここで終了
except TimeoutException as e:
retry_count += 1
print(f"タイムアウトエラー (試行 {retry_count}/{max_retries}): {e}")
if retry_count < max_retries:
print(f"5秒後にリトライします...")
time.sleep(5)
# ブラウザを一度閉じて再初期化
if self.browser:
self.browser.quit()
time.sleep(2)
self.initialize_browser()
else:
print("最大リトライ回数に達しました")
raise
except Exception as e:
retry_count += 1
print(f"検索実行エラー (試行 {retry_count}/{max_retries}): {e}")
if retry_count < max_retries:
print(f"5秒後にリトライします...")
time.sleep(5)
# ブラウザを一度閉じて再初期化
if self.browser:
self.browser.quit()
time.sleep(2)
self.initialize_browser()
else:
print("最大リトライ回数に達しました")
raise
def _set_search_period(self, year, month):
"""
検索フォームで検索期間を設定する。
開始年月と終了年月を同じ値に設定し、指定された年月のみを
検索対象とする。プライベートメソッドとして内部的に使用される。
:param year: 対象年
:type year: str
:param month: 対象月
:type month: str
"""
# 開始年月を設定
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):
"""
検索結果の表示件数を設定する。
検索フォームの表示件数選択ボックスで指定された件数を選択する。
プライベートメソッドとして内部的に使用される。
:param num: 表示件数
:type num: str
"""
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):
"""
検索結果ページからレースリンクを収集する。
複数ページにわたる検索結果を順次処理し、各ページからレースURLを抽出する。
「次へ」ボタンがある限り次ページに移動し、全ページのリンクを収集する。
: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)
return link_list
def _extract_links_from_page(self, link_list):
"""
現在のページからレースリンクを抽出する。
ページのHTMLから検索結果テーブルを取得し、レース詳細ページへのリンクを
フィルタリングして抽出する。馬、騎手、結果ページなどは除外する。
:param link_list: 既存のリンクリスト(抽出したリンクを追加)
:type link_list: 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と相対URLを結合して完全なURLを生成する。
プライベートメソッドとして内部的に使用される。
:param relative_url: 変換対象の相対URL
:type relative_url: str
: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 count_click_next: これまでの次ページクリック回数
:type count_click_next: int
: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):
"""
ブラウザを安全に終了する。
WebDriverインスタンスが存在する場合に終了処理を実行し、
エラーが発生した場合も確実にインスタンスをNoneに設定する。
"""
if self.browser:
try:
self.browser.quit()
except Exception as e:
print(f"ブラウザクローズエラー: {e}")
finally:
self.browser = None
class RequestManager:
"""
HTTPリクエストを管理するクラス(拡張版)
"""
def __init__(self, config_manager):
"""
RequestManagerの初期化
"""
self.config = config_manager.config
self.session = requests.Session()
# セッション設定
self.session.headers.update({
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "ja,en-US;q=0.7,en;q=0.3",
"Accept-Encoding": "gzip, deflate",
"DNT": "1",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1"
})
def get_html(self, url, max_retries=5, retry_delay=5):
"""
指定されたURLからHTMLコンテンツを取得しBeautifulSoupオブジェクトに変換する。
内部でretry_request()を呼び出してリトライ機能付きでHTTPリクエストを実行し、
取得したレスポンスをlxmlパーサーでBeautifulSoupオブジェクトに変換する。
:param url: 取得対象のWebページURL
:type url: str
:param max_retries: 最大リトライ回数
:type max_retries: int
:param retry_delay: リトライ間の待機時間(秒)
:type retry_delay: int
:return: 取得したHTMLのBeautifulSoupオブジェクト。取得失敗時はNone
:rtype: BeautifulSoup or 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リクエストを実行する。
ランダムなユーザーエージェントを使用し、レート制限(429エラー)に対する
特別処理、タイムアウト・接続エラーの処理を含む。エラー時は指定回数まで
自動的にリトライを実行する。
:param url: リクエスト対象のURL
:type url: str
:param max_retries: 最大リトライ回数
:type max_retries: int
:param retry_delay: リトライ間の基本待機時間(秒)
:type retry_delay: int
:return: HTTPレスポンスオブジェクト。リクエスト失敗時はNone
:rtype: requests.Response or None
"""
retries = 0
while retries < max_retries:
try:
# ランダムなユーザーエージェントを選択
headers = {
"User-Agent": random.choice(self.config["user_agents"])
}
response = self.session.get(url, headers=headers, timeout=30)
if response.status_code == 200:
return response
elif response.status_code == 429:
# レート制限の場合は長めに待機
print(f"レート制限検出 (429): {url}")
time.sleep(retry_delay * 2)
else:
print(f"ステータスコード {response.status_code}: {url}")
except requests.exceptions.Timeout:
print(f"タイムアウト: {url}")
except requests.exceptions.ConnectionError:
print(f"接続エラー: {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
def close_session(self):
"""
HTTPセッションを適切に終了する。
requests.Sessionインスタンスが存在する場合に終了処理を実行し、
接続リソースを適切に解放する。
"""
if self.session:
self.session.close()
content_parsers.py (HTML解析)
# ==============================================================================
# 競馬レース結果HTML解析モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース情報をHTML形式から構造化データに変換する解析モジュール。
# レースの基本情報、結果テーブル、出走馬の詳細血統情報、馬主・生産者情報などを
# BeautifulSoupを使用して抽出し、後続処理で利用可能なデータ形式に変換します。
#
# プログラムの主な機能:
# 1. HTML要素からのデータ抽出
# - BeautifulSoupを使用したDOM要素の検索と抽出
# - 正規表現によるテキスト処理と整形
# - エラーハンドリングによる堅牢なデータ取得
# 2. データの構造化と変換
# - pandas DataFrameへの情報整理
# - カラム名の標準化と型変換
# - データの整形と結合
# 3. 詳細情報の取得
# - 枠番、馬番、調教師情報の抽出
# - 馬主、生年月日、生産者、産地の詳細情報取得
# - 6世代血統情報(父の父、父の母、母の父、母の母)の抽出
# - コース方向、発走時刻などの競走条件詳細の取得
# 4. 例外処理と信頼性確保
# - 各メソッド単位での例外キャッチ
# - 欠損データへの対応
# - エラー発生時のフォールバック処理
#
# ==============================================================================
# クラスと機能の詳細
# ==============================================================================
# 1. RaceInfoParser(レース基本情報解析):
# - get_race_name(): レース名の抽出
# - get_race_round(): ラウンド情報の抽出(例:「東京11R」)
# - get_race_date(): 開催日の抽出(例:「2023年12月24日」)
# - get_race_place(): 開催場所の抽出(例:「東京」「中山」)
# - get_race_distance_and_direction(): レース距離(m)とコース方向(右/左)の抽出
# - get_race_start_time(): 発走時刻の抽出(例:「17:10」)
# - get_race_distance(): レース距離(m)の抽出
# - get_race_weather(): 天候情報の抽出
# - get_race_condition(): 馬場状態の抽出(芝/ダート、良/稍重など)
#
# 2. RaceResultsParser(レース結果解析):
# - parse_race_results(): レース結果テーブルの解析
# └─ 着順、枠番、馬番、馬名、性齢、斤量、騎手、タイム、上り、単勝、人気、馬体重、調教師を抽出
# - _extract_row_data_extended(): テーブル1行からのデータ抽出
# └─ 枠番、馬番、調教師情報を含む詳細データの抽出
# - format_race_results(): 抽出データの整形と結合
# └─ レース情報、血統情報、馬詳細情報を含めた最終的なデータフレーム作成
#
# 3. HorseDetailParser(馬詳細情報解析):
# - get_horse_birth_date(): 生年月日の抽出
# - get_horse_trainer(): 調教師の抽出
# - get_horse_owner(): 馬主の抽出
# - get_horse_breeder(): 生産者の抽出
# - get_horse_birthplace(): 産地の抽出
#
# 4. HorsePedigreeParser(血統情報解析):
# - get_horse_links(): 出走馬の詳細ページリンク抽出
# - get_horse_detailed_pedigree(): 詳細な6世代血統情報を抽出
# └─ 父、母、父の父、父の母、母の父、母の母の名前を取得
# - get_horse_father_name(): 父馬名の抽出
# - get_horse_mother_name(): 母馬名の抽出
#
# ==============================================================================
# データ抽出フロー
# ==============================================================================
# 1. レース基本情報の抽出:
# - レースページのHTMLから見出し要素(h1)を検索してレース名を抽出
# - クラス名を指定して特定の要素からラウンド情報や開催情報を抽出
# - diary_snap_cut要素から距離、コース方向、発走時刻の情報を抽出
# - テキスト分割と正規表現処理による情報の整形
#
# 2. レース結果テーブルの解析:
# - テーブル要素(class="race_table_01")の検索
# - 行(tr)ごとに列(td/th)のテキストを抽出
# - 着順、除外、取消の判定と処理
# - 枠番、馬番、調教師情報を含む各列の情報を辞書形式でまとめてDataFrameに変換
#
# 3. 馬詳細情報の取得:
# - 馬の詳細ページ(db_prof_table)から基本情報を検索
# - 調教師、馬主、生年月日、生産者、産地の情報を抽出
# - リンク要素とテキスト要素の両方からデータを取得
#
# 4. 血統情報の取得:
# - レーステーブルから出走馬の詳細ページへのリンクを抽出
# - 各馬の詳細ページにアクセスして血統テーブル(blood_table)を検索
# - 6世代血統情報を抽出:
# ├─ 父馬(b_ml): 1行目1列目
# ├─ 父の父(b_ml): 1行目2列目
# ├─ 父の母(b_fml): 2行目1列目
# ├─ 母馬(b_fml): 3行目1列目
# ├─ 母の父(b_ml): 3行目2列目
# └─ 母の母(b_fml): 4行目1列目
# - 全出走馬の血統リストを生成
#
# ==============================================================================
# プログラム間の関係
# ==============================================================================
# 1. 本モジュールを呼び出すプログラム:
# - scraper_manager.py
# └─ スクレイピング処理全体の制御
# └─ 各レースページのHTMLから情報を抽出
# └─ データベースへの保存とCSV出力の準備
#
# 2. 本モジュールが依存するプログラム:
# - web_manager.pyの出力結果(BeautifulSoupオブジェクト)
# └─ RequestManagerで取得したHTMLコンテンツ
# └─ WebDriverHandlerで取得したSeleniumベースのHTMLコンテンツ
#
# ==============================================================================
# 使用例
# ==============================================================================
# 1. レース基本情報の取得:
# ```python
# from content_parsers import RaceInfoParser
#
# # HTMLからレース名を取得
# race_name = RaceInfoParser.get_race_name(soup)
#
# # 開催日と場所を取得
# race_date = RaceInfoParser.get_race_date(soup)
# race_place = RaceInfoParser.get_race_place(soup)
#
# # レース距離とコース方向を同時取得
# distance, direction = RaceInfoParser.get_race_distance_and_direction(soup)
#
# # 発走時刻を取得
# start_time = RaceInfoParser.get_race_start_time(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 HorseDetailParser
#
# # 馬主情報を取得
# owner = HorseDetailParser.get_horse_owner(horse_soup)
#
# # 生年月日を取得
# birth_date = HorseDetailParser.get_horse_birth_date(horse_soup)
#
# # 生産者と産地を取得
# breeder = HorseDetailParser.get_horse_breeder(horse_soup)
# birthplace = HorseDetailParser.get_horse_birthplace(horse_soup)
# ```
#
# 4. 血統情報の取得:
# ```python
# from content_parsers import HorsePedigreeParser
#
# # 出走馬の詳細ページへのリンクを取得
# horse_links = HorsePedigreeParser.get_horse_links(race_table, base_url)
#
# # 6世代血統情報を取得
# father, mother, father_father, father_mother, mother_father, mother_mother = \
# HorsePedigreeParser.get_horse_detailed_pedigree(horse_soup)
#
# # 父馬名・母馬名取得
# father_name = HorsePedigreeParser.get_horse_father_name(horse_soup)
# mother_name = HorsePedigreeParser.get_horse_mother_name(horse_soup)
# ```
#
# ==============================================================================
# 追加データ項目の詳細
# ==============================================================================
# 1. データ項目:
# - 枠番・馬番: レース結果テーブルから抽出
# - 調教師: レース結果テーブルおよび馬詳細ページから抽出
# - 馬主: 馬詳細ページから抽出
# - 生年月日: 馬詳細ページから抽出
# - 生産者: 馬詳細ページから抽出
# - 産地: 馬詳細ページから抽出
# - コース方向: レース基本情報から抽出(右/左)
# - 発走時刻: レース基本情報から抽出(HH:MM形式)
# - 6世代血統情報: 血統テーブルから詳細抽出
#
# 2. データ処理の改良:
# - 詳細なエラーハンドリング
# - 調教師名の[東][西]表記の除去処理
# - 血統情報の階層的な抽出ロジック
# - レース結果の多カラム対応
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. HTML構造依存性:
# - 本モジュールはnetkeiba.comのHTML構造に依存しています
# - サイト側の変更によって抽出機能が動作しなくなる可能性があります
# - 定期的に抽出ロジックの検証が必要です
# - 詳細情報の抽出機能はサイト変更の影響を受けやすい可能性があります
#
# 2. エラー処理:
# - 各メソッドはエラー発生時に空文字や空のDataFrameを返すよう設計
# - try-except構文で例外を捕捉し、プログラム全体が停止しないよう配慮
# - エラーメッセージはコンソールに出力されますが処理は継続されます
# - HorseDetailParserのメソッドも同様のエラー処理を実装
#
# 3. データ整合性:
# - 除外や取消の出走馬はresults_dfから除外されます
# - 血統情報や馬詳細情報が取得できない場合は空文字が設定されます
# - format_race_resultsでは'着順'と'上り'列は削除されます
# - カラムが存在しない場合のフォールバック処理を実装
#
# 4. パフォーマンス考慮:
# - 血統情報や馬詳細情報の取得は時間がかかる処理です
# - 大量の馬情報を処理する際は適切な待機時間を設けることを推奨
# - 詳細情報の取得により処理時間が長くなることがあります
#
# 追加機能:
# - レース結果テーブルから枠番、馬番、調教師の取得
# - 馬詳細ページから詳細な血統情報、馬主、生年月日、生産者、産地の取得
# - レース基本情報からコース方向、発走時刻の取得
# - 6世代血統情報(父の父、父の母、母の父、母の母)の取得
# ==============================================================================
import re
import pandas as pd
from bs4 import BeautifulSoup
class RaceInfoParser:
"""
レース基本情報の解析を担当するクラス
"""
@staticmethod
def get_race_name(soup):
"""
HTMLコンテンツからレース名を抽出する。
h1タグの要素を検索し、2番目のh1要素からレース名を取得する。
レース名は通常「有馬記念」「日本ダービー」などの形式で記載されている。
抽出に失敗した場合は空文字を返却する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
: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コンテンツからラウンド情報を抽出する。
racedata fc クラスのdl要素内のdt要素から競馬場名とレース番号の
組み合わせ(例:「東京11R」「中山9R」)を取得する。
改行文字を除去し、整形したラウンド情報を返却する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
: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コンテンツから開催日を抽出する。
smalltxt クラスのp要素から日付情報を取得し、スペースで分割した
最初の部分を開催日として抽出する。通常「2023年12月24日」などの
形式で記載されている。ノーブレークスペースを通常スペースに変換する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
: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 soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
: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_and_direction(soup):
"""
HTMLコンテンツからレース距離とコース方向を抽出する。
diary_snap_cut要素から「ダ右1200m」「芝左2000m」などの形式の
テキストを解析し、距離(メートル単位)とコース方向(右/左)を
分離して取得する。「2周」などの表記は除去し、数値のみを抽出する。
正規表現を使用して数字とコース方向を識別する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 距離(メートル)とコース方向のタプル。取得失敗時は(0, "")
:rtype: tuple(int, 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 words:
# 最初の部分に「ダ右1200m」や「芝左2000m」が含まれる
distance_text = words[0].strip()
# コース方向を抽出(右または左)
direction = ""
if '右' in distance_text:
direction = "右"
elif '左' in distance_text:
direction = "左"
# 2周などの文字を削除し、数字のみを抽出
distance_text = re.sub(r'2周', '', distance_text)
distance = int(re.sub(r'\D', '', distance_text))
return distance, direction
except Exception as e:
print(f"距離・方向抽出エラー: {e}")
return 0, ""
@staticmethod
def get_race_start_time(soup):
"""
HTMLコンテンツから発走時刻を抽出する。
diary_snap_cut要素から「発走 : 17:10」形式のテキストを検索し、
正規表現を使用して時刻部分のみを抽出する。全角コロンは半角に
統一して返却する。HH:MM形式(例:「15:40」)で時刻を取得する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 抽出された発走時刻(HH:MM形式)。取得失敗時は空文字
: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' ')
# 発走時刻を正規表現で抽出
import re
start_time_match = re.search(r'発走\s*[::]\s*(\d{1,2}[::]\d{2})', text)
if start_time_match:
# 全角コロンを半角に統一
start_time = start_time_match.group(1).replace(':', ':')
return start_time
except Exception as e:
print(f"発走時刻抽出エラー: {e}")
return ""
@staticmethod
def get_race_distance(soup):
"""
HTMLコンテンツからレース距離を抽出する。
内部的にget_race_distance_and_direction()メソッドを呼び出し、
戻り値のタプルから距離部分のみを取得する。コース方向は無視し、
距離(メートル単位)のみを整数として返却する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 抽出されたレース距離(メートル)。取得失敗時は0
:rtype: int
"""
distance, _ = RaceInfoParser.get_race_distance_and_direction(soup)
return distance
@staticmethod
def get_race_weather(soup):
"""
HTMLコンテンツから天候情報を抽出する。
diary_snap_cut要素からスラッシュ(/)で区切られたテキストを解析し、
2番目の部分からコロン(:)で分割した後の部分を天候として取得する。
通常「晴」「曇」「雨」「小雨」などの気象状況が記載されている。
ノーブレークスペースと全角スペースを通常スペースに変換する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
: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要素からスラッシュ(/)で区切られたテキストを解析し、
3番目の部分からコロン(:)で分割して馬場の種類(芝/ダート)と
状態(良/稍重/重/不良など)を取得する。ノーブレークスペースと
全角スペースを通常スペースに変換してから解析を実行する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 馬場の種類と状態のタプル。取得失敗時は("", "")
:rtype: tuple(str, 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('/')
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コンテンツからレース結果テーブルを解析してDataFrameに変換する。
race_table_01クラスのテーブル要素を検索し、各行(tr)から出走馬の
詳細情報を抽出する。着順、枠番、馬番、馬名、性齢、斤量、騎手、タイム、
上り、単勝オッズ、人気、馬体重、調教師の情報を含む。除外・取消の
出走馬は結果から除外する。ヘッダー行はスキップして処理する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: レース結果のDataFrame。テーブルが見つからない場合は空のDataFrame
:rtype: pandas.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_extended(cols)
rows.append(row_data)
return pd.DataFrame(rows)
except Exception as e:
print(f"レース結果解析エラー: {e}")
return pd.DataFrame()
@staticmethod
def _extract_row_data_extended(cols):
"""
レース結果テーブルの1行分のセル(td/th要素)からデータを抽出する。
各カラムから着順、枠番、馬番、馬名、性齢、斤量、騎手、タイム、上り、
単勝オッズ、人気、馬体重、調教師の情報を辞書形式で取得する。
調教師名から[東][西]の表記を除去し、単勝オッズからカンマを除去する。
カラム数が不足している場合は空文字で補完する。
:param cols: テーブル行のセル要素のリスト
:type cols: list
:return: 抽出されたデータの辞書。エラー時は空の辞書
:rtype: dict
"""
try:
# 基本データ
row_data = {
'着順': cols[0].get_text().strip(),
'枠番': cols[1].get_text().strip(),
'馬番': cols[2].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 ''
}
# 調教師情報の抽出(18番目のカラム)
if len(cols) > 18:
trainer_cell = cols[18]
trainer_text = trainer_cell.get_text().strip()
# [東]や[西]の表記を除去
trainer_name = re.sub(r'\[東\]|\[西\]', '', trainer_text).strip()
row_data['調教師'] = trainer_name
else:
row_data['調教師'] = ''
return row_data
except Exception as e:
print(f"行データ抽出エラー: {e}")
return {}
@staticmethod
def format_race_results(race_results_df, race_info, horse_pedigree):
"""
レース結果DataFrameにレース基本情報と血統情報を統合して最終形式に整形する。
レース基本情報(距離、コース方向、発走時刻、天候、馬場、状態、開催日、
レース名、開催場所、ラウンド)を各行に追加し、血統情報(父親、母親、
父の父、父の母、母の父、母の母)も統合する。6世代血統情報と従来形式の
両方に対応し、使用しない列(着順、上り)を削除して最適化されたカラム順序で返却する。
:param race_results_df: レース結果のDataFrame
:type race_results_df: pandas.DataFrame
:param race_info: レース基本情報の辞書
:type race_info: dict
:param horse_pedigree: 血統情報のタプルまたはリスト
:type horse_pedigree: tuple or list
:return: 整形されたレース結果のDataFrame。エラー時は空のDataFrame
:rtype: pandas.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('レース名', '')
formatted_results['開催場所'] = race_info.get('開催場所', '')
formatted_results['ラウンド'] = race_info.get('ラウンド', '')
# 血統情報を追加(拡張版)
if horse_pedigree and len(horse_pedigree) >= 6:
father_list, mother_list, father_father_list, father_mother_list, mother_father_list, mother_mother_list = horse_pedigree
formatted_results['父親'] = father_list
formatted_results['母親'] = mother_list
formatted_results['父の父'] = father_father_list
formatted_results['父の母'] = father_mother_list
formatted_results['母の父'] = mother_father_list
formatted_results['母の母'] = mother_mother_list
else:
# 従来形式の血統情報
male_horse_pedigree_list, female_horse_pedigree_list = horse_pedigree if horse_pedigree else ([], [])
formatted_results['父親'] = male_horse_pedigree_list
formatted_results['母親'] = female_horse_pedigree_list
formatted_results['父の父'] = [''] * len(formatted_results)
formatted_results['父の母'] = [''] * len(formatted_results)
formatted_results['母の父'] = [''] * len(formatted_results)
formatted_results['母の母'] = [''] * len(formatted_results)
# 使用しない列を削除
formatted_results.drop(['着順', '上り'], axis=1, inplace=True, errors='ignore')
# 列の順序を調整(拡張版)- コース方向と発走時刻を追加
column_order = [
'タイム', '馬名', '枠番', '馬番', '父親', '母親', '父の父', '父の母', '母の父', '母の母',
'性齢', '斤量', '騎手', '調教師', '馬主', '生年月日', '生産者', '産地',
'単勝', '人気', '馬体重', '距離', 'コース方向', '発走時刻', '天候', '馬場', '状態',
'開催日', 'レース名', '開催場所', 'ラウンド'
]
# 存在するカラムのみを選択
existing_columns = [col for col in column_order if col in formatted_results.columns]
return formatted_results[existing_columns]
except Exception as e:
print(f"レース結果整形エラー: {e}")
return pd.DataFrame()
class HorseDetailParser:
"""
馬詳細情報の解析を担当するクラス
"""
@staticmethod
def get_horse_birth_date(soup):
"""
HTMLコンテンツから馬の生年月日を抽出する。
db_prof_tableクラスのテーブル要素を検索し、「生年月日」を含むth要素を
見つけて対応するtd要素から日付情報を取得する。通常「1998年4月15日」
などの形式で記載されている。テーブルが存在しない場合やデータが
見つからない場合は空文字を返却する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 抽出された生年月日。取得失敗時は空文字
:rtype: str
"""
try:
table = soup.find('table', class_='db_prof_table')
if not table:
return ""
rows = table.find_all('tr')
for row in rows:
th = row.find('th')
if th and '生年月日' in th.get_text():
td = row.find('td')
if td:
return td.get_text().strip()
except Exception as e:
print(f"生年月日抽出エラー: {e}")
return ""
@staticmethod
def get_horse_trainer(soup):
"""
HTMLコンテンツから調教師名を抽出する。
db_prof_tableクラスのテーブル要素を検索し、「調教師」を含むth要素を
見つけて対応するtd要素から調教師名を取得する。リンク要素(a)が
存在する場合はリンクテキストを優先し、なければ直接的なテキストを取得する。
通常「藤沢和雄」「角居勝彦」などの調教師名が記載されている。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 抽出された調教師名。取得失敗時は空文字
:rtype: str
"""
try:
table = soup.find('table', class_='db_prof_table')
if not table:
return ""
rows = table.find_all('tr')
for row in rows:
th = row.find('th')
if th and '調教師' in th.get_text():
td = row.find('td')
if td:
trainer_link = td.find('a')
if trainer_link:
return trainer_link.get_text().strip()
return td.get_text().strip()
except Exception as e:
print(f"調教師抽出エラー: {e}")
return ""
@staticmethod
def get_horse_owner(soup):
"""
HTMLコンテンツから馬主名を抽出する。
db_prof_tableクラスのテーブル要素を検索し、「馬主」を含むth要素を
見つけて対応するtd要素から馬主名を取得する。リンク要素(a)が
存在する場合はリンクテキストを優先し、なければ直接的なテキストを取得する。
通常「社台レースホース」「金子真人ホールディングス」などの馬主名が記載されている。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 抽出された馬主名。取得失敗時は空文字
:rtype: str
"""
try:
table = soup.find('table', class_='db_prof_table')
if not table:
return ""
rows = table.find_all('tr')
for row in rows:
th = row.find('th')
if th and '馬主' in th.get_text():
td = row.find('td')
if td:
owner_link = td.find('a')
if owner_link:
return owner_link.get_text().strip()
return td.get_text().strip()
except Exception as e:
print(f"馬主抽出エラー: {e}")
return ""
@staticmethod
def get_horse_breeder(soup):
"""
HTMLコンテンツから生産者名を抽出する。
db_prof_tableクラスのテーブル要素を検索し、「生産者」を含むth要素を
見つけて対応するtd要素から生産者名を取得する。リンク要素(a)が
存在する場合はリンクテキストを優先し、なければ直接的なテキストを取得する。
通常「ノーザンファーム」「社台ファーム」などの牧場名が記載されている。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 抽出された生産者名。取得失敗時は空文字
:rtype: str
"""
try:
table = soup.find('table', class_='db_prof_table')
if not table:
return ""
rows = table.find_all('tr')
for row in rows:
th = row.find('th')
if th and '生産者' in th.get_text():
td = row.find('td')
if td:
breeder_link = td.find('a')
if breeder_link:
return breeder_link.get_text().strip()
return td.get_text().strip()
except Exception as e:
print(f"生産者抽出エラー: {e}")
return ""
@staticmethod
def get_horse_birthplace(soup):
"""
HTMLコンテンツから産地(出生地)を抽出する。
db_prof_tableクラスのテーブル要素を検索し、「産地」を含むth要素を
見つけて対応するtd要素から産地情報を取得する。リンク要素は存在せず
直接的なテキストを取得する。通常「北海道安平町」「北海道千歳市」
などの都道府県と市町村名が記載されている。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: 抽出された産地名。取得失敗時は空文字
:rtype: str
"""
try:
table = soup.find('table', class_='db_prof_table')
if not table:
return ""
rows = table.find_all('tr')
for row in rows:
th = row.find('th')
if th and '産地' in th.get_text():
td = row.find('td')
if td:
return td.get_text().strip()
except Exception as e:
print(f"産地抽出エラー: {e}")
return ""
class HorsePedigreeParser:
"""
馬の血統情報解析を担当するクラス
"""
@staticmethod
def get_horse_links(race_table_data, base_url):
"""
レース結果テーブルから出走馬の詳細ページへのリンクURLを抽出する。
テーブル内のすべてのa要素を検索し、href属性に'/horse/'を含むリンクを
特定して馬の詳細ページURLを収集する。相対URLは絶対URLに変換する。
horse、jockey、result、sum、list、movie、javascriptを含むURLは除外し、
純粋な馬詳細ページのみを対象とする。
:param race_table_data: レース結果テーブルのHTMLコンテンツ
:type race_table_data: BeautifulSoup
:param base_url: 絶対URL変換用のベースURL
:type base_url: str
: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_detailed_pedigree(soup):
"""
HTMLコンテンツから詳細な6世代血統情報を抽出する。
blood_tableクラスのテーブル要素から血統表を解析し、父、母、父の父、
父の母、母の父、母の母の名前を取得する。各血統馬は特定の行・列位置に
配置されており(父馬:1行目1列目、父の父:1行目2列目など)、b_mlクラス(雄)と
b_fmlクラス(雌)によってセルを識別する。リンク要素が存在する場合は
リンクテキストを優先し、なければ直接テキストを取得する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
:return: (父、母、父の父、父の母、母の父、母の母)のタプル。取得失敗時は空文字6個のタプル
:rtype: tuple(str, str, str, str, str, str)
"""
try:
blood_table = soup.find('table', class_='blood_table')
if not blood_table:
return "", "", "", "", "", ""
rows = blood_table.find_all('tr')
if len(rows) < 4:
return "", "", "", "", "", ""
# 父馬(1行目の1列目、b_ml)
father = ""
father_cell = rows[0].find('td', class_='b_ml')
if father_cell:
father_link = father_cell.find('a')
if father_link:
father = father_link.get_text().strip()
# 父の父(1行目の2列目、b_ml)
father_father = ""
father_father_cell = rows[0].find_all('td')[1] if len(rows[0].find_all('td')) > 1 else None
if father_father_cell and 'b_ml' in father_father_cell.get('class', []):
father_father_link = father_father_cell.find('a')
if father_father_link:
father_father = father_father_link.get_text().strip()
# 父の母(2行目の1列目、b_fml)
father_mother = ""
if len(rows) > 1:
father_mother_cell = rows[1].find('td', class_='b_fml')
if father_mother_cell:
father_mother_link = father_mother_cell.find('a')
if father_mother_link:
father_mother = father_mother_link.get_text().strip()
# 母馬(3行目の1列目、b_fml)
mother = ""
if len(rows) > 2:
mother_cell = rows[2].find('td', class_='b_fml')
if mother_cell:
mother_link = mother_cell.find('a')
if mother_link:
mother = mother_link.get_text().strip()
# 母の父(3行目の2列目、b_ml)
mother_father = ""
if len(rows) > 2 and len(rows[2].find_all('td')) > 1:
mother_father_cell = rows[2].find_all('td')[1]
if mother_father_cell and 'b_ml' in mother_father_cell.get('class', []):
mother_father_link = mother_father_cell.find('a')
if mother_father_link:
mother_father = mother_father_link.get_text().strip()
# 母の母(4行目の1列目、b_fml)
mother_mother = ""
if len(rows) > 3:
mother_mother_cell = rows[3].find('td', class_='b_fml')
if mother_mother_cell:
mother_mother_link = mother_mother_cell.find('a')
if mother_mother_link:
mother_mother = mother_mother_link.get_text().strip()
return father, mother, father_father, father_mother, mother_father, mother_mother
except Exception as e:
print(f"詳細血統情報取得エラー: {e}")
return "", "", "", "", "", ""
@staticmethod
def get_horse_father_name(soup):
"""
HTMLコンテンツから父馬の名前を抽出する。
blood_tableクラスのテーブル要素から血統表の1行目(父馬の行)を検索し、
b_mlクラス(雄馬を示す)のセルから父馬名を取得する。リンク要素(a)が
存在する場合はリンクテキストを優先し、なければ直接的なテキストを取得する。
改行文字は除去して整形した父馬名を返却する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
: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]
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):
"""
HTMLコンテンツから母馬の名前を抽出する。
blood_tableクラスのテーブル要素から血統表の3行目(母馬の行)を検索し、
b_fmlクラス(雌馬を示す)のセルから母馬名を取得する。リンク要素(a)が
存在する場合はリンクテキストを優先し、なければ直接的なテキストを取得する。
改行文字は除去して整形した母馬名を返却する。
:param soup: 解析対象のHTMLコンテンツ
:type soup: BeautifulSoup
: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]
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 ""
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)
# ├─ direction: TEXT - コース方向(右/左)
# ├─ start_time: TEXT - 発走時刻
# ├─ year: INTEGER - 開催年
# ├─ month: INTEGER - 開催月
# └─ created_at: TIMESTAMP - 作成日時
#
# - race_horses(出走馬情報)
# ├─ id: INTEGER PRIMARY KEY - 主キー
# ├─ race_id: INTEGER - レースID(外部キー)
# ├─ horse_name: TEXT - 馬名
# ├─ frame_number: INTEGER - 枠番
# ├─ horse_number: INTEGER - 馬番
# ├─ father: TEXT - 父馬名
# ├─ mother: TEXT - 母馬名
# ├─ father_father: TEXT - 父の父
# ├─ father_mother: TEXT - 父の母
# ├─ mother_father: TEXT - 母の父
# ├─ mother_mother: TEXT - 母の母
# ├─ age: TEXT - 性齢
# ├─ rider_weight: REAL - 斤量
# ├─ rider: TEXT - 騎手名
# ├─ trainer: TEXT - 調教師名
# ├─ owner: TEXT - 馬主名
# ├─ birth_date: TEXT - 生年月日
# ├─ breeder: TEXT - 生産者名
# ├─ birthplace: 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(): 必要なテーブル構造の作成
# - _add_race_columns_if_not_exist(): racesテーブルの動的カラム追加
# - _add_horse_columns_if_not_exist(): race_horsesテーブルの動的カラム追加
#
# 2. データ操作メソッド:
# - save_race_result(): レース結果の保存
# └─ レース情報と出走馬情報を適切なテーブルに保存
# └─ 馬の詳細情報(馬主、生産者、産地等)の保存
# └─ 詳細血統情報(父の父、父の母等)の保存
# - _convert_horse_numeric_data(): 馬情報の数値変換
# - _safe_int_convert(): 安全な整数変換
# - 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
# # レース結果をデータベースに保存
# horse_details = [
# {'owner': '○○牧場', 'birth_date': '2020年3月15日',
# 'breeder': '△△ファーム', 'birthplace': '北海道'},
# # ... 他の馬の詳細情報
# ]
#
# success = db_manager.save_race_result(
# "https://db.netkeiba.com/race/202301010101/",
# race_data_df, # pandas DataFrame
# 2023, # 年
# 1, # 月
# horse_details # 馬詳細情報リスト
# )
# ```
#
# 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)}")
# print(f"カラム一覧: {list(march_results.columns)}")
# march_results.to_csv("2024_3_results.csv", index=False)
# ```
#
# ==============================================================================
# 注意事項
# ==============================================================================
# 1. データベース操作:
# - トランザクション処理により整合性を確保
# - 自動的にロールバックとコネクション管理を実行
# - 複数プロセスからの同時アクセスには注意が必要
# - 既存テーブルに対する動的カラム追加機能により後方互換性を維持
#
# 2. データ型と変換:
# - 数値データは安全に変換され、変換不能な場合はNoneが設定
# - タイムスタンプはSQLiteのCURRENT_TIMESTAMPで自動設定
# - テキストデータのエンコーディングはUTF-8を使用
# - 枠番、馬番は整数型として安全に変換
#
# 3. エラーハンドリング:
# - すべてのデータベース操作は例外処理で保護
# - エラーが発生した場合はコンソールにメッセージを出力
# - データベース初期化エラーはアプリケーション起動時に検出可能
# - カラム追加時のエラーも適切に処理され、処理は継続される
#
# 4. 馬詳細情報の管理:
# - 馬主、生産者、産地、生年月日等の包括的な情報を管理
# - 詳細血統情報(父の父、父の母、母の父、母の母)を保存
# - 調教師情報の重複管理(レース結果と馬詳細の両方で保存)
# - 情報が取得できない場合は空文字として保存
import sqlite3
import pandas as pd
import os
from contextlib import contextmanager
class DatabaseManager:
"""
レース結果データベースを管理するクラス(拡張版)
"""
def __init__(self, db_path):
"""
DatabaseManagerの初期化
"""
self.db_path = db_path
self._ensure_db_directory()
self._initialize_database()
def _ensure_db_directory(self):
"""
データベースファイルを配置するディレクトリの存在を確認し、存在しない場合は作成する。
データベースファイルパスから親ディレクトリを取得し、os.makedirsでディレクトリ構造を
再帰的に作成する。既存のディレクトリがある場合は何も行わない。
:return: なし
:rtype: None
"""
db_dir = os.path.dirname(self.db_path)
os.makedirs(db_dir, exist_ok=True)
@contextmanager
def _get_connection(self):
"""
SQLiteデータベースへの接続をコンテキストマネージャとして提供する。
with文で使用することで自動的にコネクションのクローズとエラー時のロールバックを
実行する。データベース操作の安全性と確実なリソース管理を保証する。
:return: SQLite接続のコンテキストマネージャ
:rtype: contextmanager
:raises sqlite3.Error: データベース接続エラー時
"""
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の3つのテーブルを作成し、既存テーブルには
新しいカラムを追加してスキーマを最新状態に更新する。
:return: なし
:rtype: None
:raises sqlite3.Error: テーブル作成やカラム追加時のエラー
"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
# racesテーブル(directionとstart_timeを含む)
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,
direction TEXT,
start_time TEXT,
year INTEGER,
month INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# race_horsesテーブル(directionとstart_timeを含まない)
cursor.execute('''
CREATE TABLE IF NOT EXISTS race_horses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
race_id INTEGER,
horse_name TEXT,
frame_number INTEGER,
horse_number INTEGER,
father TEXT,
mother TEXT,
father_father TEXT,
father_mother TEXT,
mother_father TEXT,
mother_mother TEXT,
age TEXT,
rider_weight REAL,
rider TEXT,
trainer TEXT,
owner TEXT,
birth_date TEXT,
breeder TEXT,
birthplace TEXT,
odds REAL,
popular INTEGER,
horse_weight TEXT,
time TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (race_id) REFERENCES races(race_id)
)
''')
# racesテーブルに新しいカラムを追加(もし存在しない場合)
self._add_race_columns_if_not_exist(cursor)
# race_horsesテーブルに新しいカラムを追加(もし存在しない場合)
self._add_horse_columns_if_not_exist(cursor)
# processed_urlsテーブル
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 _add_race_columns_if_not_exist(self, cursor):
"""
racesテーブルに新しいカラムが存在しない場合に動的に追加する。
PRAGMA table_infoでテーブル構造を取得し、direction(コース方向)と
start_time(発走時刻)のカラムが存在しない場合にALTER TABLEで追加する。
:param cursor: SQLiteデータベースのカーソルオブジェクト
:type cursor: sqlite3.Cursor
:return: なし
:rtype: None
:raises sqlite3.Error: カラム追加処理時のエラー
"""
try:
# 既存のカラム一覧を取得
cursor.execute("PRAGMA table_info(races)")
existing_columns = [column[1] for column in cursor.fetchall()]
# racesテーブル用の新しいカラムのリスト
new_columns = [
('direction', 'TEXT'),
('start_time', 'TEXT')
]
# 存在しないカラムを追加
for column_name, column_type in new_columns:
if column_name not in existing_columns:
cursor.execute(f'ALTER TABLE races ADD COLUMN {column_name} {column_type}')
print(f"racesテーブルにカラム '{column_name}' を追加しました")
except sqlite3.Error as e:
print(f"racesテーブルのカラム追加エラー: {e}")
def _add_horse_columns_if_not_exist(self, cursor):
"""
race_horsesテーブルに新しいカラムが存在しない場合に動的に追加する。
PRAGMA table_infoでテーブル構造を取得し、枠番、馬番、詳細血統情報、
馬詳細情報(調教師、馬主、生年月日等)のカラムが存在しない場合にALTER TABLEで追加する。
:param cursor: SQLiteデータベースのカーソルオブジェクト
:type cursor: sqlite3.Cursor
:return: なし
:rtype: None
:raises sqlite3.Error: カラム追加処理時のエラー
"""
try:
# 既存のカラム一覧を取得
cursor.execute("PRAGMA table_info(race_horses)")
existing_columns = [column[1] for column in cursor.fetchall()]
# race_horsesテーブル用の新しいカラムのリスト(directionとstart_timeは含まない)
new_columns = [
('frame_number', 'INTEGER'),
('horse_number', 'INTEGER'),
('father_father', 'TEXT'),
('father_mother', 'TEXT'),
('mother_father', 'TEXT'),
('mother_mother', 'TEXT'),
('trainer', 'TEXT'),
('owner', 'TEXT'),
('birth_date', 'TEXT'),
('breeder', 'TEXT'),
('birthplace', 'TEXT')
]
# 存在しないカラムを追加
for column_name, column_type in new_columns:
if column_name not in existing_columns:
cursor.execute(f'ALTER TABLE race_horses ADD COLUMN {column_name} {column_type}')
print(f"race_horsesテーブルにカラム '{column_name}' を追加しました")
except sqlite3.Error as e:
print(f"race_horsesテーブルのカラム追加エラー: {e}")
def _add_columns_if_not_exist(self, cursor):
"""
既存のrace_horsesテーブルに新しいカラムを動的に追加する汎用メソッド。
PRAGMA table_infoで既存カラムを確認し、不足している全ての新規カラム
(枠番、馬番、血統詳細、馬情報、コース情報等)をALTER TABLEで一括追加する。
:param cursor: SQLiteデータベースのカーソルオブジェクト
:type cursor: sqlite3.Cursor
:return: なし
:rtype: None
:raises sqlite3.Error: カラム追加処理時のエラー
"""
try:
# 既存のカラム一覧を取得
cursor.execute("PRAGMA table_info(race_horses)")
existing_columns = [column[1] for column in cursor.fetchall()]
# 新しいカラムのリスト
new_columns = [
('frame_number', 'INTEGER'),
('horse_number', 'INTEGER'),
('father_father', 'TEXT'),
('father_mother', 'TEXT'),
('mother_father', 'TEXT'),
('mother_mother', 'TEXT'),
('trainer', 'TEXT'),
('owner', 'TEXT'),
('birth_date', 'TEXT'),
('breeder', 'TEXT'),
('birthplace', 'TEXT'),
('direction', 'TEXT'),
('start_time', 'TEXT')
]
# 存在しないカラムを追加
for column_name, column_type in new_columns:
if column_name not in existing_columns:
cursor.execute(f'ALTER TABLE race_horses ADD COLUMN {column_name} {column_type}')
print(f"カラム '{column_name}' を追加しました")
except sqlite3.Error as e:
print(f"カラム追加エラー: {e}")
def save_race_result(self, url, race_data, year, month, horse_details=None):
"""
レース結果データを包括的にデータベースに保存し、処理済みURLとして記録する。
racesテーブルにレース基本情報を、race_horsesテーブルに出走馬情報と血統詳細、
馬詳細情報を保存し、processed_urlsテーブルに成功ステータスで記録する。
:param url: レース結果のURL
:type url: str
:param race_data: レース結果のDataFrameまたはSeries
:type race_data: pd.DataFrame or pd.Series
:param year: レースの開催年
:type year: int
:param month: レースの開催月
:type month: int or str
:param horse_details: 馬の詳細情報リスト(馬主、生年月日、生産者等)
:type horse_details: list, optional
:return: 保存が成功した場合はTrue、失敗した場合はFalse
: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, direction, start_time, year, month)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
first_row['開催日'],
first_row['レース名'],
first_row['開催場所'],
first_row['ラウンド'],
first_row['天候'],
first_row['馬場'],
first_row['状態'],
distance,
first_row.get('コース方向', ''),
first_row.get('発走時刻', ''),
year,
month
))
race_id = cursor.lastrowid
# 出走馬情報を保存(修正版 - カラム数を正確に合わせる)
if isinstance(race_data, pd.DataFrame):
horse_data = race_data
else:
horse_data = pd.DataFrame([race_data])
for i, horse in horse_data.iterrows():
# 数値データの安全な変換
odds, rider_weight, popular = self._convert_horse_numeric_data(horse)
frame_number = self._safe_int_convert(horse.get('枠番', ''))
horse_number = self._safe_int_convert(horse.get('馬番', ''))
# 馬詳細情報の取得
horse_detail = horse_details[i] if horse_details and i < len(horse_details) else {}
# SQLクエリを正確なカラム数で実行(22個のパラメータ)
cursor.execute('''
INSERT INTO race_horses
(race_id, horse_name, frame_number, horse_number, father, mother,
father_father, father_mother, mother_father, mother_mother,
age, rider_weight, rider, trainer, owner, birth_date, breeder, birthplace,
odds, popular, horse_weight, time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
race_id,
horse['馬名'],
frame_number,
horse_number,
horse.get('父親', ''),
horse.get('母親', ''),
horse.get('父の父', ''),
horse.get('父の母', ''),
horse.get('母の父', ''),
horse.get('母の母', ''),
horse['性齢'],
rider_weight,
horse['騎手'],
horse.get('調教師', horse_detail.get('trainer', '')),
horse_detail.get('owner', ''),
horse_detail.get('birth_date', ''),
horse_detail.get('breeder', ''),
horse_detail.get('birthplace', ''),
odds,
popular,
horse['馬体重'],
horse['タイム']
))
# 処理済みURLを記録
cursor.execute('''
INSERT OR REPLACE INTO processed_urls (url, race_id, status)
VALUES (?, ?, 'success')
''', (url, race_id))
conn.commit()
print(f"データベース保存成功: レースID {race_id}")
return True
except Exception as e:
print(f"データ保存エラー: {e}")
print(f"エラー発生URL: {url}")
# デバッグ情報を出力
if isinstance(race_data, pd.DataFrame):
print(f"データフレーム形状: {race_data.shape}")
print(f"カラム一覧: {list(race_data.columns)}")
return False
except Exception as e:
print(f"データ保存エラー: {e}")
print(f"エラー発生URL: {url}")
# デバッグ情報を出力
if isinstance(race_data, pd.DataFrame):
print(f"データフレーム形状: {race_data.shape}")
print(f"カラム一覧: {list(race_data.columns)}")
return False
def _safe_int_convert(self, value):
"""
文字列値を安全に整数型に変換し、変換不能な場合はNoneを返す。
空文字、None、変換不可能な値に対して例外を発生させずに
適切にNoneを返すことで、データベース保存処理の安全性を確保する。
:param value: 変換対象の値
:type value: any
:return: 変換された整数値、または変換不能な場合はNone
:rtype: int or None
"""
try:
return int(value) if value and str(value).strip() else None
except (ValueError, TypeError):
return None
def _convert_horse_numeric_data(self, horse):
"""
出走馬データから数値項目(オッズ、斤量、人気順位)を安全に変換する。
pandas.notnaでNull値をチェックし、空文字や変換不能な値に対して
適切にNoneを設定することで、データベース保存時のエラーを防ぐ。
:param horse: 出走馬の1行分のデータ(pandas Series)
:type horse: pd.Series
:return: (オッズ, 斤量, 人気順位)のタプル(変換不能時はNone)
:rtype: tuple(float or None, float or None, int or None)
"""
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をprocessed_urlsテーブルに'failed'ステータスで記録する。
INSERT OR REPLACEを使用して既存レコードがある場合は更新し、
再実行時に失敗URLを自動的にスキップできるようにする。
:param url: 失敗したURL
:type url: str
:return: なし
:rtype: None
:raises sqlite3.Error: データベース操作エラー時
"""
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のみを抽出して返す。
processed_urlsテーブルを検索して処理済みURLを特定し、
元のリストから処理済みURLを除外した未処理URLのリストを生成する。
:param url_list: 確認対象のURLリスト
:type url_list: list
:return: 未処理のURLリスト
:rtype: list
:raises sqlite3.Error: データベース検索エラー時
"""
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リストの処理状況を確認し、成功と失敗に分類して返す。
processed_urlsテーブルからstatusカラムを参照して、
'success'と'failed'のステータス別にURLを分類したタプルを返す。
:param url_list: 確認対象のURLリスト
:type url_list: list
:return: (成功URLリスト, 失敗URLリスト)のタプル
:rtype: tuple(list, list)
:raises sqlite3.Error: データベース検索エラー時
"""
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として取得する。
racesとrace_horsesテーブルをJOINして、レース基本情報、出走馬情報、
血統詳細情報、馬詳細情報を統合したDataFrameを生成し返す。
:param year: 取得対象の年
:type year: int
:param month: 取得対象の月
:type month: int or str
:return: 指定年月のレース結果DataFrame(レコードなしの場合は空DataFrame)
:rtype: pd.DataFrame
:raises Exception: データベース検索やDataFrame生成時のエラー
"""
query = '''
SELECT
rh.time as タイム,
rh.horse_name as 馬名,
rh.frame_number as 枠番,
rh.horse_number as 馬番,
rh.father as 父親,
rh.mother as 母親,
rh.father_father as 父の父,
rh.father_mother as 父の母,
rh.mother_father as 母の父,
rh.mother_mother as 母の母,
rh.age as 性齢,
rh.rider_weight as 斤量,
rh.rider as 騎手,
rh.trainer as 調教師,
rh.owner as 馬主,
rh.birth_date as 生年月日,
rh.breeder as 生産者,
rh.birthplace as 産地,
rh.odds as 単勝,
rh.popular as 人気,
rh.horse_weight as 馬体重,
r.distance as 距離,
r.direction as コース方向,
r.start_time 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()
scraper_manager.py (統括管理)
# ==============================================================================
# 競馬レース結果スクレイピング統括管理モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果をスクレイピングする全プロセスを管理・制御するモジュール。
# 年月指定でのデータ収集から、詳細情報の抽出、データベースへの保存、CSVファイル出力までの一連の流れを統括します。
# 進捗管理や実行時間予測、エラーハンドリングなどの機能も提供します。
#
# 本モジュールでは、基本的なレース情報に加えて、以下の情報を取得します:
# - 枠番・馬番・調教師情報の取得
# - 詳細血統情報(父の父、父の母、母の父、母の母)の取得
# - 馬詳細情報(馬主、生年月日、生産者、産地)の取得
# - コース方向・発走時刻情報の取得
# - Selenium+requestsの組み合わせ処理による効率化
#
# プログラムの主な機能:
# 1. スクレイピング処理の統括管理
# - 年月指定でのレース結果収集
# - バッチ処理による負荷分散
# - 詳細レース情報の取得と保存
# - CSV出力とデータ統合
# 2. 進捗・状態管理
# - 詳細な進捗状況の表示
# - 残り時間の予測と完了時刻の計算
# - 処理済み/未処理URLの管理
# - エラーハンドリングと再開可能な設計
# 3. データ整形と出力
# - 月別CSVファイルの作成
# - 年間統合CSVの生成
# - レースリンク情報のJSON保存
# - データフォーマットの標準化
# 4. データ取得機能
# - Seleniumによる血統情報の取得
# - 馬詳細ページからの情報抽出
# - エラー処理強化による安定性向上
#
# ==============================================================================
# スクレイピングプロセスの流れ
# ==============================================================================
# 1. 初期化と準備:
# - 設定管理とデータベース管理のインスタンス受け取り
# - Webドライバと要求管理のインスタンス作成
# - 処理開始時間の記録と年月ペアの生成
#
# 2. 月別データ収集処理:
# - 各月ごとの処理実行
# ├─ ブラウザ起動とネット競馬サイトへのアクセス
# ├─ 検索条件設定(年月・競馬場種別指定)
# ├─ 検索実行と結果ページからのレースリンク収集
# ├─ 未処理URLの特定とバッチ処理の準備
# ├─ 各レースページへのアクセスと情報抽出
# │ ├─ レース基本情報の取得(レース名、日付、場所、コース方向、発走時刻など)
# │ ├─ レース結果テーブルの解析(着順、馬名、枠番、馬番、調教師など)
# │ ├─ 出走馬の詳細血統情報の取得(父の父、父の母、母の父、母の母)
# │ └─ 馬詳細情報の取得(馬主、生年月日、生産者、産地)
# ├─ 抽出データのデータベースへの保存(詳細テーブル構造)
# ├─ 月別CSVファイルの作成(包括的カラム対応)
# ├─ レースリンク情報のJSON保存
# └─ 進捗状況と予想終了時刻の更新
#
# 3. バッチ処理による負荷軽減:
# - 指定数のURLごとにバッチとして処理
# ├─ 各バッチの進捗表示(tqdmプログレスバー)
# ├─ 個別URL処理の成功/失敗の追跡
# ├─ バッチ間の待機時間確保(設定可能)
# └─ 次バッチの開始時刻予告
#
# 4. ハイブリッド処理による効率化:
# - レース基本情報:requestsによる高速取得
# - 血統・馬詳細情報:Seleniumによる確実な取得
# - エラー時の自動リトライとフォールバック
#
# 5. データ統合と完了処理:
# - 全月の処理完了後の年間データ統合
# ├─ 月別CSVファイルの読み込みと結合
# ├─ 年間統合CSVファイルの作成
# ├─ 実行時間情報の表示
# └─ 処理完了の通知
#
# ==============================================================================
# メソッドと機能の詳細
# ==============================================================================
# 1. スクレイピング実行と管理:
# - run_scraping(): スクレイピング処理全体の実行
# - scrape_race_details(): 指定年月のレース詳細取得
# - _fetch_race_details(): バッチ処理でのURL処理
# - _scrape_single_race_extended(): 単一レースのスクレイピング
# - _save_race_links_info(): レースリンク情報の保存
#
# 2. 包括的データ抽出と整形:
# - _extract_race_info(): レース基本情報の抽出(コース方向・発走時刻含む)
# - _get_horse_pedigree_extended(): 詳細血統情報の取得(4世代)
# - _get_horse_details_extended(): 馬詳細情報の取得(馬主・生産者等)
# - _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: 血統情報の解析(詳細版)
# └─ HorseDetailParser: 馬詳細情報の解析
#
# 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
# └─ 取得したリンク一覧と未処理リンク情報
#
# 3. カラム構成:
# タイム, 馬名, 枠番, 馬番, 父親, 母親, 父の父, 父の母, 母の父, 母の母,
# 性齢, 斤量, 騎手, 調教師, 馬主, 生年月日, 生産者, 産地,
# 単勝, 人気, 馬体重, 距離, コース方向, 発走時刻, 天候, 馬場, 状態,
# 開催日, レース名, 開催場所, ラウンド
#
# ==============================================================================
# 使用例
# ==============================================================================
# 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. 馬詳細情報取得:
# - 馬主、生年月日、生産者、産地の包括的情報
# - 調教師情報の追加取得
# - 馬プロフィールテーブルからの詳細抽出
#
# 3. レース拡張情報:
# - 枠番・馬番の正確な取得
# - コース方向(右/左)の識別
# - 発走時刻の詳細取得
# - 距離情報の正確な数値化
#
# 4. 処理効率化:
# - ハイブリッド処理(requests + Selenium)
# - バッチ処理による負荷分散
# - エラーハンドリングの強化
# - 再実行時の重複処理回避
#
# ==============================================================================
# 注意事項と運用上の留意点
# ==============================================================================
# 1. リソース管理:
# - 長時間実行を前提とした設計(詳細データ取得により処理時間増加)
# - 安定したネットワーク接続が必要
# - 断続的な処理のため、コンピュータがスリープしないよう設定
# - 血統・詳細情報取得のためSeleniumブラウザリソースの管理重要
#
# 2. エラーハンドリング:
# - 個別のレース取得に失敗しても全体処理は継続
# - 失敗したURLは記録され再実行時に自動的にスキップ
# - 血統・詳細情報取得失敗時は空文字で対応
# - 予期せぬエラーでもできるだけ回復する設計
# - 中断後の再開が可能(処理済みURLは再処理しない)
#
# 3. サイト負荷対策:
# - バッチサイズは適切に設定(デフォルト: 40件)
# - バッチ間の待機時間を確保(デフォルト: 10分)
# - リクエスト間のランダム待機時間を導入
# - 血統・詳細情報取得時の追加待機制御
# - サイトの利用規約を確認し、過度の負荷をかけない配慮
#
# 4. データ品質管理:
# - 詳細情報の取得失敗時はログ出力で状況確認
# - データベース保存時の型変換エラーハンドリング
# - CSV出力時のエンコーディング問題への対応
# - 重複データの確認と整合性維持
#
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, HorseDetailParser
class ScraperManager:
"""
スクレイピングプロセス全体を管理するクラス(拡張版)
"""
def __init__(self, config_manager, db_manager):
"""
ScraperManagerの初期化
"""
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):
"""
指定された年月のレース詳細情報を包括的にスクレイピングする。
ブラウザを初期化してレース検索を実行し、検索結果からレースリンクを収集、
各レースの詳細情報(基本情報、血統情報、馬詳細情報)を取得する。
処理完了後はCSVファイルおよびJSONファイルとして保存する。
:param year: 取得対象の年
:type year: int or str
:param month: 取得対象の月
:type month: int or str
: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}月のレースデータが見つかりませんでした")
self.webdriver.close_browser()
return False, [], []
# 詳細情報の取得
unprocessed_links = self._fetch_race_details(link_list, year, month)
# ブラウザを閉じる
self.webdriver.close_browser()
# 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}")
self.webdriver.close_browser()
return False, [], []
def _fetch_race_details(self, link_list, year, month):
"""
レース詳細情報をバッチ処理で効率的に取得する。
未処理URLのみを抽出してバッチサイズごとに分割し、各バッチの処理後に
待機時間を設ける。個別レースの取得失敗時も全体処理を継続し、
失敗URLは記録して再実行時にスキップする。
:param link_list: 処理対象のレースURLリスト
:type link_list: list
:param year: レース開催年
:type year: int or str
:param month: レース開催月
:type month: int or str
: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, horse_details = self._scrape_single_race_extended(url)
if isinstance(race_data, str):
print(f"レース情報の取得に失敗しました: {url}")
self.db.record_failed_url(url)
elif not race_data.empty:
# 拡張版の保存メソッドを使用
success = self.db.save_race_result(url, race_data, year, month, horse_details)
if success and 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 _scrape_single_race_extended(self, url):
"""
単一レースの包括的な情報をスクレイピングする。
requests(高速)でレース基本情報と結果テーブルを取得し、
Selenium(確実)で血統情報と馬詳細情報を取得する組み合わせ処理。
取得したデータは整形してデータベース保存用の形式に変換する。
:param url: レース詳細ページのURL
:type url: str
:return: (整形済みレース結果DataFrame, 馬詳細情報リスト)のタプル。
取得失敗時は(エラーメッセージ文字列, 空リスト)
:rtype: tuple(pd.DataFrame or str, list)
"""
try:
# Step 1: レース基本情報と結果をrequestsで取得
soup_basic = self.request.get_html(url)
if not soup_basic:
return "HTMLの取得に失敗しました", []
# レース結果テーブルを解析(拡張版)
race_results = RaceResultsParser.parse_race_results(soup_basic)
if race_results.empty:
return "レース結果の解析に失敗しました", []
# レース基本情報の取得
race_info = self._extract_race_info(soup_basic)
# Step 2: 血統情報と馬詳細情報をSeleniumで取得
horse_pedigree = self._get_horse_pedigree_extended(soup_basic)
# 馬名リストを取得してhorse_detailsメソッドに渡す
horse_names = race_results['馬名'].tolist() if '馬名' in race_results.columns else []
horse_details = self._get_horse_details_extended(soup_basic, horse_names)
# 結果の整形(拡張版)
formatted_results = RaceResultsParser.format_race_results(
race_results, race_info, horse_pedigree
)
return formatted_results, horse_details
except Exception as e:
print(f"レース {url} のスクレイピング中にエラー発生: {e}")
return f"エラー: {str(e)}", []
def _get_horse_pedigree_extended(self, soup):
"""
出走馬の詳細血統情報(4世代)を取得する。
レーステーブルから出走馬の詳細ページリンクを抽出し、各馬の血統ページから
父、母、父の父、父の母、母の父、母の母の情報を取得する。
Seleniumブラウザを使用して確実にデータを取得し、エラー時は空文字で対応する。
:param soup: レースページのHTMLコンテンツ(BeautifulSoup)
:type soup: BeautifulSoup
:return: (父馬リスト, 母馬リスト, 父の父リスト, 父の母リスト, 母の父リスト, 母の母リスト)の6つのタプル
:rtype: tuple(list, list, list, list, list, list)
"""
try:
race_table_data = soup.find(class_='race_table_01 nk_tb_common')
if not race_table_data:
print("レーステーブルが見つかりません")
return ([], [], [], [], [], [])
# 出走馬のリンクを取得
horse_links = HorsePedigreeParser.get_horse_links(
race_table_data,
self.config.config["base_url"]
)
if not horse_links:
print("出走馬のリンクが取得できませんでした")
return ([], [], [], [], [], [])
print(f"詳細血統情報を取得する馬の数: {len(horse_links)}")
# ブラウザが初期化されていない場合は初期化
if not self.webdriver.browser:
print("血統情報取得用にブラウザを初期化します")
browser = self.webdriver.initialize_browser()
if not browser:
print("ブラウザの初期化に失敗しました")
empty_lists = [[""] * len(horse_links) for _ in range(6)]
return tuple(empty_lists)
# 詳細血統情報を取得
father_list = []
mother_list = []
father_father_list = []
father_mother_list = []
mother_father_list = []
mother_mother_list = []
for url in horse_links:
try:
# 馬の詳細ページにアクセス
horse_soup = self.webdriver.get_html_selenium(url)
if horse_soup:
# 詳細血統情報を取得
father, mother, father_father, father_mother, mother_father, mother_mother = \
HorsePedigreeParser.get_horse_detailed_pedigree(horse_soup)
father_list.append(father)
mother_list.append(mother)
father_father_list.append(father_father)
father_mother_list.append(father_mother)
mother_father_list.append(mother_father)
mother_mother_list.append(mother_mother)
else:
# HTMLが取得できない場合は空文字を追加
for lst in [father_list, mother_list, father_father_list,
father_mother_list, mother_father_list, mother_mother_list]:
lst.append("")
# 次のURLアクセス前の待機
self.webdriver.wait_for_random_time(1, 2)
except Exception as e:
print(f"血統情報取得エラー ({url}): {e}")
# エラー時は空文字を追加
for lst in [father_list, mother_list, father_father_list,
father_mother_list, mother_father_list, mother_mother_list]:
lst.append("")
print(f"詳細血統情報取得完了 - 取得数: {len(father_list)}")
return father_list, mother_list, father_father_list, father_mother_list, mother_father_list, mother_mother_list
except Exception as e:
print(f"血統情報取得エラー: {e}")
# エラー時は空のリストを返す
return ([], [], [], [], [], [])
def _get_horse_details_extended(self, soup, horse_names=None):
"""
出走馬の詳細情報(馬主、生年月日、生産者、産地、調教師)を取得する。
レーステーブルから出走馬の詳細ページリンクを抽出し、各馬のプロフィール
ページから包括的な情報を取得する。Seleniumブラウザを使用して確実にデータを
取得し、取得失敗時は空の辞書で対応する。
:param soup: レースページのHTMLコンテンツ(BeautifulSoup)
:type soup: BeautifulSoup
:param horse_names: 馬名のリスト(ログ表示用、オプション)
:type horse_names: list or None
:return: 馬詳細情報の辞書のリスト。各辞書には'owner', 'birth_date', 'breeder', 'birthplace', 'trainer'キーを含む
:rtype: list
"""
try:
race_table_data = soup.find(class_='race_table_01 nk_tb_common')
if not race_table_data:
print("レーステーブルが見つかりません")
return []
# 出走馬のリンクを取得
horse_links = HorsePedigreeParser.get_horse_links(
race_table_data,
self.config.config["base_url"]
)
if not horse_links:
print("出走馬のリンクが取得できませんでした")
return []
print(f"馬詳細情報を取得する馬の数: {len(horse_links)}")
# ブラウザが初期化されていない場合は初期化
if not self.webdriver.browser:
print("馬詳細情報取得用にブラウザを初期化します")
browser = self.webdriver.initialize_browser()
if not browser:
print("ブラウザの初期化に失敗しました")
return [{} for _ in horse_links]
horse_details = []
for i, url in enumerate(horse_links):
try:
# 馬の詳細ページにアクセス
horse_soup = self.webdriver.get_html_selenium(url)
if horse_soup:
# 馬詳細情報を取得
details = {
'owner': HorseDetailParser.get_horse_owner(horse_soup),
'birth_date': HorseDetailParser.get_horse_birth_date(horse_soup),
'breeder': HorseDetailParser.get_horse_breeder(horse_soup),
'birthplace': HorseDetailParser.get_horse_birthplace(horse_soup),
'trainer': HorseDetailParser.get_horse_trainer(horse_soup)
}
horse_details.append(details)
# 馬名を使ってログ出力(馬名リストがある場合)
if horse_names and i < len(horse_names):
print(f"馬詳細情報取得: {horse_names[i]}")
else:
print(f"馬詳細情報取得: {details.get('owner', 'Unknown')}")
else:
# HTMLが取得できない場合は空の辞書を追加
horse_details.append({})
# 馬名を使ってログ出力(馬名リストがある場合)
if horse_names and i < len(horse_names):
print(f"馬詳細情報取得失敗: {horse_names[i]}")
else:
print(f"馬詳細情報取得失敗: URL {i + 1}")
# 次のURLアクセス前の待機
self.webdriver.wait_for_random_time(1, 2)
except Exception as e:
if horse_names and i < len(horse_names):
print(f"馬詳細情報取得エラー ({horse_names[i]}): {e}")
else:
print(f"馬詳細情報取得エラー ({url}): {e}")
horse_details.append({})
print(f"馬詳細情報取得完了 - 取得数: {len(horse_details)}")
return horse_details
except Exception as e:
print(f"馬詳細情報取得エラー: {e}")
return []
def _extract_race_info(self, soup):
"""
レース基本情報を抽出する。
HTMLコンテンツからレース名、開催日、場所、ラウンド、距離、コース方向、
発走時刻、天候、馬場状態などの基本情報を取得する。
データベース保存用の辞書形式で返却する。
:param soup: レースページのHTMLコンテンツ(BeautifulSoup)
:type soup: BeautifulSoup
: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, direction = RaceInfoParser.get_race_distance_and_direction(soup)
# 発走時刻を取得
start_time = RaceInfoParser.get_race_start_time(soup)
weather = RaceInfoParser.get_race_weather(soup)
ground, condition = RaceInfoParser.get_race_condition(soup)
# デバッグ出力
print(f"[DEBUG] コース方向: {direction}, 発走時刻: {start_time}")
return {
'レース名': race_name,
'開催日': race_date,
'開催場所': race_place,
'ラウンド': race_round,
'距離': distance,
'コース方向': direction, # 新規追加
'発走時刻': start_time, # 新規追加
'天候': weather,
'馬場': ground,
'状態': condition
}
def _save_race_links_info(self, year, month, all_links, unprocessed_links):
"""
レースリンク情報をJSONファイルとして保存する。
スクレイピング日時、取得したレースリンク数、未処理リンク数、全リンク一覧、
未処理リンク一覧を含む管理情報をJSON形式で保存する。再実行時の状況確認や
処理進捗の把握に活用される。
:param year: 対象年
:type year: int or str
:param month: 対象月
:type month: int or str
:param all_links: 取得した全レースリンクのリスト
:type all_links: list
:param unprocessed_links: 未処理のレースリンクのリスト
:type unprocessed_links: list
:return: なし
:rtype: None
"""
# 年別ディレクトリの確保
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 _save_to_csv(self, year, month):
"""
指定年月のレース結果をCSVファイルに保存する。
データベースから該当月のレース結果を取得し、枠番、馬番、詳細血統情報、馬詳細情報、コース方向、発走時刻等を含むカラム構成で
CSVファイルに出力する。Shift-JISエンコーディングで保存される。
:param year: 対象年
:type year: int or str
:param month: 対象月
:type month: int or str
:return: なし
:rtype: None
"""
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 year: 対象年
:type year: int or str
:param month: 対象月
:type month: int or str
:param all_links: 取得した全レースリンクのリスト
:type all_links: list
:param unprocessed_links: 未処理のレースリンクのリスト
:type unprocessed_links: list
:return: なし
:rtype: None
"""
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 year: 統合対象の年
:type year: int or str
:return: 統合されたレース結果のDataFrame
: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)
# 拡張版の列名リスト(コース方向と発走時刻を追加)
extended_columns = [
'タイム', '馬名', '枠番', '馬番', '父親', '母親', '父の父', '父の母', '母の父', '母の母',
'性齢', '斤量', '騎手', '調教師', '馬主', '生年月日', '生産者', '産地',
'単勝', '人気', '馬体重', '距離', 'コース方向', '発走時刻', '天候', '馬場', '状態',
'開催日', 'レース名', '開催場所', 'ラウンド'
]
# 各ファイルを読み込んで結合
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=extended_columns,
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 year: 取得対象の年
:type year: int
:param start_month: 開始月
:type start_month: int
:param end_month: 終了月
:type end_month: int
:return: なし
:rtype: None
"""
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}")
finally:
# 最終的にブラウザをクローズ
self.webdriver.close_browser()
def _predict_remaining_time(self, current_index, total_items, item_process_time):
"""
現在の処理状況に基づいて残り処理時間を予測する。
初回処理時は全体の予想終了時刻を計算し、2回目以降は処理実績に基づいて
更新された予想終了時刻を計算する。バッチ間の待機時間も考慮した
正確な時間予測を提供し、コンソールに表示する。
:param current_index: 現在の処理インデックス(0から開始)
:type current_index: int
:param total_items: 処理対象の総アイテム数
:type total_items: int
:param item_process_time: 1アイテムあたりの処理時間(秒)
:type item_process_time: float
:return: なし
:rtype: None
"""
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):
"""
スクレイピング処理の実行時間情報を表示する。
処理開始時刻、処理終了時刻、総実行時間(秒および分単位)を
見やすい形式でコンソールに出力する。長時間処理の完了確認や
パフォーマンス分析に活用される。
:return: なし
:rtype: None
"""
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))
prepare_raceResults.py (データ前処理)
# ==============================================================================
# 競馬レース結果データ前処理モジュール
# ==============================================================================
# プログラムの概要:
# JRA(日本中央競馬会)のレース結果データを機械学習や統計分析に適した形式に
# 整形するための前処理モジュール。スクレイピングで取得した生データから欠損値や不正確なデータを除去し、
# 数値型への変換や日付の標準化など分析に必要な一連の前処理を行います。
#
# 枠番・馬番・詳細血統情報・馬詳細情報・コース方向・発走時刻等の新しいデータ項目に対応しています。
#
# プログラムの主な機能:
# 1. 拡張データの読み込みと検証
# - 31カラムのレース結果CSVファイルの読み込み
# - ヘッダー行の有無の自動検出と処理
# - エンコーディング(Shift-JIS/cp932)の管理
# - 血統詳細情報と馬詳細情報の処理
# 2. 拡張データクリーニング
# - 欠損値を含む行の削除
# - 無効なデータ(「計不」など)の除去
# - カラムの型変換(文字列→数値、日付など)
# - 血統情報と馬詳細情報の整合性チェック
# 3. 特徴量エンジニアリング
# - タイム表記(分:秒)を秒単位に変換
# - 馬体重の数値化(括弧内の増減表記の除去)
# - 日付のdatetime型への変換
# - 枠番・馬番の数値型変換
# - 生年月日の日付型変換
# - 発走時刻の時刻型変換
#
# ==============================================================================
# 実行手順と前提条件(拡張版)
# ==============================================================================
# 1. 必要なライブラリのインストール:
# ```
# pip install pandas numpy
# ```
#
# 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ファイルを検出
# - ファイル先頭部分を読み込みヘッダー行の有無を確認
# - ヘッダー行がある場合は削除してカラム名を統一
# - 31カラムの拡張データ構造に対応
#
# 2. 拡張データクリーニング処理:
# - 欠損値(NaN)を含む行を削除
# - 「計不」を含む馬体重データを持つ行を削除
# - 単勝オッズに「,」や「---」を含む行を削除
# - 日付を「%Y年%m月%d日」形式からdatetime型に変換
# - 生年月日の日付型変換と検証
# - 枠番・馬番の数値型変換
# - 発走時刻の時刻型変換
#
# 3. 拡張データ変換処理:
# - タイム列(「分:秒」形式)を秒単位の浮動小数点数に変換
# 例: "1:45.3" → 105.3秒
# - 馬体重から括弧内の増減表記を削除し数値のみを抽出
# 例: "480(+4)" → 480.0
# - 単勝オッズを文字列から浮動小数点数に変換
# - 斤量の数値型変換
# - 人気順位の数値型変換
#
# ==============================================================================
# データ項目の詳細
# ==============================================================================
# 1. カラム構成(31カラム):
# - time: タイム(秒単位の浮動小数点数)
# - horse: 馬名
# - frame_number: 枠番(整数)
# - horse_number: 馬番(整数)
# - father: 父馬名
# - mother: 母馬名
# - father_father: 父の父
# - father_mother: 父の母
# - mother_father: 母の父
# - mother_mother: 母の母
# - age: 性齢
# - rider_weight: 斤量(浮動小数点数)
# - rider: 騎手名
# - trainer: 調教師名
# - owner: 馬主名
# - birth_date: 生年月日(日付型)
# - breeder: 生産者名
# - birthplace: 産地
# - odds: 単勝オッズ(浮動小数点数)
# - popular: 人気順位(整数)
# - horse_weight: 馬体重(浮動小数点数)
# - distance: レース距離(整数)
# - direction: コース方向
# - start_time: 発走時刻(時刻型)
# - weather: 天候
# - ground: 馬場(芝/ダート)
# - condition: 馬場状態
# - date: レース日(datetime型)
# - race_name: レース名
# - location: 開催場所
# - round: ラウンド情報
#
# ==============================================================================
import pandas as pd
import numpy as np
import os
import time
import glob
import re
from datetime import datetime
class RaceDataProcessor:
"""
拡張されたレース結果データの前処理を行うクラス(拡張版)
"""
def __init__(self, base_folder_path, output_folder_path):
"""
拡張されたレース結果データの前処理クラスを初期化する。
入力フォルダパスと出力フォルダパスを設定し、31カラムの拡張されたレース結果のカラム名を定義する。
血統詳細情報、馬詳細情報、コース情報等を含むデータ構造に対応したカラム構成を設定する。
:param base_folder_path: レース結果が保存されている基本フォルダのパス
:type base_folder_path: str
:param output_folder_path: 加工後のレース結果を保存するフォルダのパス
:type output_folder_path: str
"""
self.base_folder_path = base_folder_path
self.output_folder_path = output_folder_path
# 拡張版カラム構成(31カラム)
self.columns = [
'time', 'horse', 'frame_number', 'horse_number', 'father', 'mother',
'father_father', 'father_mother', 'mother_father', 'mother_mother',
'age', 'rider_weight', 'rider', 'trainer', 'owner', 'birth_date',
'breeder', 'birthplace', 'odds', 'popular', 'horse_weight', 'distance',
'direction', 'start_time', 'weather', 'ground', 'condition',
'date', 'race_name', 'location', 'round'
]
# 日本語カラム名(ヘッダー検出用)
self.japanese_columns = [
'タイム', '馬名', '枠番', '馬番', '父親', '母親',
'父の父', '父の母', '母の父', '母の母',
'性齢', '斤量', '騎手', '調教師', '馬主', '生年月日',
'生産者', '産地', '単勝', '人気', '馬体重', '距離',
'コース方向', '発走時刻', '天候', '馬場', '状態',
'開催日', 'レース名', '開催場所', 'ラウンド'
]
def load_data(self):
"""
指定されたフォルダ内の年別フォルダから全てのall_raceresults.csvファイルを読み込む。
31カラムの拡張されたデータ構造に対応し、血統詳細情報、馬詳細情報、コース情報等を含む
レース結果データを処理する。ヘッダー行の自動検出機能により、既存ファイルとの互換性を保ちながら、
データの整合性を確保する。各ファイルの読み込み状況とデータ品質を検証し、処理進捗をコンソールに出力する。
:return: 読み込んだ各ファイルのDataFrameオブジェクトを要素とするリスト。
各DataFrameは31カラムの拡張データ構造を持ち、血統詳細情報と馬詳細情報を含む
:rtype: list[pd.DataFrame]
"""
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:
# 最初の10行をチェックしてヘッダー行があるか確認
with open(all_results_file, 'r', encoding='cp932') as f:
first_lines = [f.readline() for _ in range(10)]
# ヘッダー行があるか確認(拡張版)
has_header = any(
any(jp_col in line for jp_col in self.japanese_columns[:5]) # 主要カラムで判定
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('タイム|time', na=False) |
df['horse'].astype(str).str.contains('馬名|horse', na=False) |
df['horse_weight'].astype(str).str.contains('馬体重|weight', na=False)
].index
df = df.drop(header_rows)
# データ型の基本検証
if len(df.columns) != len(self.columns):
print(
f"警告: {all_results_file} のカラム数が期待値と異なります(期待値: {len(self.columns)}, 実際: {len(df.columns)})")
race_results.append(df)
print(f"ファイルを読み込みました: {all_results_file} ({len(df)}行)")
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):
"""
拡張されたレース結果データの数値変換と型変換を実行する。
タイム列の「分:秒」形式から秒単位への変換、枠番・馬番・人気順位・距離の整数型変換、
斤量の浮動小数点型変換を安全に実行する。各変換処理では例外処理により、
変換不能なデータに対してNaNを設定し、データの整合性を保つ。
変換対象カラム:time, frame_number, horse_number, popular, distance, rider_weight
:param race_results: 変換を行うレース結果のDataFrame。31カラムの拡張データ構造を前提とする
:type race_results: pd.DataFrame
:return: 数値カラムが適切に変換されたDataFrame。タイム列は秒単位、数値カラムは適切な型に変換済み
:rtype: pd.DataFrame
"""
# タイム列の変換(分:秒 → 秒)
def convert_time_to_seconds(time_str):
"""
レースタイムを「分:秒」形式から秒単位の浮動小数点数に変換する。
「1:45.3」のような分秒表記を105.3秒に変換し、既に秒数で記録されている場合は
そのまま浮動小数点数として処理する。空値、無効な形式、変換不能なデータに対しては
NaNを返却し、後続処理でのエラーを防ぐ。競馬レースタイムの標準的な表記形式に対応。
:param time_str: 変換対象のタイム文字列。「分:秒」形式(例: "1:45.3")または秒数文字列を想定
:type time_str: str or any
:return: 秒単位に変換されたタイム値。変換不能な場合はNaN
:rtype: float or np.nan
"""
try:
if pd.isna(time_str) or time_str == '' or str(time_str).strip() == '':
return np.nan
time_str = str(time_str).strip()
if ':' in time_str:
parts = time_str.split(':')
if len(parts) == 2:
minutes = float(parts[0])
seconds = float(parts[1])
return minutes * 60 + seconds
else:
# すでに秒数の場合
return float(time_str)
except (ValueError, TypeError):
return np.nan
race_results['time'] = race_results['time'].apply(convert_time_to_seconds)
# 枠番の数値変換
def safe_int_convert(value):
"""
文字列や数値データを安全に整数型に変換する。
枠番、馬番、人気順位、距離等の整数値が期待されるカラムに対して使用され、
空値、無効な文字列、変換不能なデータに対してはNaNを返却する。
浮動小数点数が渡された場合は一度floatに変換してからintに変換し、
「1.0」のような表記も適切に整数1として処理する。データの整合性を保ちながら
型変換エラーによる処理中断を防ぐ。
:param value: 変換対象の値。文字列、数値、その他の型を受け入れる
:type value: any
:return: 整数に変換された値。変換不能な場合はNaN
:rtype: int or np.nan
"""
try:
if pd.isna(value) or value == '' or str(value).strip() == '':
return np.nan
return int(float(str(value).strip()))
except (ValueError, TypeError):
return np.nan
race_results['frame_number'] = race_results['frame_number'].apply(safe_int_convert)
race_results['horse_number'] = race_results['horse_number'].apply(safe_int_convert)
race_results['popular'] = race_results['popular'].apply(safe_int_convert)
race_results['distance'] = race_results['distance'].apply(safe_int_convert)
# 斤量の数値変換
def safe_float_convert(value):
"""
文字列や数値データを安全に浮動小数点数型に変換する。
斤量等の小数点を含む数値が期待されるカラムに対して使用され、
空値、無効な文字列、変換不能なデータに対してはNaNを返却する。
競馬データでは斤量が「52.0」「58.5」のような形式で記録されており、
これらを適切にfloat型に変換する。データの整合性を保ちながら
型変換エラーによる処理中断を防ぐ。
:param value: 変換対象の値。文字列、数値、その他の型を受け入れる
:type value: any
:return: 浮動小数点数に変換された値。変換不能な場合はNaN
:rtype: float or np.nan
"""
try:
if pd.isna(value) or value == '' or str(value).strip() == '':
return np.nan
return float(str(value).strip())
except (ValueError, TypeError):
return np.nan
race_results['rider_weight'] = race_results['rider_weight'].apply(safe_float_convert)
return race_results
def clean_data(self, race_result):
"""
拡張されたレース結果データの包括的なクリーニング処理を実行する。
基本データ(タイム、馬名、日付)の欠損値除去、馬体重の「計不」データ除去と括弧内増減表記の処理、
単勝オッズの無効値(「,」「---」「取消」「除外」)の除去、レース日付・生年月日・発走時刻の
適切な型変換を行う。各処理段階での行数変化をログ出力し、データ品質の変化を追跡可能にする。
クリーニング後はインデックスをリセットして整合性を保つ。
:param race_result: クリーニングするレース結果のDataFrame。31カラムの拡張データ構造を前提とする
:type race_result: pd.DataFrame
:return: クリーニング済みのDataFrame。無効データが除去され、適切な型に変換されたデータ
:rtype: pd.DataFrame
"""
print(f"クリーニング開始: {len(race_result)}行")
# 基本的な空データを含む行を削除
initial_count = len(race_result)
race_result = race_result.dropna(subset=['time', 'horse', 'date'], how='any')
print(f"基本データ欠損除去: {initial_count} → {len(race_result)}行")
# 馬体重の処理
def clean_horse_weight(weight_str):
"""
馬体重データをクリーニングして数値型に変換する。
競馬データでは馬体重が「480(+4)」「456(-2)」のような括弧内増減表記付きで記録されており、
これらから基本体重のみを抽出してfloat型に変換する。「計不」(計量不能)や
空値に対してはNaNを返却し、後続の欠損値処理で適切に除去されるようにする。
正規表現により括弧内の増減表記を確実に除去する。
:param weight_str: 変換対象の馬体重文字列。「480(+4)」形式や「計不」等を想定
:type weight_str: str or any
:return: 数値に変換された馬体重。無効データの場合はNaN
:rtype: float or np.nan
"""
try:
if pd.isna(weight_str) or weight_str == '' or str(weight_str).strip() == '':
return np.nan
weight_str = str(weight_str).strip()
if '計不' in weight_str or weight_str == '計不':
return np.nan
# 括弧内の増減を除去(例: "480(+4)" → "480")
weight_str = re.sub(r'\([^)]*\)', '', weight_str)
return float(weight_str)
except (ValueError, TypeError):
return np.nan
race_result['horse_weight'] = race_result['horse_weight'].apply(clean_horse_weight)
# 馬体重が無効な行を削除
weight_before = len(race_result)
race_result = race_result.dropna(subset=['horse_weight'])
print(f"馬体重無効除去: {weight_before} → {len(race_result)}行")
# オッズの処理
def clean_odds(odds_str):
"""
単勝オッズデータをクリーニングして数値型に変換する。
競馬データでは単勝オッズが「1.2」「15.6」のような浮動小数点数で記録されるが、
無効なレース(取消、除外)では「取消」「除外」「---」等の文字列が記録される。
また、高額オッズでは「1,250.0」のようにカンマ区切りで表示される場合もある。
これらの無効データを検出してNaNに変換し、有効な数値のみをfloat型として返す。
:param odds_str: 変換対象の単勝オッズ文字列。数値または無効値(「取消」「---」等)を想定
:type odds_str: str or any
:return: 数値に変換された単勝オッズ。無効データの場合はNaN
:rtype: float or np.nan
"""
try:
if pd.isna(odds_str) or odds_str == '' or str(odds_str).strip() == '':
return np.nan
odds_str = str(odds_str).strip()
if ',' in odds_str or odds_str == '---' or odds_str == '取消' or odds_str == '除外':
return np.nan
return float(odds_str)
except (ValueError, TypeError):
return np.nan
race_result['odds'] = race_result['odds'].apply(clean_odds)
# オッズが無効な行を削除
odds_before = len(race_result)
race_result = race_result.dropna(subset=['odds'])
print(f"オッズ無効除去: {odds_before} → {len(race_result)}行")
# 日付の変換
def convert_date(date_str):
"""
レース開催日を日本語形式からdatetime型に変換する。
競馬データでは開催日が「2023年12月24日」のような日本語形式で記録されており、
これをpandasのdatetime型に変換して時系列分析や期間フィルタリングを可能にする。
無効な日付文字列や変換不能なデータに対してはpd.NaT(Not a Time)を返却し、
後続処理でのエラーを防ぐ。errors='coerce'により変換失敗時の例外を抑制する。
:param date_str: 変換対象の日付文字列。「YYYY年MM月DD日」形式を想定
:type date_str: str or any
:return: datetime型に変換された日付。変換不能な場合はpd.NaT
:rtype: pd.Timestamp or pd.NaT
"""
try:
if pd.isna(date_str) or date_str == '' or str(date_str).strip() == '':
return pd.NaT
date_str = str(date_str).strip()
return pd.to_datetime(date_str, format='%Y年%m月%d日', errors='coerce')
except:
return pd.NaT
race_result['date'] = race_result['date'].apply(convert_date)
# 生年月日の変換
def convert_birth_date(birth_date_str):
"""
馬の生年月日を日本語形式からdatetime型に変換する。
競馬データでは馬の生年月日が「1998年4月15日」のような日本語形式で記録されており、
これをpandasのdatetime型に変換して年齢計算や世代分析を可能にする。
無効な日付文字列や変換不能なデータに対してはpd.NaT(Not a Time)を返却し、
後続処理でのエラーを防ぐ。馬の詳細分析において重要な基本情報となる。
:param birth_date_str: 変換対象の生年月日文字列。「YYYY年MM月DD日」形式を想定
:type birth_date_str: str or any
:return: datetime型に変換された生年月日。変換不能な場合はpd.NaT
:rtype: pd.Timestamp or pd.NaT
"""
try:
if pd.isna(birth_date_str) or birth_date_str == '' or str(birth_date_str).strip() == '':
return pd.NaT
birth_date_str = str(birth_date_str).strip()
return pd.to_datetime(birth_date_str, format='%Y年%m月%d日', errors='coerce')
except:
return pd.NaT
race_result['birth_date'] = race_result['birth_date'].apply(convert_birth_date)
# 発走時刻の変換
def convert_start_time(time_str):
"""
レースの発走時刻をHH:MM形式からtime型に変換する。
競馬データでは発走時刻が「15:40」「17:10」のような24時間形式で記録されており、
これをpandasのtime型に変換してレース時間帯の分析を可能にする。
無効な時刻文字列や変換不能なデータに対してはpd.NaT(Not a Time)を返却し、
後続処理でのエラーを防ぐ。レースの開催パターン分析や時間帯別成績分析に活用される。
:param time_str: 変換対象の発走時刻文字列。「HH:MM」形式を想定
:type time_str: str or any
:return: time型に変換された発走時刻。変換不能な場合はpd.NaT
:rtype: datetime.time or pd.NaT
"""
try:
if pd.isna(time_str) or time_str == '' or str(time_str).strip() == '':
return pd.NaT
time_str = str(time_str).strip()
# HH:MM形式を想定
if ':' in time_str and len(time_str.split(':')) == 2:
return pd.to_datetime(time_str, format='%H:%M', errors='coerce').time()
return pd.NaT
except:
return pd.NaT
race_result['start_time'] = race_result['start_time'].apply(convert_start_time)
# 日付が無効な行を削除
date_before = len(race_result)
race_result = race_result.dropna(subset=['date'])
print(f"日付無効除去: {date_before} → {len(race_result)}行")
# インデックスをリセット
race_result = race_result.reset_index(drop=True)
print(f"クリーニング完了: {len(race_result)}行")
return race_result
def save_data(self, race_result):
"""
前処理済みのレース結果データをCSVファイルとして保存し、データ概要を出力する。
データの期間(最小年-最大年)に基づいたファイル名を自動生成し、拡張版であることを示す
「_extended」サフィックスを付与する。保存後はデータ概要(期間、レース数、欠損値状況)を
表示し、データ品質の確認を可能にする。Shift-JIS(cp932)エンコーディングで保存し、
日本語データの互換性を確保する。
:param race_result: 保存するレース結果のDataFrame。前処理完了済みの31カラムデータ
:type race_result: pd.DataFrame
:return: なし。ファイル保存処理とデータ概要の表示のみ実行
:rtype: None
"""
if race_result.empty:
print("保存するデータがありません")
return
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_extended.csv"
csv_path = os.path.join(self.output_folder_path, csv_name)
try:
race_result.to_csv(csv_path, encoding='cp932', index=False)
print(f"{csv_path}を作成しました。({len(race_result)}行、{len(race_result.columns)}列)")
# データ概要の表示
print("\n=== データ概要 ===")
print(f"期間: {start_year}年 - {end_year}年")
print(f"レース数: {len(race_result)}レース")
print(f"欠損値の状況:")
missing_summary = race_result.isnull().sum()
for col, missing_count in missing_summary.items():
if missing_count > 0:
missing_pct = (missing_count / len(race_result)) * 100
print(f" {col}: {missing_count}行 ({missing_pct:.1f}%)")
except Exception as e:
print(f"CSV保存エラー: {e}")
def process(self):
"""
拡張されたレース結果データの包括的な前処理フローを実行する。
年別フォルダからのCSVファイル読み込み、データクリーニング、数値変換、型変換の
一連の処理を順次実行し、機械学習や統計分析に適した形式のデータを生成する。
処理は新しいデータから古いデータの順で結合を行い、各段階での処理状況をログ出力する。
最終的に31カラムのレース結果データ(血統詳細情報、馬詳細情報、コース情報等を含む)
として統合し、拡張版CSVファイルとして保存する。
:return: なし。処理完了後は統合データがCSVファイルとして保存される
:rtype: None
"""
print("=== 拡張版レース結果データ前処理を開始します ===")
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):
print(f"\n--- {i}番目のデータファイルを処理中 ---")
current_data = race_results[i - 1].copy()
# 個別にクリーニングしてから結合
cleaned_data = self.clean_data(current_data)
transformed_data = self.transform_data(cleaned_data)
# 結合
combined_race_results = pd.concat([combined_race_results, transformed_data], axis=0, ignore_index=True)
# 中間保存(任意)
self.save_data(combined_race_results)
print("\n=== 全データ処理完了 ===")
def main():
"""
拡張版レース結果データ前処理のメイン実行関数。
プログラム全体の実行フローを制御し、処理開始時刻の記録、入出力パスの設定、
出力フォルダの作成、RaceDataProcessorインスタンスの生成と実行、
処理時間の計測と表示を行う。31カラムの拡張データ構造に対応した
包括的な前処理を実行し、機械学習や統計分析に適したデータを生成する。
:return: なし。処理完了後は拡張版CSVファイルが生成され、実行時間が表示される
:rtype: None
"""
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)
print(f"出力フォルダを作成しました: {output_folder_path}")
# 処理実行
processor = RaceDataProcessor(base_folder_path, output_folder_path)
processor.process()
# 実行時間表示
execution_time = time.time() - start_time
print(f"\n実行時間: {execution_time:.1f}秒 ({execution_time / 60:.1f}分)")
if __name__ == "__main__":
main()
requirements_keiba_ai.txt(パッケージ一覧)
alembic==1.13.2
attrs==24.2.0
beautifulsoup4==4.12.3
bs4==0.0.2
catboost==1.2.7
certifi==2024.7.4
cffi==1.17.0
charset-normalizer==3.3.2
code2flow==2.5.1
colorama==0.4.6
colorlog==6.8.2
contourpy==1.2.1
cycler==0.12.1
filelock==3.16.0
fonttools==4.53.1
git-filter-repo==2.47.0
graphviz==0.20.3
greenlet==3.0.3
h11==0.14.0
html5lib==1.1
idna==3.7
japanize-matplotlib==1.1.3
joblib==1.4.2
kiwisolver==1.4.5
lightgbm==4.5.0
lxml==5.2.2
Mako==1.3.5
MarkupSafe==2.1.5
matplotlib==3.9.1.post1
numpy==1.26.4
oauthlib==3.2.2
optuna==3.6.1
outcome==1.3.0.post0
packaging==24.1
pandas==2.2.2
pillow==10.4.0
plotly==5.24.0
psutil==7.0.0
pycparser==2.22
pyparsing==3.1.2
PySocks==1.7.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
pytz==2024.1
PyYAML==6.0.2
requests==2.32.3
requests-oauthlib==1.3.1
scikit-learn==1.5.1
scipy==1.14.0
selenium==4.23.1
setuptools==72.1.0
six==1.16.0
sniffio==1.3.1
sortedcontainers==2.4.0
soupsieve==2.5
SQLAlchemy==2.0.32
tenacity==9.0.0
threadpoolctl==3.5.0
tqdm==4.66.5
trio==0.26.2
trio-websocket==0.11.1
tweepy==4.14.0
typing_extensions==4.12.2
tzdata==2024.1
urllib3==2.2.2
webdriver-manager==4.0.2
webencodings==0.5.1
websocket-client==1.8.0
wsproto==1.2.0
xgboost==2.1.1
以上です。
コメント
お世話になっております。返信が遅くなり申し訳ございません。新しいコードを作成、投稿いただきありがとうございます。大変助かります。
早速になってしまいますが、何度も質問してしまい申し訳ございません。上記のコードを利用させていただきましてスクレイピングを行っているのですが、指定期間を1か月で実行してみたところ、24時間経過してもまだ動き続けています。現在レースID600台なので1か月分のスクレイピングに48時間程かかる計算になりますがこれは想定の範囲なのでしょうか?
1か月で1300レース程あるのでそうなってしまうのかもしれませんが・・・
また取得の際に下記のようなエラーが出てしまいます。
HTML取得エラー: HTTPConnectionPool(host=’localhost’, port=64881): Read timed out. (read timeout=120)
馬詳細情報取得失敗: ニジトタルト
[19080:12216:0822/214653.589:ERROR:google_apis\gcm\engine\connection_factory_impl.cc:483] ConnectionHandler failed with net error: -2
HTML取得エラー: HTTPConnectionPool(host=’localhost’, port=64881): Read timed out. (read timeout=120)
馬詳細情報取得失敗: ユラニュス
[19080:12216:0822/214911.834:ERROR:google_apis\gcm\engine\registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT
HTML取得エラー: HTTPConnectionPool(host=’localhost’, port=64881): Read timed out. (read timeout=120)
馬詳細情報取得失敗: ブルベアベージュ
↑
読み取りのタイムアウト?起動時間の問題?
[30972:5960:0823/221401.421:ERROR:gpu\command_buffer\service\gles2_cmd_decoder_passthrough.cc:1096] [GroupMarkerNotSet(crbug.com/242999)!:A0A02B002C260000]Automatic fallback to software WebGL has been deprecated. Please use the –enable-unsafe-swiftshader (about:flags#enable-unsafe-swiftshader) flag to opt in to lower security guarantees for trusted content.
[30972:5960:0823/221401.870:ERROR:gpu\command_buffer\service\gles2_cmd_decoder_passthrough.cc:1096] [GroupMarkerNotSet(crbug.com/242999)!:A0402B002C260000]Automatic fallback to software WebGL has been deprecated. Please use the –enable-unsafe-swiftshader (about:flags#enable-unsafe-swiftshader) flag to opt in to lower security guarantees for trusted content.
馬詳細情報取得: ミンナノユメミノル
馬詳細情報取得: オーマイグッネス
馬詳細情報取得: コロンバージュ
[30972:5960:0823/221415.544:ERROR:gpu\command_buffer\service\gles2_cmd_decoder_passthrough.cc:1096] [GroupMarkerNotSet(crbug.com/242999)!:A0A02B002C260000]Automatic fallback to software WebGL has been deprecated. Please use the –enable-unsafe-swiftshader (about:flags#enable-unsafe-swiftshader) flag to opt in to lower security guarantees for trusted content.
[30972:5960:0823/221416.169:ERROR:gpu\command_buffer\service\gles2_cmd_decoder_passthrough.cc:1096] [GroupMarkerNotSet(crbug.com/242999)!:A0402B002C260000]Automatic fallback to software WebGL has been deprecated. Please use the –enable-unsafe-swiftshader (about:flags#enable-unsafe-swiftshader) flag to opt in to lower security guarantees for trusted content.
↑
こちらは調べたところ、chromeのabout:flagsのunsafe-swiftshaderをenableに変更したのですが改善されませんでした。
こちらの環境の問題なのかもしれませんが、改善策をご教授いただけると幸いです。
度々コメントさせていただき誠に申し訳ございませんがよろしくお願いいたします。
提供いただいたログを基に、プログラムの改修を検討しています。
複数のSeleniumインスタントを並列に実行し、レース結果の取得に掛かる時間の短縮を試しています。
9/16までには公開できると思います。
コメントの返答及び、新しいコードのご検討ありがとうございます。
何度もご対応いただき非常に助かります。
コメント後も動かしていたのですが、wifiが途切れたようで最大リトライ回数を超えたようなメッセージが表示され停止していました。
もう一度動かした際もこちらの環境でwifiが途切れる時間があるみたいで取得がうまくいかないようなので時間が短縮されると私の環境でもデータの取得が可能だと思いますので、コードの公開楽しみにしております。