Сбор витринных цен Wildberries и цен конкурентов через клиентский сайт

Сбор цен, которые покупатель видит на витрине Wildberries, — по нашим товарам и по товарам конкурентов, — и сводный лист, показывающий, на сколько мы дороже или дешевле рынка после всех скидок площадки.

ОПИСАНИЕ

Технически — часть того же проекта по аналитике Wildberries, но данные берутся не из кабинета продавца, а с покупательского сайта, поэтому это отдельный контур со своей авторизацией, своим браузером и своими задачами.

Кабинет продавца знает только нашу собственную цену. Что за неё в итоге платит покупатель, там не видно: поверх нашей цены Wildberries вешает свою скидку, а сверху ещё скидку кошелька, и обе считаются на стороне витрины. Цен конкурентов в кабинете нет вовсе. Поэтому цены собираются с покупательского сайта — из того же источника, который видит обычный покупатель.

Отказ от браузера в пользу JSON карточки

Первая версия открывала карточки браузером и читала цену со страницы. Это работало, но стоило около 790 КБ трафика и 0.6 секунды на товар, а список включает и наш каталог, и артикулы конкурентов. Рабочая версия обращается к служебному JSON самой карточки пачками до трёхсот артикулов: около килобайта и сотые доли секунды на товар. Браузер остался, но нужен ровно один и только на старте.

Обход антибота без зашитых заголовков

Задача браузера — пройти антибот и отдать заголовки, без которых API отвечает 403. Заголовки и адрес запроса не зашиты в код: до загрузки скриптов сайта в страницу через CDP внедряется обёртка над fetch, которая складывает адреса и заголовки настоящих запросов страницы, и скрипт подсматривает у самого сайта, как тот ходит к своему API. Это принципиально: у гостя и у авторизованного покупателя разные эндпоинты и разные параметры, а фронт меняет их от версии к версии — со своим зашитым адресом скрипт получал бы ответ не про ту сессию либо не получал бы вовсе. Сами запросы выполняются изнутри страницы: тогда куки, заголовки и TLS-отпечаток родные и антибот не отличает нас от самого сайта — тот же запрос снаружи даёт 403.

Собственный браузер и виртуальный экран

Браузер здесь свой, а не общий для проекта: общий работает headless, а антибот headless не пропускает ни обычный драйвер, ни undetected. Нужен настоящий Chrome, поэтому на сервере без монитора поднимается виртуальный экран Xvfb — браузер работает как обычный, но рисует в фреймбуфер. Драйвер тоже не скачивается на лету: из России до хранилища Google не достучаться, а системный прокси здесь намеренно срезается (через него Wildberries ведёт себя иначе), так что в проекте лежит своя копия драйвера, которая патчится под установленную версию Chrome. Перед стартом с профиля снимаются lock-файлы, оставшиеся от аварийно убитого по таймауту браузера: без этого Chrome на профиле больше не поднимается, и парсер молча ушёл бы в гостевой режим навсегда.

Покупательский профиль Chrome

Отдельный профиль Chrome — покупательский, со своей разовой авторизацией по SMS. Профили кабинета продавца здесь не годятся: другой сайт, другая сессия. Скрипт авторизации спрашивает телефон и код в терминале и вводит их в страницу сам, поэтому заводить профиль можно по SSH, где окна браузера не видно. Определение «вошли или смотрим гостем» сделано по двум свидетельствам — шапке сайта и сессионным кукам, — потому что Wildberries сначала отдаёт гостевую шапку и лишь затем подменяет её: по первому виду страницы профиль с живой сессией объявлялся гостем. Если профиль занят или побит, сбор продолжается гостем: цены важнее лишнего процента скидки.

Скидка кошелька

