Негативный отзыв на 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 рабочих дней. Стоимость рассчитывается индивидуально под ваш стек и количество площадок. Мы гарантируем стабильную работу бота при соблюдении лимитов площадок.
Свяжитесь с нами — оценим ваш проект за один рабочий день. Получите консультацию инженера.







