bvp / scraper
Boatrace Vibe Project Scraper
Requires
- php: ^8.3
- nesbot/carbon: ^2.63 || ^3.0
- psr/simple-cache: ^3.0
- symfony/browser-kit: ^7.0 || ^8.0
- symfony/cache: ^7.0 || ^8.0
- symfony/css-selector: ^7.0 || ^8.0
- symfony/http-client: ^7.0 || ^8.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.52
- phpunit/phpunit: ^12.0 || ^13.0
- vimeo/psalm: ^6.0
README
BVP Scraper は、ボートレースの公式サイトから出走表、直前情報、オッズ、結果をスクレイピングするための PHP ライブラリです。
v11 では、後継ライブラリである turnmark/scraper とは異なる方向性を持つ派生として、以下の 2 点に力を入れています。
- 鮮度に応じたキャッシュ: 確定済みの過去日のレースは不変とみなし、キャッシュに永続化。バックフィル用途で同じ日付を何度も取り直す必要がなくなります。
- インスタンス単位の並行実行: レート制御・キャッシュ参照をインスタンススコープに保持するため、プロキシやワーカーごとに複数の
Scraperインスタンスを同一プロセス内で干渉なく並行運用できます。
📦 Requirements
- php: ^8.3
- nesbot/carbon: ^2.63 || ^3.0
- psr/simple-cache: ^3.0
- symfony/browser-kit: ^7.0 || ^8.0
- symfony/cache: ^7.0 || ^8.0
- symfony/css-selector: ^7.0 || ^8.0
- symfony/http-client: ^7.0 || ^8.0
💾 Installation
composer require bvp/scraper
⚡ Usage
サポートメソッド一覧
Scraper はインスタンスベースの API です。静的なシングルトンファサードは提供していません(後述)。
| メソッド | 説明 | 引数 |
|---|---|---|
scrapeProgram($date, $stadiumNumber, $raceNumber) |
出走表を取得 | $date : Carbon対応日付文字列またはCarbonインスタンス$stadiumNumber : 1〜24$raceNumber : 1〜12 |
scrapePreview($date, $stadiumNumber, $raceNumber) |
直前情報を取得 | 同上 |
scrapeOdds($date, $stadiumNumber, $raceNumber) |
全オッズ(7種)を取得 | 同上 |
scrapeWin / scrapePlace / scrapeExacta / scrapeQuinella / scrapeQuinellaPlace / scrapeTrifecta / scrapeTrio |
単勝・複勝・2連単・2連複・拡連複・3連単・3連複のオッズを個別に取得 | 同上 |
scrapeSingle / scrapePair / scrapeTriple |
単勝・複勝 / 2連単・2連複・拡連複 / 3連単・3連複をまとめて取得 | 同上 |
scrapeResult($date, $stadiumNumber, $raceNumber) |
結果を取得 | 同上 |
scrapeStadium($date) |
開催中の場を取得 | $date のみ |
一括取得は BatchScraper が提供します。メソッド名は Scraper と同名で、引数だけが一括用($stadiumNumber/$raceNumber → $stadiumNumbers/$raceNumbers)になります。
| メソッド | 説明 | 引数 |
|---|---|---|
scrapeProgram($date, $stadiumNumbers = [], $raceNumbers = []) |
出走表を一括取得 | $stadiumNumbers/$raceNumbers 省略時はその日開催している全場・全レース |
scrapePreview / scrapeOdds / scrapeResult |
同上の一括取得版 | 同上 |
scrapeWin / scrapePlace / scrapeExacta / scrapeQuinella / scrapeQuinellaPlace / scrapeTrifecta / scrapeTrio |
同上の一括取得版 | 同上 |
scrapeSingle / scrapePair / scrapeTriple |
同上の一括取得版 | 同上 |
Deprecated:
Scraper側のscrape*Bulk()(scrapeProgramBulkなど 14 メソッド)は非推奨です。内部でBatchScraperに委譲しているため動作は変わりませんが、BatchScraperの同名メソッドへ移行してください。
$date の例
'2025-01-01''2025/01/01''yesterday'Carbon::now()->subDay()
基本的な使い方
<?php require __DIR__ . '/vendor/autoload.php'; use BVP\Scraper\Scraper; $scraper = new Scraper(); // 出走表を取得 $program = $scraper->scrapeProgram('2025-01-01', 24, 1); // 直前情報を取得 $preview = $scraper->scrapePreview('2025-01-01', 24, 1); // オッズを取得 $odds = $scraper->scrapeOdds('2025-01-01', 24, 1); // 結果を取得 $result = $scraper->scrapeResult('2025-01-01', 24, 1); print_r($program); print_r($preview); print_r($odds); print_r($result);
一括取得
use BVP\Scraper\BatchScraper; // レート制御・キャッシュを共有したい場合は既存の Scraper を渡す(省略時は内部で生成) $batchScraper = new BatchScraper($scraper); // その日開催している全場・全レースの結果を取得 $results = $batchScraper->scrapeResult('2025-01-01'); // 開催場・レースを絞り込む $results = $batchScraper->scrapeResult('2025-01-01', [24], [1, 2, 3]);
1レースの失敗で全体を落とさない(onError)
既定では、リトライを使い切ったレースが1つでもあると例外がそのまま伝播し、そのレース以降の全場・全レースが取得されません。メンテナンス中のように全体が落ちる場合は問題になりませんが、特定のレースだけが恒久的に壊れている場合、失うのは壊れたレースではなくその後ろに並んでいた全場です。
onError を渡すと、1レースの失敗はコールバックに渡されて結果から除外され、走査は続行します。ログに出すか、件数を数えて閾値で判断するかは呼び出し側で決められます。
$failures = []; $results = $batchScraper->scrapeResult('2025-01-01', onError: function ( Throwable $throwable, int $stadiumNumber, int $raceNumber, ) use (&$failures): void { $failures[] = compact('stadiumNumber', 'raceNumber'); });
開催場一覧の解決(scrapeStadium())だけは onError の対象外で、従来どおり例外になります。走査すべきグリッドが決まらないため、隔離のしようがないためです。
レスポンス形式(_source / 変換済み値)
各フィールドは、公式サイトから取得した生の文字列({field}_source)と、型変換・Enum変換済みの値({field})のペアで返されます。生データが常に残るため、変換ロジックの検証やデバッグがしやすくなっています。
$result = $scraper->scrapeResult('2017-03-31', 24, 1); // $result の抜粋 [ 'weather_number_source' => '雨', 'weather_number' => 3, 'wind_direction_number_source' => '南西', 'wind_direction_number' => 11, 'racers' => [ 1 => [ 'name' => '中辻 博訓', 'number_source' => '3833', 'number' => 3833, // ... ], // ... ], ];
racers は必ず 1〜6 号艇すべてを含みます。ページに載っていない艇も、キーは揃えたうえで値が null になります。
決着しなかった舟券(特払・不成立)は、組番の代わりに公式サイトの表記が label に入ります。
// $result['payouts']['win'] の抜粋 [ ['combination' => null, 'amount' => 70, 'label' => '特払'], ];
返還艇があったレースでは remarks に備考、refunds に返還された艇番が入ります(無い場合は remarks が null、refunds が [])。
直前情報の各艇には、プロペラ交換の表記 propeller と、部品交換の一覧 parts が入ります。parts は交換が無ければ空配列、直前情報自体が未掲載なら null です。数量が印字されない部品は quantity が null になります。
// $preview['racers'][2]['parts'] の抜粋 [ ['part_number_source' => 'ピストン', 'part_number' => 1, 'quantity' => 2], ['part_number_source' => 'シリンダ', 'part_number' => 5, 'quantity' => null], ];
キャッシュ
過去日(実行日より前の日付)のスクレイピング結果は、既定でファイルシステムベースの PSR-16 キャッシュに無期限保存されます。当日・未来日はキャッシュされません(レース情報自体が変動しうるため)。
use BVP\Scraper\Caching\CacheFactory; // キャッシュディレクトリを指定 $scraper = new Scraper(cache: CacheFactory::createDefault('/path/to/cache')); // 1回目: ネットワークにアクセス $scraper->scrapeResult('2017-03-31', 24, 1); // 2回目: キャッシュから即座に返る $scraper->scrapeResult('2017-03-31', 24, 1);
Psr\SimpleCache\CacheInterface を実装した任意のバックエンド(Redis や APCu など)や、キャッシュ対象の判定ロジック(BVP\Scraper\Caching\CachePolicyInterface)を差し替えることもできます。
確定済み過去レースが修正された場合(forceRefresh)
ごく稀に、公式サイト側で確定済みの過去レースのデータに修正が入ることがあります。そのような場合は forceRefresh: true を指定すると、キャッシュを無視してネットワークから再取得し、その結果でキャッシュを上書きします。以降の通常呼び出しは上書き後の値を返します。
// キャッシュを無視して再取得し、キャッシュも上書きする $scraper->scrapeResult('2017-03-31', 24, 1, forceRefresh: true);
forceRefresh は Scraper/BatchScraper の全ての scrape*() メソッドに指定できます(BatchScraper に指定した場合、開催場一覧の解決も含めて一括分すべてが再取得されます)。
レスポンス形状の変更とキャッシュ
キャッシュの名前空間にはレスポンス形状のバージョンが含まれています(現在 bvp-scraper.v3)。レスポンスのキーや値の意味が変わるリリースでは、このバージョンが上がるため、旧形状のエントリは参照されなくなります。過去日のキャッシュは無期限に保存されるため、これが無いとバックフィルが初回実行時の形状を返し続けることになります。forceRefresh を使う必要はありません。
並行実行・マルチテナンシー
レート制御はインスタンスごとに保持されるため、プロキシやアカウントが異なる複数の Scraper インスタンスを同一プロセス内で並行運用しても、互いのペース配分を食い合いません。
use BVP\Scraper\RateLimiting\ThrottleRateLimiter; use Symfony\Component\BrowserKit\HttpBrowser; use Symfony\Component\HttpClient\HttpClient; // ワーカー1: プロキシA経由で今日分を取得(3秒間隔) $scraperA = new Scraper( httpBrowser: new HttpBrowser(HttpClient::create(['proxy' => 'http://proxy-a:8080'])), rateLimiter: new ThrottleRateLimiter(3.0), ); // ワーカー2: プロキシB経由で過去分をバックフィル(1秒間隔) $scraperB = new Scraper( httpBrowser: new HttpBrowser(HttpClient::create(['proxy' => 'http://proxy-b:8080'])), rateLimiter: new ThrottleRateLimiter(1.0), ); // 両者は独立したレート状態を持つため、同一プロセス内で並行運用しても // 互いのペース配分を食い合わない
タイムアウト
既定の HTTP クライアントにはタイムアウトの指定がありません(Symfony の既定値=default_socket_timeout、多くの環境で 60 秒。総時間の上限 max_duration は無制限)。応答が返らない場合、1回の試行あたり最大でこの時間ブロックし、さらにリトライ回数だけ繰り返します。一括取得で1日分を走査する用途では、明示的に指定することを推奨します。
HttpBrowserFactory::create() に自前のクライアントを渡せば、このライブラリの UA 偽装ヘッダを維持したままトランスポート設定だけを差し替えられます。
use BVP\Scraper\Factories\HttpBrowserFactory; use Symfony\Component\HttpClient\HttpClient; $scraper = new Scraper( httpBrowser: HttpBrowserFactory::create(httpClient: HttpClient::create([ 'timeout' => 15.0, 'max_duration' => 30.0, ])), );
⚠️ Notes
- v11 はレスポンススキーマの破壊的変更を含みます。v10 から上げる場合は以下を確認してください。
payoutsの各行にlabelが加わりました。特払・不成立の行ではcombinationがnullになるため、stringとして型宣言している箇所は?stringに変更が必要です。- 数値でないオッズ(出走取消の文言など)と、欠損した払戻金額は
0/0.0ではなくnullを返すようになりました。float/intとして型宣言している箇所は同様に変更が必要です。 racersは常に 1〜6 号艇すべてを含みます。従来はページに載っていない艇のキー自体が欠落していました。- 結果に
remarks/refunds、直前情報にpropeller/partsが加わりました。 - キャッシュの名前空間が変わるため、蓄積済みの過去日キャッシュは参照されなくなり、再取得が発生します。バックフィル用途で大量にためている場合は、レート制限のもとで再取得にかかる時間を見込んでください。
- v10 は v6 との後方互換性を意図的に持たない大きな設計変更(インスタンスベース API・レスポンススキーマの変更)を含みます。v6 のまま利用する場合は
bvp/scraper: ^6.0に固定してください。 - スクレイピング対象の公式サイトの構造が変更された場合、正しくデータを取得できなくなる可能性があります。
- 利用時は対象サイトの利用規約を遵守してください。
📄 License
Scraper は MIT license の元で公開されています。