Самая нетривиальная часть — скидка кошелька. В ответе API её нет вообще, витрина считает эту цену на фронте, а процент зависит от того, вошёл покупатель или нет (у гостя 2%, у авторизованного 3%). Вместо ручной константы скрипт замеряет процент сам: открывает несколько живых карточек, читает нарисованные там цены и восстанавливает по ним процент. Замер обязан пройти проверку — найденный процент должен воспроизводить каждую увиденную цену с точностью до рубля; на выборке из трёх товаров без такой проверки намерялось 0.6% вместо гостевых 2%, потому что второй ценой на одной из карточек оказалась не кошельковая, а акционная. Не сошлось или результат вышел за границы правдоподобия — берётся константа и в лог пишется предупреждение; замер сравнивается с прошлым прогоном, чтобы заметить, что площадка изменила процент.

Список артикулов и статусы товаров

Список артикулов скрипт берёт из базы, а не из файла: наши — из справочника товаров с отсечкой по последней выгрузке, чтобы в прогон не попали давно пропавшие из каталога карточки; конкуренты — из отдельного справочника, где к каждому нашему товару привязано до семи чужих артикулов. Товары, которых нет в ответе, не теряются, а получают статус: «в продаже», «нет в наличии» (карточка есть, продажи нет) и «удалён» (карточки нет — снята или заблокирована).

Хранение цен историей

В базе цены хранятся историей: каждый прогон дописывает строки, ничего не перезаписывая, и актуальная цена — последняя по времени. Раньше строка была одна на артикул и перезаписывалась, из-за чего нельзя было увидеть, когда и как часто конкурент менял цену. Цены и связка «наш товар — его конкуренты» лежат в разных таблицах и соединяются при выгрузке. Отдельно хранятся чистая цена, одинаковая для всех покупателей (именно по ней и надо сравниваться с рынком), и цена с кошельком, которая зависит от покупателя и потому справочная.

Сводный лист сравнения с рынком

Сводный лист собирается соединением четырёх источников. Цена показана четырьмя шагами, где каждый следующий включает все предыдущие скидки: без скидок, с нашей скидкой, со скидкой Wildberries, с кошельком — первые два шага из кабинета, вторые с витрины. Раньше все четыре брались с витрины, и «скидкой ВБ» называлась на самом деле наша собственная — лист показывал 73% там, где 70 поставили мы сами. Скидки выводятся долями, процент рисует формат ячейки; общая скидка площадки считается как падение цены от нашей цены до цены с кошельком — ни суммой отдельных долей (у них разные базы), ни отношением цен, обе замены директор забраковал. Рядом — предмет и категория товара (категории в отчёте по товарам нет, она берётся из отдельно собираемого справочника предметов WB), заказы за вчера и отклонение нашей цены от минимальной цены конкурентов.

ИСПОЛЬЗУЕМЫЕ ИНСТРУМЕНТЫ

  • Python 3
  • Selenium
  • undetected_chromedriver
  • Chrome DevTools Protocol
  • Xvfb / pyvirtualdisplay
  • внутренний JSON API карточки Wildberries
  • PostgreSQL
  • psycopg2
  • gspread (Google Sheets API)
  • Git

РЕЗУЛЬТАТ

  • Собираются цены, которые реально видит покупатель, — по всему нашему каталогу и по артикулам конкурентов, чего в кабинете продавца нет в принципе.
  • Скорость сбора выросла на порядки: вместо ~790 КБ и 0.6 сек на товар — около килобайта и сотые доли секунды, пачками до трёхсот артикулов за запрос.
  • Обход антибота построен без зашитых в код заголовков: скрипт подсматривает настоящий запрос сайта через перехват fetch и выполняет свои запросы изнутри страницы, поэтому обновления фронта его не ломают.
  • Заведён отдельный покупательский профиль Chrome с разовой авторизацией по SMS прямо из терминала — с работой на сервере без монитора через Xvfb — и с безопасной деградацией до гостевого режима.
  • Скидка кошелька, которой нет в данных API, замеряется по живым карточкам с проверкой на воспроизводимость до рубля и откатом на константу.
  • Цены хранятся историей, что позволило видеть динамику цен конкурентов, а не только их текущее значение.
  • Построен сводный лист сравнения с рынком: четыре шага цены, доли скидок, предмет и категория, заказы за вчера и отклонение от минимальной цены конкурентов.

AI АССИСТЕНТ

Задать вопрос по этой работе