Разработка бота для мониторинга цен конкурентов с отчётами

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка бота для мониторинга цен конкурентов с отчётами
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    824

Разработка бота для мониторинга цен конкурентов с отчётами

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

Архитектура

Scheduler (cron) → Scraper Workers → Price DB → Analytics → Reports/Alerts

Ключевые компоненты:

  • Scraper — получает HTML или JSON с сайта конкурента
  • Parser — извлекает цену из полученного контента
  • Storage — хранит историю изменений
  • Analyzer — вычисляет дельты, тренды
  • Notifier — отправляет отчёты в Telegram/Email

Схема данных

CREATE TABLE monitored_products (
    id              BIGSERIAL PRIMARY KEY,
    our_product_id  BIGINT REFERENCES products(id),
    competitor_id   INT REFERENCES competitors(id),
    url             TEXT NOT NULL,
    selector        VARCHAR(500),          -- CSS-селектор для цены
    last_price      NUMERIC(12,2),
    last_checked_at TIMESTAMP,
    is_active       BOOLEAN DEFAULT TRUE,
    UNIQUE(competitor_id, url)
);

CREATE TABLE price_snapshots (
    id              BIGSERIAL PRIMARY KEY,
    monitored_id    BIGINT REFERENCES monitored_products(id),
    price           NUMERIC(12,2),
    in_stock        BOOLEAN,
    raw_text        VARCHAR(100),          -- "29 990 руб." до парсинга
    captured_at     TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_snapshots_monitored_captured
    ON price_snapshots(monitored_id, captured_at DESC);

Scraper с ротацией User-Agent и прокси

class CompetitorScraper
{
    private array $userAgents = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15...',
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36...',
    ];

    public function fetch(string $url): ?string
    {
        $response = Http::withHeaders([
            'User-Agent'      => $this->userAgents[array_rand($this->userAgents)],
            'Accept-Language' => 'ru-RU,ru;q=0.9',
            'Accept-Encoding' => 'gzip, deflate, br',
        ])
        ->timeout(15)
        ->retry(3, 2000, fn($e) => $e instanceof ConnectionException)
        ->get($url);

        if ($response->status() === 429) {
            // Rate limit — задержка перед retry
            sleep(rand(30, 60));
            return null;
        }

        if (!$response->successful()) {
            Log::warning("Scraper failed: {$url}", ['status' => $response->status()]);
            return null;
        }

        return $response->body();
    }
}

Для сайтов с JavaScript-рендерингом используется Playwright через API или Browserless.io:

class PlaywrightScraper
{
    public function fetch(string $url): ?string
    {
        $response = Http::post(config('scraper.browserless_url') . '/content', [
            'url'     => $url,
            'waitFor' => '.price, [data-price], .product-price',  // ждём появления элемента
            'options' => ['timeout' => 20000],
        ]);

        return $response->successful() ? $response->body() : null;
    }
}

Парсер цен

class PriceParser
{
    public function parse(string $html, MonitoredProduct $config): ?ParsedPrice
    {
        $crawler = new Symfony\Component\DomCrawler\Crawler($html);

        // Попробовать CSS-селектор из конфигурации
        if ($config->selector) {
            try {
                $text = $crawler->filter($config->selector)->first()->text();
                return $this->extractPrice($text);
            } catch (\Exception $e) {
                // Селектор не сработал — fallback на эвристику
            }
        }

        // Эвристический поиск по типичным паттернам
        $priceSelectors = [
            '[itemprop="price"]',
            '.price__current',
            '.product-price',
            '[data-price]',
            '.js-price',
        ];

        foreach ($priceSelectors as $selector) {
            try {
                $node = $crawler->filter($selector)->first();
                if ($node->count()) {
                    // Попробовать атрибут data-price сначала
                    $dataPrice = $node->attr('data-price') ?? $node->attr('content');
                    if ($dataPrice && is_numeric($dataPrice)) {
                        return new ParsedPrice(price: (float) $dataPrice, rawText: $dataPrice);
                    }
                    return $this->extractPrice($node->text());
                }
            } catch (\Exception $e) {
                continue;
            }
        }

        return null;
    }

