← Wszystkie wpisy

Scraping stanów magazynowych dealerów poza serwisami ogłoszeniowymi

Scraping stanów magazynowych dealerów zawodzi na stronach salonów, a nie na agregatorach. Ceny po stronie klienta, garść szablonów platform i blokady udające poprawne dane.

Każdy zaczyna od scrapera agregatorów. Cars.com, CarGurus, AutoTrader: trzy serwisy, po jednym parserze na każdy i przed końcem tygodnia masz feed, który wygląda jak rynek samochodów używanych.

Wtedy ktoś pyta, gdzie jest reszta.

Wyzwanie

Według raportu NADA z połowy 2025 roku w Stanach Zjednoczonych działa 16 972 koncesjonowanych dealerów pojazdów lekkich, a to jeszcze przed uwzględnieniem niezależnych komisów, których nikt nie liczy w spójny sposób. Inne opracowania tych samych danych NADA podają od 15 720 do 16 990, co mówi samo za siebie o precyzji pomiarów tego rynku.

Każdy z tych punktów dealerskich prowadzi własną stronę. Znajdująca się tam oferta to aktualny stan magazynowy z cenami z dzisiejszego dnia, na kilka dni przed tym, jak trafi do jakiegokolwiek zewnętrznego agregatora. Jeśli wyceniasz auta używane, prognozujesz wartości rezydualne lub sprzedajesz narzędzia analityczne dla dealerów, dane bezpośrednio ze stron dealerów są dokładnie tym, czego potrzebujesz. Agregatory to jedynie opóźniona, przefiltrowana kopia.

Zespoły zaczynają więc scrapować bezpośrednie strony dealerów i szybko odkrywają trzy fakty.

Strony nie są unikalne. Prawie wszystkie działają na wąskiej grupie platform dealerskich: Dealer.com, DealerOn, Dealer Inspire, CDK, Reynolds, Sincro, Lotlinx (dokładna lista zależy od źródła). Każdy, kto sprzedaje te dane, buduje jeden parser na platformę, a nie na każdego dealera z osobna. Scraper stron dealerskich od Apify mówi o tym wprost: najpierw wykryj platformę, potem wyciągaj dane. Kilka szablonów pokrywa dziesiątki tysięcy dealerów. To akurat dobra wiadomość.

Ceny zwykle nie ma w pobranym HTML. Platformy te renderują blok z ceną po stronie klienta, a szacunki rat leasingowych i rabaty często docierają w kolejnym osobnym zapytaniu. Zwykły request HTTP zwraca rok, markę, model, przebieg i VIN. Pole z ceną pozostaje puste.

I wreszcie element, który po cichu niszczy zbiory danych: na stronie dealera błąd wygląda identycznie jak poprawna dana. System ochrony przed botami odpowiada na podejrzany request kodem HTTP 200 i stroną pośrednią. Niewyrenderowany blok cenowy zostawia w DOM tekst "Zadzwoń po cenę", który dealerzy wpisują tam również celowo. Oba rekordy trafiają do twojej bazy i wyglądają na tak samo poprawne.

Opisywaliśmy ten schemat w innej branży, gdzie blokada wygląda jak punkt danych. Branża automotive to jeszcze trudniejszy przypadek, ponieważ "brak ceny" jest w pełni poprawnym stanem biznesowym, a nie oczywistą anomalią.

Co się zmienia, gdy podzielisz architekturę według kosztu

Stabilne pipeline'y nie są zorganizowane według serwisów. Są zorganizowane według kosztu pojedynczego requestu.

Kosztowny jest pierwszy request do strony dealera: ten, który musi uruchomić przeglądarkę, ominąć zabezpieczenia platformy i zwrócić poprawną sesję. Wszystko po nim to tanie zapytania HTTP, które korzystają z tego, co wypracowało pierwsze wywołanie.

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

Następnie przejrzyj resztę oferty tego dealera bez ponownego płacenia za przeglądarkę:

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()

Dwa szczegoly maja tutaj wieksze znaczenie, niz mogloby sie wydawac.

Sesja przemieszcza sie jako jedna calosc. Cookie autoryzacyjne jest powiazane z adresem wyjsciowym proxy, na ktorym zostalo uzyskane, oraz z naglowkiem User-Agent uzytym podczas jego generowania. Ponowienie zadania z innego adresu lub pod innym User-Agent sprawia, ze witryna ponownie wyswietla challenge. Dlatego cookie jar, User-Agent i identyfikator proxy musza byc przekazywane razem. To najczestszy blad, jaki obserwujemy: zachowanie cookie przy zmianie IP wyjsciowego, a nastepnie zdziwienie, ze tania sciezka przestala byc tania.

