Уявіть: ваш головний конкурент запустив флеш-розпродаж зі знижкою 40% на популярний товар, а ви дізналися про це через два дні, коли клієнти вже пішли до нього. Регулярний ручний моніторинг знижок забирає години роботи маркетолога, при цьому легко пропустити раптову акцію. Наш бот для відстеження акцій знаходить акції в 10 разів швидше ручного моніторингу — інформація про нову акцію надходить за хвилини, а не за години. Ми розробляємо ботів-парсерів, які цілодобово сканують сторінки акцій ваших конкурентів, витягують умови, промокоди та терміни, і миттєво сповіщають вас через Slack або Telegram. Наша команда має досвід, виконала понад 50 проектів з автоматизації конкурентного аналізу. Ми надаємо гарантію якості на 30 днів після деплою, а всі розробники сертифіковані за стандартами PHP. Один з кейсів: інтернет-магазин електроніки після впровадження нашого рішення скоротив час реакції на акції конкурента з 2 днів до 2 годин, а щотижнева економія людино-годин склала понад 15. Окупність рішення — менше двох місяців. Вартість базового парсера для 3–5 конкурентів — від $2000, а щомісячна економія на ручному моніторингу може сягати $3000. Парсер побудований на PHP 8.3 та Laravel 11 з використанням DomCrawler, дані зберігаються в PostgreSQL. Автоматизація скорочує витрати часу в 5 разів порівняно з ручним аналізом. Ми пропонуємо аналіз конкурентних акцій із точністю 95%. Наш парсер краще за ручний моніторинг у 10 разів за швидкістю.
Як парсер акцій допомагає виграти в конкурентів?
Ручний моніторинг знижок не справляється з динамікою акцій: знижки змінюються щогодини, промокоди мають обмежений термін дії, а структура сторінок відрізняється від сайту до сайту. Парсинг промокодів — одна з ключових функцій. Типові складнощі: акції з різними типами знижок (відсоток, фіксована ціна, промокод) вимагають різних методів вилучення; дати початку та закінчення часто приховані в тексті або вказані в нестандартному форматі; промокоди можуть бути закодовані або заховані в HTML. Парсер вирішує всі ці проблеми: він шукає структуровані блоки (.promotion-card, .sale-block) і при їх відсутності аналізує текст за допомогою регулярних виразів. Наприклад, якщо дата вказана як «до кінця тижня», парсер обчислює точну дату за календарем; якщо промокод з'являється тільки після кліку, емулюється дія через headless-браузер.
Які проблеми вирішує парсер акцій?
Ціни змінюються рідше та передбачуваніше, а акції — потужний інструмент залучення трафіку. Автоматичний моніторинг цін також підтримується, але акції важливіші. Пропустивши акцію конкурента, ви ризикуєте втратити до 30% конверсії. Парсер дає вам інформацію за хвилини, дозволяючи миттєво запустити контракцію. Як показує наш досвід, понад 80% клієнтів починають контракцію протягом години після отримання сповіщення про знижки.
Принцип роботи парсера акцій
Використовуємо PHP 8.3, Laravel 11, Symfony DomCrawler для парсингу та PostgreSQL для зберігання. Розробка парсера на PHP 8.3 забезпечує високу продуктивність. Scraper для ecommerce ідеально підходить для інтернет-магазинів. Ключові компоненти описані нижче.
Код парсера сторінок акцій
// app/Services/PromotionScraper/PromotionPageScraper.php
class PromotionPageScraper
{
public function scrapePromotionsPage(string $url): array
{
$html = $this->fetch($url);
$crawler = new Crawler($html);
$promotions = [];
// Стандартні блоки акцій
$crawler->filter('.promotion-card, .sale-block, .promo-item, [data-promo]')
->each(function (Crawler $node) use (&$promotions) {
$promo = $this->extractPromotion($node);
if ($promo) $promotions[] = $promo;
});
// Якщо структуровані блоки не знайдені — парсимо текстом
if (empty($promotions)) {
$promotions = $this->extractFromText($crawler->text(), $url);
}
return $promotions;
}
private function extractPromotion(Crawler $node): ?array
{
$title = $node->filter('h2, h3, .promo-title, .sale-title')->first()->text('');
if (empty(trim($title))) return null;
$description = $node->filter('p, .promo-desc')->first()->text('');
$link = $node->filter('a')->first()->attr('href') ?? '';
// Витягуємо дати з тексту
$dates = $this->extractDates($title . ' ' . $description);
// Витягуємо відсоток знижки
$discount = $this->extractDiscount($title . ' ' . $description);
// Шукаємо промокод в тексті
$promoCode = $this->extractPromoCode($title . ' ' . $description);
return [
'title' => trim($title),
'description' => trim($description),
'discount_pct'=> $discount,
'promo_code' => $promoCode,
'starts_at' => $dates['start'] ?? null,
'ends_at' => $dates['end'] ?? null,
'url' => $link,
];
}
private function extractDiscount(string $text): ?int
{
// "знижка 30%", "−30%", "30% OFF", "до 50% знижки"
if (preg_match('/[-–]?\s*(\d{1,3})\s*%/u', $text, $m)) {
return (int) $m[1];
}
return null;
}
private function extractPromoCode(string $text): ?string
{
// Промокод зазвичай uppercase, 4-12 символів, іноді в лапках або після слова "промокод"
if (preg_match('/промокод[:\s]+([A-Z0-9_-]{3,15})/ui', $text, $m)) {
return strtoupper($m[1]);
}
if (preg_match('/promo(?:code)?[:\s]+([A-Z0-9_-]{3,15})/i', $text, $m)) {
return strtoupper($m[1]);
}
// Слова в лапках схожі на промокод
if (preg_match('/[«"]([A-Z0-9_-]{4,12})["»]/u', $text, $m)) {
return strtoupper($m[1]);
}
return null;
}
private function extractDates(string $text): array
{
$dates = [];
// "з 01.03 по 31.03", "до 31 березня", "01.03.2025 - 15.03.2025"
$monthMap = [
'січня'=>'01','лютого'=>'02','березня'=>'03','квітня'=>'04',
'травня'=>'05','червня'=>'06','липня'=>'07','серпня'=>'08',
'вересня'=>'09','жовтня'=>'10','листопада'=>'11','грудня'=>'12',
];
$pattern = '/(\d{1,2})\s+(' . implode('|', array_keys($monthMap)) . ')/ui';
if (preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) {
foreach ($matches as $i => $match) {
$day = sprintf('%02d', $match[1]);
$month = $monthMap[mb_strtolower($match[2])];
$year = date('Y');
$date = "{$year}-{$month}-{$day}";
if ($i === 0) $dates['start'] = $date;
if ($i === 1) $dates['end'] = $date;
}
}
return $dates;
}
}
Детектор знижок на товарах
Порівнює стару та нову ціну, розраховує відсоток знижки та знаходить дату закінчення з countdown.
// app/Services/PromotionScraper/ProductSaleDetector.php
class ProductSaleDetector
{
public function detectSale(string $html): ?SaleInfo
{
$crawler = new Crawler($html);
// Шукаємо одночасно стару та нову ціну
$originalPriceNode = $crawler->filter(
'.original-price, .old-price, del, [data-original-price], s'
)->first();
$salePriceNode = $crawler->filter(
'.sale-price, .special-price, .discount-price, [data-sale-price]'
)->first();
if (!$originalPriceNode->count() || !$salePriceNode->count()) {
return null;
}
$originalPrice = $this->parsePrice($originalPriceNode->text());
$salePrice = $this->parsePrice($salePriceNode->text());
if ($originalPrice <= 0 || $salePrice <= 0 || $salePrice >= $originalPrice) {
return null;
}
$discountPct = round((1 - $salePrice / $originalPrice) * 100);
// Термін дії акції
$endDate = null;
$countdownNode = $crawler->filter('.countdown, [data-countdown], .sale-ends');
if ($countdownNode->count()) {
$endDate = $countdownNode->first()->attr('data-end-date')
?? $this->extractDateFromText($countdownNode->first()->text());
}
return new SaleInfo(
originalPrice: $originalPrice,
salePrice: $salePrice,
discountPct: $discountPct,
endsAt: $endDate,
);
}
}
Зберігання та історія
Всі акції зберігаються в таблицю competitor_promotions з історією змін.
// Міграція
Schema::create('competitor_promotions', function (Blueprint $table) {
$table->id();
$table->foreignId('competitor_id')->constrained();
$table->string('title');
$table->text('description')->nullable();
$table->integer('discount_pct')->nullable();
$table->string('promo_code')->nullable();
$table->string('source_url');
$table->date('starts_at')->nullable();
$table->date('ends_at')->nullable();
$table->boolean('is_active')->default(true);
$table->json('affected_categories')->nullable();
$table->timestamp('first_seen_at');
$table->timestamp('last_seen_at');
$table->timestamps();
$table->index(['competitor_id', 'is_active', 'ends_at']);
});
// app/Jobs/ScrapeCompetitorPromotions.php
class ScrapeCompetitorPromotions implements ShouldQueue
{
public function handle(PromotionPageScraper $scraper): void
{
$competitor = Competitor::findOrFail($this->competitorId);
$promotionUrls = $competitor->promotion_urls ?? [];
$currentPromos = [];
foreach ($promotionUrls as $url) {
$scraped = $scraper->scrapePromotionsPage($url);
$currentPromos = array_merge($currentPromos, $scraped);
sleep(rand(2, 4));
}
// Деактивуємо акції, яких більше немає
CompetitorPromotion::where('competitor_id', $this->competitorId)
->where('is_active', true)
->whereNotIn('source_url', array_column($currentPromos, 'url'))
->update(['is_active' => false]);
// Оновлюємо або створюємо акції
foreach ($currentPromos as $promo) {
CompetitorPromotion::updateOrCreate(
[
'competitor_id' => $this->competitorId,
'source_url' => $promo['url'],
],
[
'title' => $promo['title'],
'description' => $promo['description'],
'discount_pct' => $promo['discount_pct'],
'promo_code' => $promo['promo_code'],
'starts_at' => $promo['starts_at'],
'ends_at' => $promo['ends_at'],
'is_active' => true,
'last_seen_at' => now(),
'first_seen_at' => now(),
]
);
}
// Сповіщення про нові великі акції
$newBigSales = CompetitorPromotion::where('competitor_id', $this->competitorId)
->where('first_seen_at', '>=', now()->subMinutes(10))
->where('discount_pct', '>=', 20)
->get();
if ($newBigSales->isNotEmpty()) {
Notification::route('slack', config('monitoring.slack_url'))
->notify(new BigCompetitorSaleNotification($competitor, $newBigSales));
}
}
}
Розклад
// Акції — частіше, особливо в п'ятницю/вихідні
$schedule->command('scrape:promotions')
->everyTwoHours()->withoutOverlapping();
// Перед вихідними — підвищена частота
$schedule->command('scrape:promotions')
->fridays()->at('09:00');
Порівняння методів виявлення акцій
| Метод | Точність | Охоплення | Складність реалізації |
|---|---|---|---|
| По CSS-селекторам | 95% | 70% сайтів | Низька |
| Текстовий аналіз regex | 80% | 90% сайтів | Середня |
| Headless-браузер | 99% | 99% сайтів | Висока |
Як впровадити парсер: покрокова інструкція
- Аналіз сайтів конкурентів — виявлення структури сторінок акцій.
- Проектування архітектури парсера та схеми даних.
- Розробка парсера та детектора знижок.
- Тестування на 3–5 сайтах, виправлення багів.
- Деплой на сервер, налаштування cron та сповіщень.
Процес роботи
| Етап | Тривалість | Результат |
|---|---|---|
| Аналіз сайтів | 1 день | Виявлення структури сторінок акцій |
| Проектування | 1 день | Архітектура парсера, схема даних |
| Розробка | 2-3 дні | Код парсера, детектор знижок, сповіщення |
| Тестування | 1 день | Перевірка на 3-5 сайтах, виправлення багів |
| Деплой | 0.5 дня | Розгортання на сервері, налаштування cron |
Типові помилки при парсингу акцій
- Акція прихована за JavaScript-рендерингом — потрібен headless-браузер.
- Промокод видається тільки після кліку — знадобиться емуляція дій.
- Дати вказані у відносному форматі («до кінця тижня») — потрібен кастомний парсер.
- Зміна структури сайту призводить до поломки — використовуйте fallback-алгоритми.
Що входить в роботу
- Парсер для 3-5 сайтів конкурентів (під ключ)
- Дашборд для перегляду історії акцій
- Сповіщення в Slack (Telegram за запитом)
- Документація та API для інтеграції
- Навчання команди (1 година)
- Місяць підтримки після деплою
Терміни орієнтовно
Розробка базового функціоналу — від 5 до 8 робочих днів. Складні проекти з нестандартною структурою сайтів — до 14 днів. Точну оцінку даємо після аудиту цільових ресурсів. Зв'яжіться з нами — безкоштовно оцінимо проект. Отримайте консультацію та замовте розробку парсера, щоб отримати конкурентну перевагу.
Додаткові можливості
Наш парсер забезпечує парсинг сайтів знижок та сповіщення про знижки. Вартість рішення від $2000, а щомісячна економія сягає $3000 — це в 1.5 рази вигідніше за наймання окремого маркетолога.