    private function extractPrice(string $text): ?ParsedPrice
    {
        // Убрать пробелы, заменить запятую на точку
        $normalized = preg_replace('/[^\d,.]/', '', $text);
        $normalized = str_replace(',', '.', $normalized);

        // Убрать разделители тысяч: "29.990" → "29990" (если нет копеек)
        if (preg_match('/^\d{1,3}[.]\d{3}$/', $normalized)) {
            $normalized = str_replace('.', '', $normalized);
        }

        if (!is_numeric($normalized) || (float)$normalized <= 0) {
            return null;
        }

        return new ParsedPrice(price: (float)$normalized, rawText: $text);
    }
}

Job для проверки цены

class CheckCompetitorPriceJob implements ShouldQueue
{
    public int $timeout = 60;

    public function handle(
        CompetitorScraper $scraper,
        PriceParser       $parser,
        PriceAlertService $alerts,
    ): void {
        $monitored = MonitoredProduct::findOrFail($this->monitoredId);

        $html   = $scraper->fetch($monitored->url);
        if (!$html) return;

        $parsed = $parser->parse($html, $monitored);
        if (!$parsed) {
            Log::warning("Price parse failed", ['url' => $monitored->url]);
            return;
        }

        PriceSnapshot::create([
            'monitored_id' => $monitored->id,
            'price'        => $parsed->price,
            'in_stock'     => $parsed->inStock,
            'raw_text'     => $parsed->rawText,
        ]);

        // Уведомление при значимом изменении
        if ($monitored->last_price) {
            $changePct = abs($parsed->price - $monitored->last_price) / $monitored->last_price * 100;
            if ($changePct >= 5) {
                $alerts->priceChanged($monitored, $monitored->last_price, $parsed->price);
            }
        }

        $monitored->update([
            'last_price'      => $parsed->price,
            'last_checked_at' => now(),
        ]);
    }
}

Отчёты

Ежедневный отчёт формируется по расписанию и отправляется в Telegram-группу:

class DailyPriceReportJob implements ShouldQueue
{
    public function handle(): void
    {
        $report = $this->buildReport();
        $this->telegram->sendMessage([
            'chat_id'    => config('telegram.price_alerts_chat'),
            'text'       => $this->formatMarkdown($report),
            'parse_mode' => 'Markdown',
        ]);
    }

    private function buildReport(): array
    {
        return MonitoredProduct::with(['ourProduct', 'competitor'])
            ->whereHas('snapshots', fn($q) => $q->where('captured_at', '>=', now()->subDay()))
            ->get()
            ->map(function ($m) {
                $yesterday = $m->snapshots()->where('captured_at', '>=', now()->subDays(2))
                    ->where('captured_at', '<', now()->subDay())->avg('price');
                $today = $m->last_price;

                return [
                    'product'    => $m->ourProduct->name,
                    'competitor' => $m->competitor->name,
                    'price_now'  => $today,
                    'price_was'  => $yesterday,
                    'delta_pct'  => $yesterday ? round(($today - $yesterday) / $yesterday * 100, 1) : null,
                    'our_price'  => $m->ourProduct->price,
                    'position'   => $today < $m->ourProduct->price ? 'cheaper' : 'expensive',
                ];
            })
            ->toArray();
    }
}

Расписание проверок

// Высокоприоритетные конкуренты — каждые 2 часа
$schedule->command('monitor:check --priority=high')->everyTwoHours();

// Остальные — раз в 6 часов
$schedule->command('monitor:check --priority=normal')->everySixHours();

// Отчёт — в 9:00 ежедневно
$schedule->job(new DailyPriceReportJob)->dailyAt('09:00');

Сроки реализации

  • Scraper + PriceParser + схема данных: 1–2 дня
  • Playwright-адаптер для JS-сайтов: +1 день
  • Job-система + расписание: 0.5 дня
  • Отчёты в Telegram + Email: 1 день
  • Интерфейс управления списком конкурентов в админке: 1 день

Итого: 4–5 рабочих дней.