Ценовая аналитика для рынка музыкальных инструментов Казахстана. Два связанных инструмента: агрегатор цен по 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, светлая/тёмная тема)
Собирает предложения из 9 источников в 4 странах (KZ, RU, US, DE), приводит
валюты к тенге и склеивает одну модель между магазинами: детерминированный
match_key из бренда, ядра артикула и формы корпуса, с синонимами финишей и
отсевом аксессуаров. Зарубежные площадки (Reverb, Thomann, eBay) работают как
«мировой ориентир»: витрина показывает отклонение локальной цены от мировой
медианы — детектор накрутки.
Отдельная история — обход защит: Muzzone.kz закрыт DDoS-Guard, и гибрид «Playwright → cookies → httpx» не работает (защита привязана к состоянию браузера), поэтому источник целиком ведёт headless-браузер. Muztorg.ru использует JS-фингерпринт и остался неподключённым — задокументировано в IDEAS.md вместе с разведкой остальных площадок.
Обходит весь товарный каталог магазина (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.
| Эндпоинт | Назначение |
|---|---|
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 при новом перебивании и графики динамики цен.
Учебно-практический проект. Собираются только общедоступные данные каталогов с вежливыми паузами между запросами; инструмент предназначен для мониторинга магазином собственных цен относительно конкурентов. Товарные знаки принадлежат их владельцам.