Blok validate eliminuje problem cichych bledow (silent failures). Pozwala zdefiniowac w zadanym request, jak wyglada poprawna strona: zaakceptowac znacznik obecny dopiero po wyrenderowaniu siatki ofert, odrzucic odpowiedz zawierajaca ciagi znakow typowe dla stron przejsciowych. Odpowiedz niespelniajaca tych regul to nie wiersz z wartoscia null w polu ceny. To blad, sklasyfikowany jako failure i niezaliczany jako sukces. W branzy automotive nalezy definiowac zarowno reguly pozytywne, jak i liste negatywna, poniewaz komunikat "Zadzwon po cene" bywa niejednoznaczny, ale brak wyrenderowania karty pojazdu jest zawsze jednoznacznym bledem.

Gdy nie wiadomo jeszcze, jakiej sciezki wymaga dana platforma, Auto ustali to w ramach pojedynczego wywolania i zwroci dzialajaca sesje. Nalezy traktowac to jako rekonesans, a nie jako docelowy proces produkcyjny. Po ustaleniu, ze dana platforma wymaga przegladarki, a pozostale nie, nalezy przypisac kazda z nich bezposrednio do wlasciwego silnika. Pozwala to uniknac kosztow ponownego wykrywania tego samego rozwiazania kazdej nocy przez orkiestrator.

Wyniki

Kalkulacja dla sredniej wielkosci zadania (scenariusz pogladowy oparty na benchmarkach branzowych, nie dotyczy konkretnego klienta): 4000 salonów, okolo 180 uzywanych pojazdow na salon, odswiezanie co noc.

  • 4000 wyrenderowanych stron zamiast 720 000. Jedno renderowanie na salon otwiera sesje, a pozostale 716 000 stron pobieranych jest tania sciezka w ramach tej samej sesji. Te proporcje, a nie parser, decyduja o tym, czy codzienne zbieranie danych miesci sie w budzecie.
  • Dwa rodzaje braku ceny w dwoch oddzielnych tabelach. Dzieki regułom walidacji sytuacje "dealer nie podaje ceny" oraz "strona nie zostala pobrana" nie trafiaja do tej samej struktury danych. Model otrzymuje wylacznie pierwszy przypadek.
  • Jeden parser na platforme, a nie na dealera. Wykrywanie platformy na podstawie odpowiedzi i przekazanie HTML do dedykowanego parsera. Dodanie nowego dealera na obslugiwanej juz platformie nie wymaga zadnych prac wdrozeniowych.
  • Zmiana platformy przez dealera generuje wyrazny blad. Brak dopasowania platformy uniemozliwia zapisanie wiersza, co natychmiast tworzy zgloszenie zamiast rejestrowania blednych cen przez szesc tygodni.

Miejsca, w ktorych proces sie komplikuje: duze grupy dealerskie coraz czesciej korzystaja z niestandardowych stron poza glownymi platformami. Wymagaja one recznie tworzonych parserow i biezacego utrzymania. Swiezosc danych w poszczegolnych salonach rowniez nie jest jednolita. Niektore platformy agresywnieแคchuja strony z ofertami, przez co "dzisiejsza cena" moze miec dzien opoznienia, bez wzgledu na czestotliwosc scrapowania. Jesli model zaklada, ze kazdy znacznik czasu oznacza dane w czasie rzeczywistym, generuje bledy, ktorych nie naprawi zadna infrastruktura pobierania danych.

Kluczowe wnioski

Trudną częścią danych motoryzacyjnych nigdy nie były trzy agregatory, do których wszyscy się porównują. To siedemnaście tysięcy małych serwisów, dla których pojedynczo nigdy nie opłacało się pisać dedykowanego scrapera, a które razem mają ogromną wartość.

Ten schemat pojawia się daleko poza branżą automotive. Apteki, dystrybutorzy sprzętu, regionalne sklepy spożywcze, cokolwiek działającego we franczyzie: długi ogon wydaje się drogi tylko wtedy, gdy traktujesz każdy jego element jako unikalną stronę. Zwykle tak nie jest. Skonfiguruj poprawnie pierwszy request, zadbaj o możliwość ponownego użycia sesji, a to, co zostanie, przestanie być problemem zbierania danych i stanie się problemem parsowania, który jest zdecydowanie tańszy.