П Прорвёмся!
← Все статьи
· 6 мин DjangoELOParsingDevOps

Турнир для вишлиста: как ELO-рейтинг помогает выбрать из 50 вещей

Как мы сделали Vybra: попарные сравнения, ELO-рейтинг, парсинг Wildberries и прод на минимальных ресурсах.

Дмитрий Дмитрий лид-разработчик «Прорвёмся!»

Человек открывает Wildberries, видит «Избранное» и понимает: добавить-то добавил, а выбрать не может.

Пятьдесят позиций. Похожие цвета, похожие цены, похожие описания. Скроллишь взад-вперёд, открываешь вкладки, сравниваешь глазами, устаёшь, откладываешь. Через неделю то же самое.

Вот ровно отсюда вырос Vybra.

Попарно, а не списком

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

Vybra делает ровно то, что мозг делать не умеет: превращает попарные сравнения в рейтинг. Вы видите две вещи, выбираете лучшую. Следующая пара. Ещё. Ещё. Через двадцать сравнений система уже знает, что вам нравится на самом деле, а не что вы мысленно поставили на первое место в списке.

Это не новый UX-паттерн ради паттерна. Это инженерное решение психологической проблемы.

ELO — не только для шахмат

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

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

Коэффициент K в Vybra адаптивный: 64 на первых сравнениях, когда система ещё ничего не знает, потом 32, потом 16. Новые вещи быстро находят своё место, устоявшиеся — не прыгают от случайного клика.

И да, пересчёт идёт под select_for_update() в транзакции. Потому что два одновременных сравнения одной пары не должны испортить рейтинг. Это не учебный проект, где «никто не заметит». Это продакшен, где гонка данных — реальный риск.

Парсинг Wildberries: API вперёд, Selenium — когда некуда деваться

Импорт избранного из Wildberries — главный вход в продукт. Пользователь копирует текст из шаринга, вставляет в поле, нажимает «импорт». И тут начинается то, что снаружи не видно.

Wildberries не даёт нормального API для избранного. Их публичные эндпоинты работают нестабильно, меняют формат, требуют подбора регионального параметра dest. Мы пробуем три разных эндпоинта с разными регионами. Если все три молчат — только тогда идём в Selenium.

Selenium-парсер — это отдельная история. User-agent из пула. Экспоненциальный backoff с jitter. Куки через сессии. Обнаружение anti-bot-страницы по заголовку и телу. Вытаскивание данных из JSON-LD, потом из встроенных JSON-фрагментов, потом regex по видимому тексту. И да, нормализация URL картинок: апгрейд миниатюр c246x328 до big.

Но самое важное — enrichment intelligence. Не каждый продукт надо парсить заново. Свежие товары с ценой и картинкой пропускаются. Повторно лезут только те, у которых данных нет или последняя проверка была больше 48 часов назад.

Экономия не в том, чтобы «не ходить лишний раз». Экономия в том, чтобы не гонять Selenium для двухсот товаров, когда девяносто пять процентов из них и так в порядке.

Очистка названий: когда regex — это фича

Wildberries в шаринге отдаёт не название товара, а месиво.

Туда попадают проценты скидки, цена, блок «с WB Кошельком», рейтинг звёздами, «Похожие товары» и ещё десяток промо-вставок. Пользователь этого не видит — он видит кнопку «поделиться», копирует, вставляет. А мы получаем строку, из которой надо достать именно название.

_clean_wb_title() делает ровно это: регулярками вычищает всё лишнее, оставляет имя товара. Скучная функция? Да. Критичная для продукта? Ещё как. Если название потерялось, пользователь не узнает товар в сравнении, а дальше продукт теряет смысл.

Хорошая разработка часто выглядит именно так: не «мы применили нейросеть для категоризации», а «мы regex вычистили мусор, потому что без этого не работает».

Прод на минималках — это архитектурное решение

Vybra живёт на скромном VPS. Не потому что мы не умеем в Kubernetes. Потому что проекту не нужен Kubernetes.

Базовый прод — три контейнера: web (Gunicorn, 1 worker, 2 threads), PostgreSQL (shared_buffers=32MB, max_connections=15), Redis (maxmemory 32mb, allkeys-lru). В простое это около 128 МБ памяти. Celery, Celery Beat и Selenium вынесены в отдельный compose-файл и запускаются только когда нужны.

Это не экономия ради экономии. Это архитектурное решение. Парсинг и фоновые задачи — опциональная надстройка. Ядро продукта (сравнения, рейтинг, личный кабинет) не должно страдать от того, что где-то там Selenium жрёт память.

Мы так и думали: если сервис можно выключить без потери основной функциональности, он не должен быть в ядре. Он должен быть оверлеем.

JWT в куках и маркер для фронтенда

Авторизация сделана на stateless JWT. Access-токен живёт час, refresh — неделю. Ключ для JWT отделён от Django SECRET_KEY: можно ротировать токены, не трогая сессии, CSRF и парольные ссылки.

Но вот интересная деталь: кроме HttpOnly access/refresh кук есть третья — vybra_logged_in. Она не HttpOnly, в ней лежит просто "1". Она нужна фронтенду, чтобы мгновенно понять, залогинен пользователь или нет, не дёргая API и не декодя JWT в Alpine.js.

Мелочь? Да. Но фронтенд без этой мелочи либо делает лишний запрос на каждую страницу, либо строит догадки по косвенным признакам. А мы не любим догадки. Особенно в авторизации.

Ночной дозор: Celery Beat и забытые товары

Раз в сутки Celery Beat запускает recovery-задачу: пробегает по товарам без цены и картинки, которые давно не проверялись, и пытается их обогатить. Не все сразу — батчами по 20 штук с кулдауном между запусками.

Это тот случай, когда «работает» и «работает надёжно» — разные вещи. Сегодня парсинг прошёл, завтра нет, послезавтра изменился формат. Система должна уметь добрать пропущенное сама, без человека в консоли.

И да, _product_needs_enrichment() — это не просто проверка на None. Это проверка с TTL: если цена проверялась недавно, она считается свежей, даже если парсер ничего не вернул. Не дёргаем зря. Бережём и себя, и Wildberries.

Что получилось

Vybra — это не «аналитика покупок» и не «умный вишлист». Это инструмент для одного конкретного действия: выбрать из множества.

Внутри:

  • попарные сравнения с ELO-рейтингом (адаптивный K-фактор, транзакционная защита);
  • импорт избранного из Wildberries (текстовый и через браузерное расширение);
  • умный парсинг: API-first, Selenium — fallback, enrichment intelligence с TTL;
  • ночной recovery пропущенных товаров;
  • дашборд со статистикой, топами и бюджетными срезами;
  • история цен;
  • Google OAuth наряду с email/паролем;
  • инфраструктура с разделением ядра и парсинга, развёртывание одной командой.

Мне этот проект интересен не ELO, не парсингом и даже не тем, что мы втиснули всё в минимальные ресурсы.

Мне интересно, что настоящая проблема пользователя оказалась не в том, чтобы «найти товары». Их и так полно. Проблема в том, чтобы из найденного выбрать. И именно под это мы собрали продукт.

Не «добавить технологию». А понять, что мешает человеку, и убрать это.