Розробка бота для моніторингу появи нових товарів у конкурентів

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка бота для моніторингу появи нових товарів у конкурентів
Середня
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розроблення бота для моніторингу появи нових товарів у конкурентів

Новинки у конкурентів — сигнал для закупок, ціноутворення та SEO. Якщо конкурент виклав нову лінійку, а ви дізналися про це через тиждень — упущені позиції в пошуку і частина аудиторії, що вже обрала інший магазин. Бот відстежує появу нових SKU на сторінках конкурентів і одразу повідомляє команду.

Принцип роботи

Моніторинг нових товарів відрізняється від моніторингу цін: тут потрібно слідкувати не за конкретним URL, а за розділами каталогу — категоріями, сторінками "Новинки", результатами пошуку.

Конфігурація (URL каталогу + селектор) → Scraper → Snapshot → Diff → Alert

Алгоритм:

  1. Завантажити сторінку категорії/розділу конкурента
  2. Вилучити список товарів (URL + назва + SKU)
  3. Порівняти з попереднім снімком
  4. Нові позиції — відправити в повідомлення

Схема даних

CREATE TABLE competitor_catalogs (
    id              BIGSERIAL PRIMARY KEY,
    competitor_id   INT REFERENCES competitors(id),
    url             TEXT NOT NULL,              -- URL сторінки категорії
    scrape_config   JSONB NOT NULL,             -- CSS-селекторы
    check_interval  INTERVAL DEFAULT '6 hours',
    last_checked_at TIMESTAMP,
    is_active       BOOLEAN DEFAULT TRUE
);

-- Кожен товар, виявлений у конкурента
CREATE TABLE competitor_items (
    id              BIGSERIAL PRIMARY KEY,
    catalog_id      BIGINT REFERENCES competitor_catalogs(id),
    external_url    TEXT NOT NULL,
    title           TEXT,
    external_sku    VARCHAR(255),
    price           NUMERIC(12,2),
    image_url       TEXT,
    first_seen_at   TIMESTAMP DEFAULT NOW(),
    last_seen_at    TIMESTAMP DEFAULT NOW(),
    is_new          BOOLEAN DEFAULT TRUE,       -- скинення після повідомлення
    UNIQUE(catalog_id, external_url)
);

CREATE INDEX idx_competitor_items_new
    ON competitor_items(catalog_id, first_seen_at)
    WHERE is_new = TRUE;

Конфігурація через JSON

Кожен конкурент налаштовується через JSONB-конфіг:

{
  "pagination": {
    "type": "url_param",
    "param": "page",
    "max_pages": 20
  },
  "item_selector": ".catalog-item",
  "fields": {
    "url":   {"selector": "a.product-link", "attr": "href"},
    "title": {"selector": ".product-name", "text": true},
    "sku":   {"selector": "[data-sku]", "attr": "data-sku"},
    "price": {"selector": ".price", "text": true},
    "image": {"selector": "img.product-image", "attr": "src"}
  }
}

Scraper для каталогу

class CatalogScraper
{
    public function scrape(CompetitorCatalog $catalog): array
    {
        $config = $catalog->scrape_config;
        $items  = [];

        $maxPages = $config['pagination']['max_pages'] ?? 1;

        for ($page = 1; $page <= $maxPages; $page++) {
            $url  = $this->buildPageUrl($catalog->url, $config['pagination'], $page);
            $html = $this->fetchWithRetry($url);

            if (!$html) break;

            $pageItems = $this->extractItems($html, $config);

            if (empty($pageItems)) break; // Остання сторінка

            $items = array_merge($items, $pageItems);

            // Повага до сервера — затримка між сторінками
            usleep(rand(1_500_000, 3_000_000));
        }

        return $items;
    }

    private function extractItems(string $html, array $config): array
    {
        $crawler = new \Symfony\Component\DomCrawler\Crawler($html);
        $items   = [];

        $crawler->filter($config['item_selector'])->each(function ($node) use ($config, &$items) {
            $item = [];

            foreach ($config['fields'] as $field => $fieldConfig) {
                try {
                    $el = $node->filter($fieldConfig['selector'])->first();
                    if ($el->count() === 0) continue;

                    $item[$field] = isset($fieldConfig['attr'])
                        ? $el->attr($fieldConfig['attr'])
                        : $el->text();
                } catch (\Exception $e) {
                    continue;
                }
            }

            if (!empty($item['url'])) {
                $items[] = $item;
            }
        });

        return $items;
    }
}

