תארו לעצמכם שהמתחרה הראשי שלכם משיק מבצע בזק עם הנחה של 40% על מוצר פופולרי, ואתם מגלים על כך רק יומיים אחרי — אחרי שהלקוחות כבר עזבו. ניטור ידני שוטף של אתרי מתחרים גוזל שעות רבות מזמנו של איש שיווק, וקל לפספס מבצעים פתאומיים. סקרפר מוצא מבצעים פי 10 מהר יותר מאשר ניטור ידני — מידע על עסקה חדשה מגיע תוך דקות, לא שעות. אנו מפתחים בוטים לסקראפינג שסורקים ברציפות את דפי המבצעים של המתחרים שלכם, מחלצים תנאים, קודי קופון ומועדי סיום, ומודיעים לכם מיידית דרך Slack או Telegram. לצוות שלנו ניסיון רב בבניית בוט לסקראפינג מבצעי מתחרים, עם למעלה מ-50 פרויקטים של אוטומציה לניתוח תחרותי. תוצאות מובטחות עם המפתחים המוסמכים שלנו. מקרה לדוגמה: חנות מסחר אלקטרוני למוצרי חשמל צמצמה את זמן התגובה למבצעי מתחרים מיומיים לשעתיים לאחר הטמעת הפתרון שלנו, וחסכה יותר מ-15 שעות עבודה שבועיות. תקופת ההחזר הייתה מתחת לחודשיים. חיסכון חודשי אופייני: $2,000–$5,000 בזמן אנליסט. הסקרפר בנוי על PHP 8.3 ו-Laravel 11 באמצעות DomCrawler, עם אחסון נתונים ב-PostgreSQL. גישה זו מאפשרת תגובה מהירה למבצעים, ניטור מחירים אוטומטי וניתוח ארוך טווח של מגמות מתחרים — אילו מוצרים מקבלים הנחות תכופות, גודל הנחה ממוצע, עונתיות. הפיתוח מתחיל ב-$3,500.
אילו בעיות פותר הסקרפר למבצעים?
ניטור ידני לא יכול לעמוד בקצב הדינמיקה של מבצעים: הנחות משתנות משעה לשעה, קודי קופון מוגבלים בזמן, ומבני הדפים משתנים בין אתרים. אתגרים אופייניים: מבצעים עם סוגי הנחות שונים (אחוז, מחיר קבוע, קוד קופון) דורשים שיטות חילוץ שונות; תאריכי התחלה וסיום מוסתרים לעיתים קרובות בטקסט או ניתנים בפורמטים לא סטנדרטיים; קודי קופון עשויים להיות מקודדים או מוסתרים ב-HTML. הסקרפר פותר את כל הבעיות הללו: הוא מחפש בלוקים מובנים (.promotion-card, .sale-block) ואם הם חסרים, מנתח את הטקסט באמצעות ביטויים רגולריים. לדוגמה, אם תאריך כתוב כ"עד סוף השבוע," הסקרפר מחשב את התאריך המדויק מהלוח; אם קוד קופון מופיע רק לאחר לחיצה, הוא מדמה לחיצה באמצעות דפדפן ללא ראש.
מדוע ניטור מבצעים חשוב יותר מניטור מחירים?
מחירים משתנים בתדירות נמוכה יותר ובאופן צפוי, בעוד שמבצעים הם מנוע תנועה חזק. פספוס מבצע של מתחרה יכול לעלות לכם בעד 30% מההמרות. הסקרפר נותן לכם מידע תוך דקות, ומאפשר תגובת נגד מיידית. הניסיון שלנו מראה שלמעלה מ-80% מהלקוחות מתחילים קמפיין נגדי תוך שעה מקבלת ההודעה. הפתרון שלנו מספק ניטור הנחות מקיף על פני מספר מתחרים. בוט מעקב מבצעים זה מיועד למסחר אלקטרוני. הוא מתמחה בחילוץ קודי קופון וסקראפינג. המערכת כוללת לוח מחוונים לניתוח מבצעי מתחרים. זהו סקרפר למסחר אלקטרוני המותאם לפלטפורמות חנות שונות. אנו מציעים שירותי פיתוח סקרפרים מקצועיים ב-PHP. הסקרפר יכול לטפל בסקראפינג של אתרי הנחות עבור דומיינים מרובים. ניתן להגדיר התראות הנחה לכל סף שתרצו.
כיצד עובד הסקרפר למבצעים?
אנו משתמשים ב-PHP 8.3, Laravel 11, Symfony DomCrawler לסקראפינג, ו-PostgreSQL לאחסון. הרכיבים המרכזיים מתוארים להלן.
סקרפר דפי מבצעים
מחלץ מבצעים מבלוקים מובנים. אם בלוקים לא נמצאו, עובר לניתוח טקסט.
// 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;
}
} גלאי מכירות מוצרים
משווה מחירים ישנים וחדשים, מחשב אחוז הנחה, ומוצא את תאריך הסיום מהספירה לאחור.
// 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,
);
}
} אחסון והיסטוריה
כל המבצעים מאוחסנים בטבלת // 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; } } עם היסטוריית שינויים.
// Миграция
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% מהאתרים | נמוכה |
| ניתוח טקסט עם ביטויים רגולריים | 80% | 90% מהאתרים | בינונית |
| דפדפן ללא ראש | 99% | 99% מהאתרים | גבוהה |
כיצד ליישם את הסקרפר: שלב אחר שלב
- ניתוח אתרי מתחרים — זיהוי מבנה דף המבצעים.
- עיצוב ארכיטקטורת הסקרפר וסכימת הנתונים.
- פיתוח הסקרפר וגלאי המכירות.
- בדיקה על 3–5 אתרים, תיקון באגים.
- פריסה לשרת, הגדרת cron והתראות.
תהליך העבודה
| שלב | משך | תוצאה |
|---|---|---|
| ניתוח אתרים | יום אחד | זיהוי מבנה דף המבצעים |
| עיצוב | יום אחד | ארכיטקטורת סקרפר, סכימת נתונים |
| פיתוח | 2–3 ימים | קוד סקרפר, גלאי מכירות, התראות |
| בדיקות | יום אחד | אימות על 3–5 אתרים, תיקון באגים |
| פריסה | חצי יום | פריסה לשרת, הגדרת cron |
טעויות סקראפינג אופייניות
- מבצע מוסתר מאחורי רינדור JavaScript — יש צורך בדפדפן ללא ראש.
- קוד קופון מופיע רק לאחר לחיצה — דורש הדמיית פעולה.
- תאריכים בפורמט יחסי ("עד סוף השבוע") — דורשים מפענח מותאם אישית.
- שינוי במבנה האתר שובר את הסקרפר — יש להשתמש באלגוריתמי גיבוי.
מה כלול בעבודה
- סקרפר ל-3–5 אתרי מתחרים (מפתח מלא)
- לוח מחוונים לצפייה בהיסטוריית מבצעים
- התראות דרך Slack (Telegram לפי בקשה)
- תיעוד ו-API לאינטגרציה
- הדרכת צוות (שעה אחת)
- חודש תמיכה לאחר הפריסה
- עלות פיתוח: מ-$3,500
הערכות לוחות זמנים
פיתוח פונקציונליות בסיסית — 5 עד 8 ימי עבודה. פרויקטים מורכבים עם מבני אתרים לא סטנדרטיים — עד 14 יום. אנו מספקים הערכה מדויקת לאחר בדיקת המשאבים היעד. צרו קשר להערכת פרויקט. קבלו ייעוץ והזמינו פיתוח סקרפר כדי להשיג יתרון תחרותי.







