誰もが最初はアグリゲーターのスクレイパーを構築します。Cars.com、CarGurus、AutoTraderの3サイトにそれぞれ1つのパーサーを用意すれば、週末には中古車市場を網羅したかのようなフィードが完成します。
しかし、その後に「残りのデータはどこにあるのか」という疑問が生じます。
課題
NADA(全米自動車ディーラー協会)の2025年中期レポートによると、米国には16,972のフランチャイズ小型車ディーラーが存在します。これには、誰も正確に集計できていない独立系販売店は含まれていません。同じNADAの数値を引用した二次資料でも15,720から16,990までばらつきがあり、この市場の計測がいかに曖昧であるかを物語っています。
各ディーラー(個別店舗)は独自のウェブサイトを運営しています。そこに掲載されている在庫は、サードパーティのリスティングサイトに反映される数日前の、今日価格設定されたそのディーラーのリアルタイム在庫です。中古車の価格設定、残存価格の予測、またはディーラー向けの競合分析ツールを提供するなら、真に求めるべきデータは個別店舗サイトにあります。アグリゲーターの情報は、遅延しフィルタリングされた複製にすぎません。
そのため、開発チームは個別店舗サイトの収集に着手しますが、順を追って3つの現実に直面します。
まず、サイトごとに完全に固有というわけではありません。ほぼすべてがDealer.com、DealerOn、Dealer Inspire、CDK、Reynolds、Sincro、Lotlinxなど、限られた数のディーラー向けウェブサイトプラットフォーム上で稼働しています(集計元によってリストは多少異なります)。このデータを商用利用する場合、ディーラーごとではなくプラットフォームごとに1つのパーサーを構築します。Apifyのディーラーウェブサイト在庫スクレイパーも仕様通りのアプローチをとっています。まずプラットフォームを判別し、その後に抽出を行います。一握りのテンプレートで数万の店舗をカバーできる点は、この取り組みにおける救いです。
次に、取得したHTMLには通常、価格情報が含まれていません。これらのプラットフォームは価格ブロックをクライアントサイドでレンダリングし、支払い試算額やインセンティブ情報はさらに2回目のコールで取得されるケースが多く見られます。単純なHTTPリクエストでは、年式、メーカー、モデル、走行距離、VINのみが取得され、価格フィールドは空のまま返されます。
そして、データセットを静かに破壊する要因があります。ディーラーサイトでは、エラーが正常なデータとまったく同じように見えてしまう点です。Bot対策サービスは不審なリクエストに対してHTTP 200とともにインターセクション(中間画面)を返します。レンダリングされなかった価格ブロックのDOMには「Call for Price(要問合せ)」が残りますが、これはディーラーが意図して記載するテキストでもあります。どちらのパターンも、データウェアハウスには一見正常なレコードとして蓄積されます。
FourAでは別の業界において、ブロックされたレスポンスが正常なデータポイントに見えてしまうパターンについて解説しました。自動車分野はさらに困難です。「価格なし」が明らかな異常値ではなく、ビジネス上正当な状態として成立しているためです。
コストで分離することで何が変わるか
安定して稼働し続けるパイプラインは、サイト単位で構造化されていません。リクエストにかかるコストごとに整理されています。
最も高コストなリクエストは、個別店舗サイトに対する最初のリクエストです。ブラウザを実行し、ディーラーのプラットフォームが提示する防御をクリアして、セッションを確立する必要があります。それ以降の処理はすべて、最初のリクエストで得たセッションを再利用する低コストなHTTPコールで完結します。
import requests
FOURA = "https://api.foura.ai/api"
AUTH = {"Authorization": "Bearer pk_live_..."}
# First page of a rooftop: render it, clear the defense, keep the session.
first = requests.post(f"{FOURA}/browser", headers=AUTH, json={
"url": "https://example-motors.com/used-inventory/index.htm",
"unblocker": True,
"timeout_ms": 45000,
}).json()
listings = first["body"]
jar = "; ".join(f'{c["name"]}={c["value"]}' for c in first["cookies"])
agent = first["userAgent"]
exit_id = first["proxy"] # opaque proxy ID, send it back to stay on the same exit
その後は、ブラウザのコストを再度かけることなく、そのディーラーの残りの在庫を取得します。
page = requests.post(f"{FOURA}/single", headers=AUTH, json={
"method": "GET",
"url": "https://example-motors.com/used-inventory/index.htm?start=20",
"proxy": exit_id,
"headers": [["Cookie", jar], ["User-Agent", agent]],
"validate": {
"status": {"accept": [200]},
"data": {
"accept": ["vehicle-card"],
"fail": ["Just a moment", "Access Denied"]
}
}
}).json()
ここには、一見目立たないものの極めて重要な2つの詳細があります。
セッションは1つの単位として移動します。クリアランスcookieは、それを取得した出口IPと、取得時に使用したUser-Agentに紐付いています。別の場所から、あるいは別のUser-Agentでリプレイすると、サイトはチャレンジ画面からやり直させます。そのため、cookie jar、User-Agent文字列、proxy IDは常にセットで移動させる必要があります。これは最もよく見られる失敗です。cookieだけを保持して出口IPを破棄し、なぜ安価なパスが機能しなくなったのか頭を抱えることになります。
validateブロックは、サイレント障害の問題を排除する仕組みです。これはリクエスト側から「正常なページとは何か」を定義するものです。車両一覧グリッドがレンダリングされた後にのみ存在するマーカーを受け入れ、インタースティシャル文字列が含まれていれば失敗と判定します。これらのルールを満たさないレスポンスは、「価格がnullの行」ではありません。障害として分類され、成功としてカウントされません。自動車分野では、ポジティブマーカーとネガティブリストの両方を記述してください。「価格はお問い合わせ」は本当に価格非公開なのか曖昧ですが、「車両カードがレンダリングされなかった」という事象には曖昧さがないからです。
特定のプラットフォームにどのパスが必要かまだ分からない場合、Autoは1回の呼び出しでそれを特定し、成功したセッションを返します。これは本番運用ルートではなく、事前調査として扱ってください。あるプラットフォームにはブラウザが必要で、他には不要だと判明したら、それぞれを直接エンジンに固定し、毎晩同じ答えを再検出するためにオーケストレーターへ余計なコストを支払うのをやめましょう。
Results
中規模のジョブで計算してみます(特定顧客のデータではなく、業界ベンチマークに基づく想定シナリオ)。4,000店舗、各店舗あたり約180台の中古車、毎晩更新。
- 720,000ページではなく4,000ページのレンダリング。 店舗ごとに1回のレンダリングでセッションを開き、残りの716,000ページは同一セッションの安価なパスを経由します。毎晩のデータ収集を現実的なコストに抑えられるかどうかは、パーサーではなくこの比率で決まります。
- 2種類の「価格なし」を2つの異なるテーブルに分離。 validateルールを導入することで、「ディーラーが価格を公開していない」ケースと「ページの取得に失敗した」ケースが同一の行形式を共有しなくなります。モデルには前者のデータのみが渡されます。
- ディーラーごとではなく、プラットフォームごとに1つのパーサー。 レスポンスからプラットフォームを検出し、該当するパーサーにHTMLを渡します。対応済みプラットフォーム上の新しい店舗であれば、追加コストなしでオンボーディングできます。
- プラットフォームを移行したディーラーは明確にエラーになる。 プラットフォーム検出が失敗し、行は書き込まれず、6週間にわたって誤った価格が静かに蓄積される代わりにチケットが起票されます。
注意すべき点もあります。大規模なディーラーグループは独自のカスタムサイトを構築する傾向が強まっており、そうしたサイトには依然として手作業でのパーサー構築とメンテナンスが必要です。店舗ごとのデータ鮮度も一律ではありません。一部のプラットフォームは在庫ページを強力にキャッシュするため、収集頻度にかかわらず「本日の価格」が1日前のデータである可能性があります。モデルがすべての店舗のタイムスタンプを一様に最新として扱うなら、どれだけ収集インフラを強化しても解決できない誤りを抱えることになります。
Key Takeaway
自動車データにおいて最も困難なのは、誰もがベンチマークにする3大アグリゲーターではありませんでした。真の課題は、単体では専用スクレイパーを作る価値がなくても、集約すれば莫大な価値を持つ1万7,000の中小サイトです。
この構造は自動車業界に限りません。薬局、建設機器ディーラー、地域密着型スーパー、各種フランチャイズなど、ロングテールが高コストに見えるのは各サイトを個別の対象として扱っている間だけです。実際には共通パターンが存在します。最初のリクエストを正しく処理し、セッションを再利用可能にすれば、残る課題はデータ収集ではなくパース処理へと移行します。そしてパース処理の方が、収集よりもはるかに低コストです。