← Всички публикации

Scraping на наличности от автодилъри отвъд платформите за обяви

Scraping-ът на наличности от автодилъри се чупи в сайтовете на самите търговци, а не в агрегаторите. Цени от страната на клиента, шепа плат Built-in платформи и блокирания, които изглеждат като данни.

Всеки първо изгражда скрапер за агрегаторите. Cars.com, CarGurus, AutoTrader: три сайта, по един парсър за всеки, и до края на седмицата разполагате с фийд, който прилича на пазара за употребявани автомобили.

След това някой пита къде е останалата част.

Предизвикателството

NADA отчита 16 972 франчайзинг дилъри на лекотоварни автомобили в САЩ според доклада си от средата на 2025 г., и това е преди независимите автокъщи, които никой не брои последователно. Вторичните анализи на същата цифра от NADA варират между 15 720 и 16 990, което говори достатъчно за това колко добре се измерва този пазар.

Всеки от тези търговски обекти поддържа собствен уебсайт. Наличността в него е инвентарът на съответния дилър, с актуални цени от днес, дни преди каквото и да е да се появи в сайт на трета страна. Ако определяте цени на употребявани автомобили, прогнозирате остатъчна стойност или продавате инструмент за конкурентен анализ обратно на дилърите, сайтът на самия дилър е източникът, който ви трябва. Агрегаторите са негово изоставащо, филтрирано копие.

Така че екипите се насочват към сайтовете на дилърите и последователно откриват три неща.

Те не са уникални. Почти всички работят на малък набор от платформи за дилърски сайтове: Dealer.com, DealerOn, Dealer Inspire, CDK, Reynolds, Sincro, Lotlinx (точният списък зависи от това кой брои). Всеки, който продава тези данни, изгражда по един парсър за платформа, а не за отделно дилърство. Инструментът на Apify dealer website inventory scraper го посочва директно: първо се разпознава платформата, след това се извличат данните. Шепа шаблони покриват десетки хиляди обекти. Това е добрата новина в случая.

Цената обикновено не е в изтегления HTML. Тези платформи рендерират блока с цената от страна на клиента, а прогнозите за вноски и отстъпките често пристигат с второ извикване след това. Обикновена HTTP заявка ви дава година, марка, модел, пробег и VIN. Полето за цена се връща празно.

И следва частта, която тихо съсипва масивите от данни: в сайт на дилър неуспехът изглежда точно като реален факт. Услуга за засичане на ботове отговаря на подозрителна заявка с HTTP 200 и междинна страница. Блок за цена, който никога не се е рендерирал, оставя "Call for Price" в DOM дървото, което дилърите понякога пишат и съвсем умишлено. И двата записа влизат във вашето хранилище, изглеждайки еднакво коректни.

Разгледахме този модел в друг сектор, където блокирането изглежда като стойност от данни. Автомобилният сектор е по-трудният вариант, тъй като липсата на цена е валидно бизнес състояние, а не очевидна аномалия.

Какво се променя, когато разделите по цена

Устойчивите пайплайни не се организират по сайтове. Те се организират според това колко струва една заявка.

Скъпата заявка е първата към даден обект: тази, която трябва да стартира браузър, да преодолее защитите на дилърската платформа и да се върне с валидна сесия. Всичко след това е евтино 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()

Два детайла тук са много по-важни, отколкото изглеждат.

Сесията пътува като едно цяло. Дадено clearance cookie е обвързано с exit точката, от която е получено, и с User-Agent, под който е получено. Ако го изпълните повторно от друго място или под различен User-Agent, сайтът ще ви върне обратно в началото към challenge проверката. Затова cookie jar, агентът и ID-то на proxy се движат заедно. Това е детайлът, който виждаме хората да бъркат най-често: запазват cookie, сменят exit точката и след това се чудят защо евтиният път вече не е евтин.

Блокът validate е това, което премахва проблема с тихите грешки (silent failures). Това е начинът, по който даден request описва как изглежда реалната страница: приеми маркер, който съществува само след като списъкът с обяви е рендериран, отхвърли при наличие на междинни низове (interstitials). Response, който не отговаря на тези правила, не е просто ред с null цена. Това е неуспех, класифициран като такъв и не се отчита като успех. В автомобилния сектор описвайте както положителния маркер, така и списъка с отрицателни, защото "Обадете се за цена" е наистина двусмислено, докато "картата на автомобила изобщо не се зареди" никога не е.

Когато все още не знаете от кой път се нуждае дадена платформа, Auto ще разбере това с едно единствено извикване и ще върне сесията, която е сработила. Приемайте това като разузнаване, а не като производствен маршрут. След като разберете, че дадена платформа изисква браузър, а съседните не, насочете всяка към съответния директен engine и спрете да плащате на оркестратор да преоткрива същия отговор всяка вечер.

Резултати

Пресметнете числата за средно голяма задача (илюстративен сценарий, базиран на индустриални бенчмаркове, а не на конкретен клиент): 4 000 автокъщи, около 180 употребявани автомобила във всяка, обновявани всяка вечер.

  • 4 000 рендерирани страници вместо 720 000. Едно рендериране на автокъща отваря сесията; останалите 716 000 страници минават през евтиния път в рамките на същата сесия. Това съотношение, а не parser инструментът, определя дали ежедневното извличане е икономически изгодно.
  • Два вида липсваща цена, в две различни таблици. С въведените правила за валидация, случаите "дилърът не публикува цена" и "изобщо не получихме страницата" вече не споделят една и съща структура на реда. Вашият модел вижда само първия случай.
  • Един parser за платформа, а не за отделна автокъща. Разпознайте платформата от върнатия response и предайте HTML кода на съответния parser. Добавянето на нова автокъща на поддържана платформа не струва нищо.
  • Дилър, който сменя платформата, се проваля шумно. Разпознаването на платформата се проваля, редът никога не се записва и някой получава ticket, вместо шест седмици тихо подавани грешни цени.

Къде нещата стават по-сложни: големите дилърски групи все по-често използват персонализирани сайтове извън стандартните платформи, а те все още изискват ръчно написани parsers с цялата поддръжка, която това включва. Актуалността на данните по автокъщи също не е еднаква. Някои платформи кешират страниците си с наличности агресивно, така че "днешната цена" може да е на един ден, без значение колко често я извличате. Ако вашият модел приема времевия маркер на всяка автокъща за еднакво актуален, той греши по начин, който никаква инфраструктура за събиране на данни не може да поправи.

Основен извод

Трудната част при данните за автомобили никога не са били трите агрегатора, с които всички се сравняват. Това са седемнадесетте хиляди малки сайта, за които поотделно никога не си е струвало да се пише отделен scraper, но взети заедно имат огромна стойност.

Този модел се среща далеч отвъд автомобилите. Аптеки, дилъри на оборудване, регионални бакалии, всякакъв вид франчайзи: дългата опашка изглежда скъпа само докато третирате всяка единица от нея като уникален сайт. Обикновено тя не е такъв. Направете първия request правилно, осигурете преизползваемост на сесията и останалото престава да бъде проблем със събирането на данни, а се превръща в проблем с парсването, което е значително по-евтиният от двата варианта.