Все начинают со скрапинга агрегаторов. Cars.com, CarGurus, AutoTrader: три сайта, по парсеру на каждый, и к концу недели у вас готов фид, похожий на рынок подержанных автомобилей.
А потом кто-то спрашивает, где остальная часть.
Проблема
NADA насчитывает 16 972 франчайзинговых дилера легковых автомобилей в США по отчету на середину 2025 года, и это без учета независимых площадок, которые никто точно не считает. В сторонних разборах этой же цифры NADA разброс составляет от 15 720 до 16 990, что наглядно показывает точность измерений на этом рынке.
У каждой такой точки есть свой сайт. Каталог на нем отражает реальные остатки дилера с актуальными ценами на сегодня, за несколько дней до их появления на площадках-агрегаторах. Если вы рассчитываете цены на подержанные авто, прогнозируете остаточную стоимость или продаете аналитику дилерам, вам нужны данные с сайтов самих салонов. Агрегаторы дают лишь запаздывающую и отфильтрованную копию.
Команды начинают парсить сайты дилеров и последовательно выясняют три вещи.
Они не уникальны. Почти все они работают на базе небольшого набора платформ: Dealer.com, DealerOn, Dealer Inspire, CDK, Reynolds, Sincro, Lotlinx (список варьируется от источника к источнику). Те, кто продает эти данные, пишут по одному парсеру на платформу, а не на каждый отдельный салон. В описании dealer website inventory scraper от Apify прямо указано: сначала определяем платформу, затем извлекаем данные. Небольшой набор шаблонов закрывает десятки тысяч сайтов. Это хорошая новость.
Цены обычно нет в полученном HTML. Эти платформы рендерят блок с ценой на клиенте, а расчеты платежей и скидки часто подтягиваются отдельным вторым запросом. Простой HTTP-запрос отдает год, марку, модель, пробег и VIN. Поле цены остается пустым.
И главное, что незаметно ломает датасеты: на сайте дилера сбой выглядит как валидный факт. Система защиты от ботов отвечает на подозрительный request статусом HTTP 200 и промежуточной страницей. Нерендерящийся блок цены оставляет в DOM строку "Call for Price", которую дилеры нередко указывают намеренно. Обе записи попадают в ваше хранилище и выглядят одинаково корректно.
Мы разбирали этот паттерн на примере другой ниши, где блокировка выглядит как точка данных. В автобизнесе все сложнее, поскольку отсутствие цены является штатным состоянием, а не очевидной аномалией.
Что меняется при разделении по стоимости
Надежные пайплайны строятся не вокруг структуры сайтов. Они строятся вокруг стоимости каждого request.
Дорогим является первый request к сайту дилера: тот, которому нужно поднять headless-браузер, пройти проверки платформы и получить рабочую сессию. Все последующие шаги представляют собой дешевые 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 node, на котором он был получен, и к User-Agent, под которым он был выдан. Если воспроизвести его из другого места или с другим User-Agent, сайт снова отправит вас на проверку. Именно поэтому jar, строка User-Agent и ID proxy должны перемещаться вместе. Эту ошибку совершают чаще всего: сохраняют cookie, сбрасывают exit node, а затем удивляются, почему дешевый маршрут перестал быть дешевым.
Блок validate решает проблему скрытых сбоев. В нем request определяет критерии настоящей страницы: принять маркер, который появляется только после рендера сетки объявлений, и завершать сбоем при наличии промежуточных строк interstitial. Response, не отвечающий этим правилам, не должен становиться строкой с пустым значением цены (null). Это сбой, классифицированный соответствующим образом и не учитываемый как успешный. В автоиндустрии всегда указывайте как позитивные маркеры, так и список исключений, потому что статус «Уточняйте цену» может быть неоднозначным, а факт «карточка автомобиля не отрендерилась» однозначен всегда.
Когда вы еще не знаете, какой путь требуется для конкретной платформы, режим Auto определит это за один вызов и вернет рабочую сессию. Рассматривайте это как разведку, а не как продакшн-маршрут. Как только вы выяснили, что одной платформе нужен headless browser, а соседним нет, зафиксируйте каждую на прямом движке и перестаньте платить оркестратору за повторный поиск одного и того же решения каждую ночь.
Результаты
Посчитаем экономику для средней задачи (показательный сценарий на основе отраслевых бенчмарков, не привязанный к конкретному клиенту): 4 000 дилерских центров, примерно по 180 подержанных автомобилей в каждом, обновление каждую ночь.
- 4 000 отрендеренных страниц вместо 720 000. Один рендер на дилерский центр открывает сессию; остальные 716 000 страниц проходят по дешевому пути внутри этой же сессии. Именно эта пропорция, а не парсер, определяет рентабельность ежедневного сбора данных.
- Два типа отсутствующих цен в двух разных таблицах. С настроенными правилами валидации случаи «дилер не указал цену» и «страница не была получена» больше не попадают в одну категорию. Ваша модель видит только первый случай.
- Один парсер на платформу, а не на каждого дилера. Определяйте платформу по response и передавайте HTML соответствующему парсеру. Подключение нового дилера на уже поддерживаемой платформе не требует дополнительных затрат.
- Смена платформы дилером вызывает явный сбой. Определение платформы не срабатывает, строка не записывается, и инженер получает тикет вместо шести недель накопления незаметно искаженных цен.
Где возникают сложности: крупные дилерские группы все чаще используют кастомные сайты вне стандартных платформ, и для них по-прежнему требуются индивидуальные парсеры со всеми вытекающими затратами на поддержку. Актуальность данных по дилерам также неоднородна. Некоторые платформы агрессивно кэшируют страницы каталога, поэтому «цена на сегодня» может отставать на сутки вне зависимости от частоты сбора. Если ваша модель считает временные метки всех дилеров одинаково свежими, эта ошибка не устраняется никаким масштабированием инфраструктуры сбора данных.
Главный вывод
Сложность автомобильных данных никогда не заключалась в трех агрегаторах, на которые все ориентируются. Дело в семнадцати тысячах мелких сайтов, ради которых по отдельности никогда не стоило писать отдельный скрапер, но которые вместе представляют огромную ценность.
Такая картина характерна далеко не только для автомобилей. Аптеки, поставщики оборудования, региональные продуктовые сети, любые франшизы: длинный хвост кажется дорогим в обслуживании только до тех пор, пока вы воспринимаете каждую его часть как уникальный сайт. Обычно это не так. Настройте первый request правильно, сделайте сессию переиспользуемой, и задача перестанет быть проблемой сбора данных, превратившись в проблему парсинга. А это несравнимо более дешевая задача из двух.