Сервіс обнаруження новинок

class NewProductDetectionService
{
    public function process(CompetitorCatalog $catalog): DetectionResult
    {
        $scraped  = $this->scraper->scrape($catalog);
        $newItems = [];

        foreach ($scraped as $item) {
            $url = $this->normalizeUrl($item['url'], $catalog->url);

            $existing = CompetitorItem::where([
                'catalog_id'   => $catalog->id,
                'external_url' => $url,
            ])->first();

            if (!$existing) {
                // Новий товар!
                $created = CompetitorItem::create([
                    'catalog_id'   => $catalog->id,
                    'external_url' => $url,
                    'title'        => $item['title'] ?? null,
                    'external_sku' => $item['sku'] ?? null,
                    'price'        => $this->parsePrice($item['price'] ?? ''),
                    'image_url'    => $item['image'] ?? null,
                    'is_new'       => true,
                ]);
                $newItems[] = $created;
            } else {
                // Оновити час останнього виявлення
                $existing->update(['last_seen_at' => now()]);
            }
        }

        // Товари, що зникли з каталогу конкурента
        $disappeared = CompetitorItem::where('catalog_id', $catalog->id)
            ->where('last_seen_at', '<', now()->subDays(3))
            ->get();

        $catalog->update(['last_checked_at' => now()]);

        return new DetectionResult(newItems: $newItems, disappeared: $disappeared);
    }
}

Повідомлення в Telegram

class NewProductNotifier
{
    public function notify(DetectionResult $result, CompetitorCatalog $catalog): void
    {
        if ($result->newItems->isEmpty()) return;

        $lines = ["🆕 *Нові товари у {$catalog->competitor->name}*\n"];

        foreach ($result->newItems->take(10) as $item) {
            $price = $item->price ? number_format($item->price, 0, '.', ' ') . ' грн.' : 'ціна не визначена';
            $lines[] = "• [{$item->title}]({$item->external_url}) — {$price}";
        }

        if ($result->newItems->count() > 10) {
            $lines[] = "\n_...і ще " . ($result->newItems->count() - 10) . " товарів_";
        }

        $this->telegram->sendMessage([
            'chat_id'                  => config('telegram.new_products_chat'),
            'text'                     => implode("\n", $lines),
            'parse_mode'               => 'Markdown',
            'disable_web_page_preview' => true,
        ]);

        // Скинути прапор is_new
        CompetitorItem::whereIn('id', $result->newItems->pluck('id'))
            ->update(['is_new' => false]);
    }
}

Обхід захистів від ботів

Більшість крупних магазинів використовують захист. Стратегії обходу:

Захист Метод обходу
Cloudflare Bot Management Playwright + stealth plugin
Rate limiting Випадкові затримки 2–5 сек між запитами
IP-блокування Ротація проксі (residential proxies)
Потребує cookies/сессії Headless-браузер із збереженням сессії
JS-рендеринг Playwright/Puppeteer замість curl

Розписання

// Перевірка кожні 6 годин для стандартних каталогів
$schedule->command('competitors:scan-catalogs')->everySixHours();

// Щотижневий зведений звіт
$schedule->job(new WeeklyNewProductsReportJob)->weekly()->mondays()->at('08:00');

Додаткові можливості

  • Автоматичне додавання в список закупок — знайдені новинки конкурента одразу йдуть в завдання байєра
  • Повідомлення про зникнення — товар пропав у конкурента (знятий з продажу, немає в наявності)
  • Ценове порівняння — якщо аналог є в нашому каталозі, одразу показати різницю цін

Строки реалізації

  • CatalogScraper + конфігурація через JSON: 1–2 дні
  • NewProductDetectionService + схема даних: 1 день
  • Повідомлення Telegram: 0.5 дня
  • Playwright-адаптер для JS-сайтів: +1 день
  • Ротація проксі: +0.5 дня

Разом: 3–4 робочих дні.