Skip to content

Repository files navigation

TruePrice

Ценовая аналитика для рынка музыкальных инструментов Казахстана. Два связанных инструмента: агрегатор цен по 9 источникам в 4 странах и монитор конкурентов для конкретного магазина на маркетплейсе Kaspi.kz.

English TL;DR — Price intelligence for the musical-instrument market in Kazakhstan. Two Python tools sharing one codebase: (1) a multi-source price aggregator (9 scrapers, 4 countries, ~19.5k offers) that normalises currencies and matches the same model across shops; (2) a competitor monitor that maps a shop's own catalogue onto Kaspi.kz marketplace listings deterministically — via the shop's own Kaspi widget SKU instead of fuzzy name matching — pulls every seller's price and flags where competitors undercut, including a data-driven counterfeit heuristic. Runs unattended: nightly incremental collection, always-on local web UI. Stack: Python, httpx, BeautifulSoup, SQLite, FastAPI, vanilla JS.


Зачем это нужно

Kaspi.kz — доминирующий маркетплейс Казахстана. Под одной карточкой товара там торгуют десятки продавцов, и покупатель почти всегда выбирает по цене из общего списка. Для магазина это значит: можно молча терять продажи, даже не зная, что тебя перебили на 300 ₸. Вручную проверять тысячи позиций невозможно.

Проект решает две задачи:

Инструмент Вопрос, на который отвечает
trueprice Сколько этот инструмент стоит в мире и не накручена ли цена локально?
ymkaspi Кто из конкурентов на Kaspi продаёт мой товар дешевле меня — прямо сейчас?

⭐ Ключевая техническая находка: детерминированное сопоставление товаров

Это центральная инженерная идея проекта, поэтому опишу подробно.

Наивный подход (и почему он провалился). Чтобы сравнить цены, нужно понять, что товар магазина и карточка на Kaspi — это один и тот же товар. Очевидное решение — искать на Kaspi по названию и сопоставлять нормализованные строки. Так и было сделано сначала: нормализация, синонимы финишей, выделение артикула, отсев аксессуаров. Работало — но давало систематические ошибки двух видов:

  • Ложные «нет на Kaspi» — поиск не находил товар, который там есть (ARIA MAC-STD → выдача забита чехлами и простынями со словом «Aria»).
  • Ложные «конкурент дешевле» — что опаснее: матчинг привязывал товар к другой расцветке той же модели, где торгует другой продавец. Магазину показывалась несуществующая угроза.

Решение. У магазина на своём сайте есть виджет-кнопка «Купить на Kaspi». В её HTML лежат атрибуты data-merchant-sku и data-merchant-code — то есть сам магазин уже сообщает, под каким SKU он торгует на Kaspi. Фрейм кнопки Kaspi по этому SKU возвращает прямую ссылку на карточку с productId:

Страница товара магазина
  └─ data-merchant-sku="MACSTDMBS"  data-merchant-code="Ymusic"
       │
       ▼  GET kaspi.kz/shop/kaspibutton/frame/?merchantSku=…&merchantCode=…&city=…
     BACKEND.components.button = {"data":{"link":"/shop/p/…-106215493/…"}}
       │
       ▼  productId = 106215493
     POST kaspi.kz/yml/offer-view/offers/106215493   ← вкладка «Продавцы»
       │
       ▼
     [{merchantName, price, merchantRating, merchantReviewsQuantity}, …]

Что это дало. Сопоставление стало детерминированным — не «похоже по названию», а «это ровно тот товар, который магазин сам выставил». Обе категории ложных срабатываний исчезли: товаров, корректно найденных на Kaspi, стало на 14% больше, а артефактный статус «продаёт только конкурент» обнулился. Побочный бонус — ссылки на карточки Kaspi стали рабочими, потому что берутся из ответа самого Kaspi, а не собираются вручную.

Вывод, который стоит запомнить: прежде чем строить эвристику сопоставления, стоит поискать, не публикует ли одна из сторон готовый идентификатор связи. Эвристику потом придётся бесконечно чинить, идентификатор — точен сразу.


🔍 Детектор подделок: эвристика, найденная в данных

