Негативний відгук на Ozon або Wildberries може висіти без відповіді добу. За цей час він встигає відштовхнути десятки потенційних покупців. Ми розробили бота, який сканує відгуки в реальному часі та миттєво сповіщає команду в Telegram. За нашими даними, 70% покупців змінюють думку про бренд після швидкої відповіді на негатив. Наш бот скорочує час реакції з годин до хвилин. За роки ми реалізували 30+ проектів з репутаційного моніторингу — від простих парсерів до Enterprise-систем. Економія часу на обробку відгуків сягає 40%.
Чому бот окупається за місяць?
Ручна перевірка 1000 карток товарів на кількох майданчиках може займати до 4 годин щодня. При вартості години оператора в X гривень щомісячні витрати становлять десятки тисяч. Бот виконує ту саму роботу за хвилини та обходиться в одноразову інвестицію, яка окупається за 1–2 місяці за рахунок зниження трудовитрат та підвищення конверсії. В одному з проектів для великого продавця електроніки ми замінили ручний моніторинг 500 карток на автоматизований. Час реакції на негатив скоротився з 4 годин до 2 хвилин, а конверсія в лояльність зросла на 15%.
Бот швидший за ручний моніторинг
Ручна перевірка десятків карток товарів на кількох майданчиках займає години. Бот перевіряє кожен майданчик за розкладом: для API-майданчиків — раз на годину, для парсингу — раз на 4 години. Повідомлення надходить у Telegram за 1–2 хвилини після появи відгуку. Це в 10–20 разів швидше за ручний підхід.
| Параметр | Ручний | Автоматизований |
|---|---|---|
| Час реакції на негатив | 2–24 години | 1–5 хвилин |
| Кількість перевірених карток на день | до 50 | необмежено |
| Частота перевірки | 1 раз на день | щогодини |
| Вартість обслуговування | висока | низька |
Як налаштувати бота за 3 кроки?
-
Підготовка джерел. Ви передаєте список посилань на товари або API-ключі (для Яндекс.Маркет). Ми створюємо записи в
review_sources. -
Запуск адаптерів. Бот запускає команди
reviews:check --platform=...за розкладом. Кожен адаптер реалізує інтерфейсReviewAdapterInterfaceі повертає масивReviewDTO. - Налаштування повідомлень. Вказуєте Telegram-чат для звичайних відгуків та окремий чат для термінових (негативних). Все готово до роботи.
Архітектура адаптерів для майданчиків
Кожен майданчик — окремий адаптер. Для платформ з API (Яндекс.Маркет) використовуємо прямі запити, для інших — парсинг HTML через Playwright. Всі адаптери реалізують єдиний інтерфейс ReviewAdapterInterface, що спрощує додавання нових майданчиків.
Схема даних включає дві основні таблиці: review_sources (посилання на товари) та reviews (самі відгуки). Індекси прискорюють вибірку непрочитаних відгуків. Ось структура:
CREATE TABLE review_sources ( id BIGSERIAL PRIMARY KEY, platform VARCHAR(50) NOT NULL, -- 'yandex_market', 'ozon', 'google', 'otzovik' product_id BIGINT REFERENCES products(id), external_url TEXT NOT NULL, external_id VARCHAR(255), -- ID карточки на площадке scrape_config JSONB, is_active BOOLEAN DEFAULT TRUE, UNIQUE(platform, external_url) ); CREATE TABLE reviews ( id BIGSERIAL PRIMARY KEY, source_id BIGINT REFERENCES review_sources(id), external_id VARCHAR(255), -- ID отзыва на площадке author VARCHAR(255), rating SMALLINT, -- 1–5 text TEXT, pros TEXT, cons TEXT, published_at TIMESTAMP, discovered_at TIMESTAMP DEFAULT NOW(), sentiment VARCHAR(20), -- 'positive', 'negative', 'neutral' (ML) is_notified BOOLEAN DEFAULT FALSE, UNIQUE(source_id, external_id) ); CREATE INDEX idx_reviews_rating ON reviews(source_id, rating); CREATE INDEX idx_reviews_notified ON reviews(source_id) WHERE is_notified = FALSE; API чи парсинг: порівняння
API стабільніше, але не у всіх майданчиків відкритий. Яндекс.Маркет надає API партнерам — без блокувань і з повними даними. Ozon і Wildberries API для відгуків не дають, тому використовуємо Playwright: браузер рендерить сторінку та витягує відгуки. Це повільніше, але надійно.
Ось порівняння підходів:
| Характеристика | API (Яндекс.Маркет) | Парсинг (Ozon/WB) |
|---|---|---|
| Стабільність | Висока | Середня (залежить від структури сторінки) |
| Швидкість | Миттєво | 2–5 секунд на сторінку |
| Повнота даних | Всі поля | Обмежено HTML |
| Ризик блокування | Немає | Мінімальний при розумних затримках |
Адаптери для Яндекс.Маркет та Ozon:
class YandexMarketReviewAdapter implements ReviewAdapterInterface { public function fetchReviews(ReviewSource $source): array { $modelId = $source->external_id; $response = Http::withHeaders([ 'Authorization' => 'Bearer ' . config('services.yandex_market.token'), ])->get("https://api.partner.market.yandex.ru/v2/models/{$modelId}/reviews", [ 'count' => 30, 'page' => 1, ]); return collect($response->json('result.reviews', [])) ->map(fn($r) => new ReviewDTO( externalId: (string) $r['id'], author: $r['author']['name'] ?? 'Аноним', rating: (int) $r['grade'], text: $r['text'] ?? '', pros: $r['pros'] ?? null, cons: $r['cons'] ?? null, publishedAt: Carbon::parse($r['date']), )) ->toArray(); } } class OzonReviewAdapter implements ReviewAdapterInterface { public function fetchReviews(ReviewSource $source): array { $data = $this->playwright->evaluate($source->external_url, <<<JS await page.waitForSelector('[data-widget="webReviewProductScore"]', {timeout: 10000}); const items = document.querySelectorAll('[data-widget="webSingleReview"]'); return Array.from(items).map(el => ({ id: el.dataset.reviewId, rating: parseInt(el.querySelector('[data-rating]')?.dataset.rating) || 0, text: el.querySelector('.review-text')?.textContent?.trim() || '', pros: el.querySelector('.pros')?.textContent?.trim() || null, cons: el.querySelector('.cons')?.textContent?.trim() || null, author: el.querySelector('.author-name')?.textContent?.trim() || 'Аноним', date: el.querySelector('time')?.getAttribute('datetime'), })); JS); return collect($data)->map(fn($r) => new ReviewDTO( externalId: $r['id'], author: $r['author'], rating: $r['rating'], text: $r['text'], pros: $r['pros'], cons: $r['cons'], publishedAt: $r['date'] ? Carbon::parse($r['date']) : now(), ))->toArray(); } } Механізм аналізу тональності
Використовуємо два рівні. Якщо рейтинг 1–2 або 4–5 — тональність очевидна. При рейтингу 3 запускаємо аналіз за ключовими словами або AI:
class SentimentAnalyzer { private array $negativeKeywords = [ 'брак', 'сломан', 'не работает', 'возврат', 'обман', 'разочарован', 'ужас', 'кошмар', 'мусор', 'дрянь', ]; private array $positiveKeywords = [ 'отлично', 'супер', 'доволен', 'рекомендую', 'превзошёл', 'быстро', 'качественно', 'спасибо', ]; public function analyze(ReviewDTO $review): string { if ($review->rating <= 2) return 'negative'; if ($review->rating >= 4) return 'positive'; $text = mb_strtolower($review->text . ' ' . $review->cons); foreach ($this->negativeKeywords as $kw) { if (str_contains($text, $kw)) return 'negative'; } return 'neutral'; } public function analyzeWithAI(string $text): string { $response = $this->openai->chat()->create([ 'model' => 'gpt-4o-mini', 'messages' => [ ['role' => 'system', 'content' => 'Определи тональность отзыва. Ответь одним словом: positive, negative или neutral.'], ['role' => 'user', 'content' => $text], ], 'max_tokens' => 10, ]); return in_array($response->choices[0]->message->content, ['positive', 'negative', 'neutral']) ? $response->choices[0]->message->content : 'neutral'; } } Повідомлення надсилаються в Telegram: для негативних відгуків — в окремий чат термінової підтримки, для інших — в загальний. Повідомлення містить рейтинг, текст відгуку та посилання.
class ReviewNotifier { public function notifyNew(Review $review): void { $emoji = match ($review->sentiment) { 'positive' => '⭐', 'negative' => '🚨', default => '💬', }; $stars = str_repeat('★', $review->rating) . str_repeat('☆', 5 - $review->rating); $text = "{$emoji} *Новий відгук* — {$review->source->platform}\n" . "{$stars} {$review->rating}/5\n" . "*{$review->author}*\n\n" . mb_substr($review->text, 0, 300) . (mb_strlen($review->text) > 300 ? '...' : '') . "\n\n" . "[Відкрити відгук]({$review->source->external_url})"; $chatId = $review->sentiment === 'negative' ? config('telegram.urgent_reviews_chat') : config('telegram.reviews_chat'); $this->telegram->sendMessage([ 'chat_id' => $chatId, 'text' => $text, 'parse_mode' => 'Markdown', ]); $review->update(['is_notified' => true]); } } Аналітика рейтингу та розклад перевірки:
-- Агрегат рейтингу по платформах за останні 30 днів SELECT rs.platform, COUNT(*) AS total_reviews, ROUND(AVG(r.rating), 2) AS avg_rating, COUNT(*) FILTER (WHERE r.rating <= 2) AS negative_count, COUNT(*) FILTER (WHERE r.rating >= 4) AS positive_count FROM reviews r JOIN review_sources rs ON r.source_id = rs.id WHERE r.published_at >= NOW() - INTERVAL '30 days' GROUP BY rs.platform ORDER BY avg_rating; -- Швидкі платформи з API — щогодини $schedule->command('reviews:check --platform=yandex_market')->hourly(); -- Парсинг через браузер — кожні 4 години (ресурсомістко) $schedule->command('reviews:check --platform=ozon')->everyFourHours(); $schedule->command('reviews:check --platform=wildberries')->everyFourHours(); -- Щотижневий зведений звіт з трендом рейтингу $schedule->job(new WeeklyReviewsReportJob)->weekly()->mondays()->at('09:00'); Що робити, якщо майданчик змінює HTML-структуру?
Парсинг залежить від верстки. При зміні селекторів адаптер перестає працювати. Ми передбачили моніторинг: раз на добу бот перевіряє, що адаптер повертає непорожній результат. Якщо помилка — повідомлення в Telegram. Ми оперативно оновлюємо селектори. У договорі фіксуємо 2 тижні пост-релізної підтримки, протягом яких адаптери коригуються безкоштовно.
Що входить в роботу?
Проект під ключ включає: проектування схеми даних, розробку адаптерів для трьох майданчиків, налаштування розкладу, реалізацію аналізу тональності, Telegram-повідомлення, дашборд аналітики в адмінці. Терміни: базова версія — 2 дні, повний комплект — до 5 робочих днів. Вартість розраховується індивідуально під ваш стек та кількість майданчиків. Ми гарантуємо стабільну роботу бота при дотриманні лімітів майданчиків.
Зв'яжіться з нами — оцінимо ваш проект за один робочий день. Отримайте консультацію інженера.