Когда данные собрались, обнаружилась аномалия: по струнам конкуренты «перебивали» цену магазина в разы. Разбор лесенки продавцов на примере одной позиции (D'Addario EJ49):

Цена Продавцы Что это
635 – 640 ₸ 4 продавца ниже закупки оригинала — почти наверняка подделка
1 299 – 3 238 ₸ 4 продавца серая зона
5 453 ₸ магазин оригинал
7 347 – 7 500 ₸ 5 профильных музыкальных магазинов оригинал, дороже

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

Рейтинг и отзывы продавца не отличают подделку (у таких продавцов ★4.9 и сотни отзывов) — единственный рабочий сигнал оказался ценовым. Продавец с ценой ниже 40 % от цены магазина помечается «⚠ возможно подделка».

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


Что внутри

TruePrice/
├── trueprice/              # агрегатор цен по 9 источникам
│   ├── scrapers/           #   kaspi, ymusic, skifmusic, popmusic, thomann,
│   │                       #   reverb (REST API), ebay (Browse API),
│   │                       #   musicroom, muzzone (Playwright)
│   ├── matching.py         # склейка одной модели между магазинами (match_key)
│   ├── normalize.py        # бренды, алиасы, чистка названий
│   ├── categories.py       # реестр категорий товаров
│   ├── db.py               # SQLite: offers + price_history
│   └── web.py + static/    # витрина сравнения с нормализацией валют в ₸
│
└── ymkaspi/                # монитор конкурентов на Kaspi.kz
    ├── catalog.py          # реестр разделов магазина, авто-обход подкатегорий
    ├── kaspi.py            # SKU → productId → список продавцов
    ├── collect.py          # оркестрация сбора, вердикты, детектор подделок
    ├── db.py               # SQLite: products + check_history
    ├── web.py              # FastAPI: сводка, товары, конкуренты, CSV
    └── static/index.html   # интерфейс (vanilla JS, светлая/тёмная тема)

trueprice — агрегатор

Собирает предложения из 9 источников в 4 странах (KZ, RU, US, DE), приводит валюты к тенге и склеивает одну модель между магазинами: детерминированный match_key из бренда, ядра артикула и формы корпуса, с синонимами финишей и отсевом аксессуаров. Зарубежные площадки (Reverb, Thomann, eBay) работают как «мировой ориентир»: витрина показывает отклонение локальной цены от мировой медианы — детектор накрутки.

Отдельная история — обход защит: Muzzone.kz закрыт DDoS-Guard, и гибрид «Playwright → cookies → httpx» не работает (защита привязана к состоянию браузера), поэтому источник целиком ведёт headless-браузер. Muztorg.ru использует JS-фингерпринт и остался неподключённым — задокументировано в IDEAS.md вместе с разведкой остальных площадок.

ymkaspi — монитор конкурентов

Обходит весь товарный каталог магазина (22 категории — от гитар до коммутации), сопоставляет с Kaspi по SKU и раскладывает каждый товар по статусу:

Статус Значение
Конкурент дешевле кто-то на Kaspi продаёт этот товар дешевле магазина
Магазин не дороже всех цена конкурентоспособна
Только магазин на Kaspi единственный продавец под карточкой
Нет в наличии магазин выставлен, но сейчас торгует только конкурент
Нет на Kaspi товар не выставлен на маркетплейс — это нормально

Результаты на реальных данных

Инструмент работает не «на демо-данных» — он ежедневно обходит живой каталог.

Метрика Значение
Товаров в мониторинге 2 803 в 22 категориях
Сопоставлено с карточками Kaspi 1 519
Отслеживаемых продавцов-конкурентов 69
Позиций, где конкурент дешевле 46
Позиций с подозрением на подделку 12
Непрерывной работы автосбора 10 дней
Записано изменений цен 3 059 снимков, 151 товар с динамикой
Агрегатор trueprice 19 477 предложений, 651 модель в 2+ магазинах

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


Запуск

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt

# токены нужны только агрегатору (Reverb / eBay); ymkaspi работает без ключей
copy .env.example .env

Монитор конкурентов

# сбор: инкрементальный (SKU и id Kaspi берутся из БД — примерно втрое быстрее)
.\.venv\Scripts\python.exe -m ymkaspi collect
.\.venv\Scripts\python.exe -m ymkaspi collect --full                 # полный переобход
.\.venv\Scripts\python.exe -m ymkaspi collect --categories guitar_strings
.\.venv\Scripts\python.exe -m ymkaspi collect --pages 3 --limit 100  # быстрая проба

.\.venv\Scripts\python.exe -m ymkaspi stats        # сводка по статусам
.\.venv\Scripts\python.exe -m ymkaspi reclassify   # пересчёт вердиктов без сети
.\.venv\Scripts\python.exe -m ymkaspi web          # http://localhost:8770

Агрегатор

.\.venv\Scripts\python.exe -m trueprice.cli scrape --pages 2
.\.venv\Scripts\python.exe -m trueprice.cli stats --brand Epiphone
.\.venv\Scripts\python.exe -m uvicorn trueprice.web:app --port 8766

Автономная работа

Оба инструмента рассчитаны на работу без участия человека — через Планировщик задач Windows (pythonw.exe, без окна, лог в logs/):

# веб-интерфейс всегда поднят: старт при входе в систему + авторестарт
Register-ScheduledTask -TaskName ymkaspi-web -Action (New-ScheduledTaskAction `
  -Execute "$PWD\.venv\Scripts\pythonw.exe" -Argument "-m ymkaspi web" -WorkingDirectory $PWD) `
  -Trigger (New-ScheduledTaskTrigger -AtLogOn)

# ночной инкрементальный пересбор
Register-ScheduledTask -TaskName ymkaspi-collect -Action (New-ScheduledTaskAction `
  -Execute "$PWD\.venv\Scripts\pythonw.exe" -Argument "-m ymkaspi collect" -WorkingDirectory $PWD) `
  -Trigger (New-ScheduledTaskTrigger -Daily -At 05:30)

Интерфейс

Локальная веб-витрина на FastAPI + vanilla JS (без сборки и зависимостей, поддерживает светлую и тёмную тему):

  • KPI-плитки — товаров, перебиваний, подозрительных, «магазин лучший».
  • Вкладки по статусам и фильтр по 22 категориям.
  • Лесенка продавцов по клику на товар: все цены от дешёвой к дорогой, магазин выделен, подозрительные помечены, показаны рейтинг и число отзывов.
  • Вид «Конкуренты» — сводка по 69 продавцам: сколько общих товаров, скольких перебивают, среднее отставание; клик — товары конкретного продавца.
  • История изменений цены по товару и выгрузка выборки в CSV для Excel.

API

Эндпоинт Назначение
GET /api/summary KPI, разбивка по категориям, дата последней проверки
GET /api/products фильтры: status, category, seller, q, sort
GET /api/product/{id} карточка: все продавцы + история проверок
GET /api/competitors агрегат по продавцам-конкурентам
GET /api/export.csv выгрузка текущей выборки

Инженерные решения, о которых стоит сказать

  • Инкрементальный сбор. Полный проход — 3 запроса на товар. После первого сбора SKU и productId уже в базе, поэтому обычный прогон делает только запрос цен: около трёхкратного ускорения. Флаг --full заставляет перечитать всё — он ловит смену SKU и появление товара на маркетплейсе.
  • История только по изменениям. В check_history пишется строка, лишь если изменилась цена или статус. База не пухнет от ежедневных прогонов, а тренд остаётся видимым.
  • Категория из URL, а не из листинга. Один товар встречается в нескольких разделах магазина, из-за чего его категория «плавала» от прогона к прогону. Категория выводится из канонического пути товара — стало устойчиво.
  • Добавить раздел = одна строка. Реестр в catalog.py принимает "section": "/slug/", а подкатегории обходчик находит сам. Так за один цикл подключились 10 разделов магазина.
  • SQLite в WAL. Ночной сбор пишет, веб-сервер читает — в WAL они не блокируют друг друга.
  • Вежливость к источникам. Случайные паузы между запросами, ретраи с экспоненциальной задержкой, обработка Retry-After при 429, ранний стоп пагинации по 404.
  • Прогретые cookies. Внутренний API Kaspi отвечает только после захода на страницу поиска и с заголовками Referer + X-KS-City (код города).

Стек

Python 3.13 · httpx · BeautifulSoup + lxml · SQLite · FastAPI · Uvicorn · Playwright (для источников за антиботом) · vanilla JS. Внешних сервисов и платных API не требуется: агрегатору нужны лишь бесплатные токены Reverb/eBay, монитор Kaspi работает вообще без ключей.

Ограничения и планы

  • Сбор идёт по городу Алматы (X-KS-City); мультигород — вопрос параметра.
  • Порог подделки (40 % от цены магазина) подобран по данным струн; для других категорий его стоит калибровать отдельно.
  • Внутренние эндпоинты Kaspi не документированы публично и могут измениться — слой доступа изолирован в ymkaspi/kaspi.py, чинить придётся одно место.
  • Из планов: уведомления в Telegram при новом перебивании и графики динамики цен.

Дисклеймер

Учебно-практический проект. Собираются только общедоступные данные каталогов с вежливыми паузами между запросами; инструмент предназначен для мониторинга магазином собственных цен относительно конкурентов. Товарные знаки принадлежат их владельцам.

Лицензия

MIT

About

Ценовая аналитика для рынка музыкальных инструментов Казахстана: агрегатор цен по 9 источникам + монитор конкурентов магазина на Kaspi.kz с детерминированным сопоставлением товаров по SKU

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages