מדוע ניטור מחירים הוא קריטי לעסק שלך?
מתחרים משנים מחירים כל יום. אם לא תגיב בזמן, תפסיד מכירות. ניטור מחירים נותן לך יתרון: אתה רואה כל הפחתת מחיר של מתחרה תוך שעה. עבור מפיצים, מדובר בהבטחת עמידה ב-MAP (מחיר פרסום מינימלי). הפרות נרשמות אוטומטית, ואתה יכול להטיל קנסות. קבל התראות על הנחות כאשר מחירים יורדים מתחת לסף. מערכת ניטור המפיצים שלנו רושמת אוטומטית הפרות ויכולה להטיל קנסות. עם ניתוח מתחרים, אתה רואה כל הפחתת מחיר תוך שעה.
אנו מיישמים מערכת לניטור מחירים ומעקב מלאי באתרים חיצוניים, תוך התמודדות עם האתגר העומד בפני בעלי חנויות מקוונות ומנהלי רכש. התרחיש הראשון: מעקב אחר מפיצים כדי להבטיח שהם לא מפרים את מחיר הקמעונאות המומלץ. השני: צפייה במתחרים עבור SKU ספציפיים כדי להתאים במהירות את המחיר שלך. המערכת שלנו בודקת באופן קבוע כתובות URL שצוינו, משווה תוצאות לערכים קודמים, ומתריעה כאשר חריגות עוברות סף מוגדר.
הניסיון שלנו מראה: בדיקה ידנית של 50 SKU אורכת עד 10 שעות בשבוע. בעלות ממוצעת של עובד של $50 לשעה, זה $500 לשבוע. אוטומציה מחזירה את עצמה תוך 2–3 חודשים עם 100+ מוצרים. עבור 500 SKU, החיסכון עולה על 150 שעות בחודש, כלומר חיסכון חודשי של $7,500. עלות פרויקט טיפוסית נעה בין $2,500 ל-$5,000, עם תקופת החזר של פחות מ-3 חודשים. אנו מבטיחים דיוק נתונים של 99.5% — מאומת על 200+ פרויקטים. המערכת שלנו מתמודדת עם עד 10,000 כתובות URL לשרת, כאשר כל בדיקה אורכת כ-2 שניות. בהשוואה לבדיקה ידנית, המערכת האוטומטית שלנו טובה פי 100 במהירות ופי 10 בדיוק (שגיאה של 0.5% לעומת 5%).
כיצד פועל מפענח המחירים הגמיש?
אתרים שונים מאחסנים מחירים בדרכים שונות. המפענח תומך במספר מצבים. למד עוד על נתונים מובנים ב-Schema.org.
URL List → Scheduler → Fetcher → Parser → Comparator → Alert Engine ↓ Snapshot Store | שיטה | מהירות | אמינות | אוניברסליות |
|---|---|---|---|
| טקסט (בורר CSS) | גבוהה | בינונית (תלוי בסימון) | נמוכה |
| attr (מאפיין) | גבוהה | גבוהה (אם המאפיין סטטי) | בינונית |
| meta (תגי מטא) | גבוהה | גבוהה (מתוקנן) | גבוהה |
| json (בלוק JSON) | בינונית | גבוהה | בינונית |
| ld (JSON-LD Schema) | בינונית | גבוהה מאוד (נתונים מובנים) | גבוהה מאוד |
לא יודע איזו שיטה מתאימה לאתרים שלך? המהנדסים שלנו ינתחו את מבנה העמוד ויבחרו את המפענח האופטימלי. המפענח הגמיש מתאים עצמו לכל מבנה אתר, ומחלץ נתונים מ-HTML, JSON-LD, תגי מטא או בלוקי JSON.
ארכיטקטורת המערכת ומודל הנתונים
CREATE TABLE watch_targets ( id BIGSERIAL PRIMARY KEY, url TEXT NOT NULL UNIQUE, label VARCHAR(255), our_product_id BIGINT REFERENCES products(id), site_id INT REFERENCES external_sites(id), check_interval INTERVAL DEFAULT '4 hours', price_selector VARCHAR(500), stock_selector VARCHAR(500), price_type VARCHAR(20) DEFAULT 'text', price_attr VARCHAR(100), price_regex VARCHAR(255), alert_threshold_pct NUMERIC(5,2) DEFAULT 5.0, is_active BOOLEAN DEFAULT TRUE, last_checked_at TIMESTAMP, last_price NUMERIC(12,2), last_in_stock BOOLEAN ); CREATE TABLE watch_snapshots ( id BIGSERIAL PRIMARY KEY, target_id BIGINT REFERENCES watch_targets(id) ON DELETE CASCADE, price NUMERIC(12,2), in_stock BOOLEAN, raw_price_text VARCHAR(200), http_status SMALLINT, error TEXT, captured_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_snapshots_target_time ON watch_snapshots(target_id, captured_at DESC); תכונה מרכזית: המערכת מאחסנת את היסטוריית הערכים, לא רק את הערך הנוכחי. זה מאפשר בניית גרפי שינויים וזיהוי דפוסים. צפה בגרפי היסטוריית מחירים בפאנל הניהול.
class FlexiblePriceExtractor { public function extract(string $html, WatchTarget $target): ?ExtractedValue { return match ($target->price_type) { 'text' => $this->extractText($html, $target), 'attr' => $this->extractAttr($html, $target), 'meta' => $this->extractMeta($html, $target), 'json' => $this->extractJson($html, $target), 'ld' => $this->extractLdJson($html), default => null, }; } private function extractLdJson(string $html): ?ExtractedValue { // <cite>Schema.org Product markup</cite> — универсальный для многих магазинов // Подробнее: [Schema.org](https://ru.wikipedia.org/wiki/Schema.org) $crawler = new Crawler($html); $nodes = $crawler->filter('script[type="application/ld+json"]'); foreach ($nodes as $node) { $data = json_decode($node->textContent, true); if (!$data) continue; $type = $data['@type'] ?? $data[0]['@type'] ?? null; if (!in_array($type, ['Product', 'Offer'])) continue; $offer = $data['offers'] ?? $data; if (is_array($offer) && isset($offer[0])) $offer = $offer[0]; $price = $offer['price'] ?? null; $inStock = ($offer['availability'] ?? '') === 'https://schema.org/InStock'; if ($price !== null) { return new ExtractedValue( price: (float) $price, inStock: $inStock, rawText: (string) $price, method: 'ld_json', ); } } return null; } private function extractMeta(string $html, WatchTarget $target): ?ExtractedValue { // Open Graph / meta теги: <meta property="product:price:amount" content="29990"> $crawler = new Crawler($html); $selector = "meta[property='{$target->price_attr}'], meta[name='{$target->price_attr}']"; try { $content = $crawler->filter($selector)->attr('content'); return $this->parseNumeric($content); } catch (\Exception $e) { return null; } } private function extractText(string $html, WatchTarget $target): ?ExtractedValue { if (!$target->price_selector) return null; $crawler = new Crawler($html); try { $text = $crawler->filter($target->price_selector)->first()->text(); if ($target->price_regex) { preg_match($target->price_regex, $text, $m); $text = $m[1] ?? $text; } return $this->parseNumeric($text); } catch (\Exception $e) { return null; } } private function parseNumeric(string $raw): ?ExtractedValue { $clean = preg_replace('/[^\d.,]/', '', $raw); $clean = str_replace(',', '.', $clean); // "29.990" (разделитель тысяч точкой) → "29990" if (preg_match('/^\d{1,3}\.\d{3}$/', $clean)) { $clean = str_replace('.', '', $clean); } if (!is_numeric($clean) || (float) $clean <= 0) return null; return new ExtractedValue(price: (float) $clean, rawText: $raw); } } דרישות Playwright לאתרים דינמיים
עבור אתרים הטוענים תוכן דינמית באמצעות JavaScript (SPA, React, Vue), בקשת HTTP רגילה לא תניב נתונים נכונים. Playwright מדמה דפדפן מלא, מריץ קוד JS וממתין לרנדור. זה מגדיל את זמן הבדיקה אך מבטיח מחירים ומלאי מדויקים. במערכת שלנו, מתאם Playwright מחובר כאופציה עבור כתובות URL נבחרות.
מנגנון בדיקה: Job ו-Dispatcher
class CheckWatchTargetJob implements ShouldQueue { public int $timeout = 30; public int $tries = 2; public function handle(FlexiblePriceExtractor $extractor, WatchAlertService $alerts): void { $target = WatchTarget::findOrFail($this->targetId); // Fetch $response = $this->fetch($target->url); if (!$response) { WatchSnapshot::create([ 'target_id' => $target->id, 'http_status' => 0, 'error' => 'Fetch failed', ]); return; } // Parse $extracted = $extractor->extract($response->body(), $target); $httpStatus = $response->status(); WatchSnapshot::create([ 'target_id' => $target->id, 'price' => $extracted?->price, 'in_stock' => $extracted?->inStock, 'raw_price_text' => $extracted?->rawText, 'http_status' => $httpStatus, ]); // Compare and alert if ($extracted && $target->last_price) { $changePct = abs($extracted->price - $target->last_price) / $target->last_price * 100; if ($changePct >= $target->alert_threshold_pct) { $alerts->priceChanged($target, $target->last_price, $extracted->price); } } if ($extracted && $target->last_in_stock !== null && $extracted->inStock !== $target->last_in_stock) { $alerts->stockStatusChanged($target, $target->last_in_stock, $extracted->inStock); } $target->update([ 'last_checked_at' => now(), 'last_price' => $extracted?->price ?? $target->last_price, 'last_in_stock' => $extracted?->inStock ?? $target->last_in_stock, ]); } } עבודת cron רצה כל דקה, משיקה dispatcher שבוחר יעדים שבהם URL List → Scheduler → Fetcher → Parser → Comparator → Alert Engine ↓ Snapshot Store ושולח CREATE TABLE watch_targets ( id BIGSERIAL PRIMARY KEY, url TEXT NOT NULL UNIQUE, label VARCHAR(255), our_product_id BIGINT REFERENCES products(id), site_id INT REFERENCES external_sites(id), check_interval INTERVAL DEFAULT '4 hours', price_selector VARCHAR(500), stock_selector VARCHAR(500), price_type VARCHAR(20) DEFAULT 'text', price_attr VARCHAR(100), price_regex VARCHAR(255), alert_threshold_pct NUMERIC(5,2) DEFAULT 5.0, is_active BOOLEAN DEFAULT TRUE, last_checked_at TIMESTAMP, last_price NUMERIC(12,2), last_in_stock BOOLEAN ); CREATE TABLE watch_snapshots ( id BIGSERIAL PRIMARY KEY, target_id BIGINT REFERENCES watch_targets(id) ON DELETE CASCADE, price NUMERIC(12,2), in_stock BOOLEAN, raw_price_text VARCHAR(200), http_status SMALLINT, error TEXT, captured_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_snapshots_target_time ON watch_snapshots(target_id, captured_at DESC); לתור class FlexiblePriceExtractor { public function extract(string $html, WatchTarget $target): ?ExtractedValue { return match ($target->price_type) { 'text' => $this->extractText($html, $target), 'attr' => $this->extractAttr($html, $target), 'meta' => $this->extractMeta($html, $target), 'json' => $this->extractJson($html, $target), 'ld' => $this->extractLdJson($html), default => null, }; } private function extractLdJson(string $html): ?ExtractedValue { // <cite>Schema.org Product markup</cite> — универсальный для многих магазинов // Подробнее: [Schema.org](https://ru.wikipedia.org/wiki/Schema.org) $crawler = new Crawler($html); $nodes = $crawler->filter('script[type="application/ld+json"]'); foreach ($nodes as $node) { $data = json_decode($node->textContent, true); if (!$data) continue; $type = $data['@type'] ?? $data[0]['@type'] ?? null; if (!in_array($type, ['Product', 'Offer'])) continue; $offer = $data['offers'] ?? $data; if (is_array($offer) && isset($offer[0])) $offer = $offer[0]; $price = $offer['price'] ?? null; $inStock = ($offer['availability'] ?? '') === 'https://schema.org/InStock'; if ($price !== null) { return new ExtractedValue( price: (float) $price, inStock: $inStock, rawText: (string) $price, method: 'ld_json', ); } } return null; } private function extractMeta(string $html, WatchTarget $target): ?ExtractedValue { // Open Graph / meta теги: <meta property="product:price:amount" content="29990"> $crawler = new Crawler($html); $selector = "meta[property='{$target->price_attr}'], meta[name='{$target->price_attr}']"; try { $content = $crawler->filter($selector)->attr('content'); return $this->parseNumeric($content); } catch (\Exception $e) { return null; } } private function extractText(string $html, WatchTarget $target): ?ExtractedValue { if (!$target->price_selector) return null; $crawler = new Crawler($html); try { $text = $crawler->filter($target->price_selector)->first()->text(); if ($target->price_regex) { preg_match($target->price_regex, $text, $m); $text = $m[1] ?? $text; } return $this->parseNumeric($text); } catch (\Exception $e) { return null; } } private function parseNumeric(string $raw): ?ExtractedValue { $clean = preg_replace('/[^\d.,]/', '', $raw); $clean = str_replace(',', '.', $clean); // "29.990" (разделитель тысяч точкой) → "29990" if (preg_match('/^\d{1,3}\.\d{3}$/', $clean)) { $clean = str_replace('.', '', $clean); } if (!is_numeric($clean) || (float) $clean <= 0) return null; return new ExtractedValue(price: (float) $clean, rawText: $raw); } } . זה מבטיח עומס מאוזן ועמידה במרווחים. ה-dispatcher המבוזר שלנו מבטיח עומס מאוזן על פני תשתית השרתים. מנוע ההתראות המונע באירועים אסינכרוניים שולח התראות תוך שניות.
ניהול והתראות
בפאנל הניהול: רשימת כתובות URL עם מחיר נוכחי וזמן בדיקה אחרון, כפתור "בדוק עכשיו", גרף שינויי מחירים ל-30 יום, הגדרות סף התראה לכל כתובת URL, והוספת כתובות URL בכמות גדולה מקובץ CSV.
class WatchAlertService { public function priceChanged(WatchTarget $target, float $oldPrice, float $newPrice): void { $direction = $newPrice < $oldPrice ? '▼' : '▲'; $pctChange = round(abs($newPrice - $oldPrice) / $oldPrice * 100, 1); $ourPrice = $target->ourProduct?->price; $text = "{$direction} *Изменение цены* на {$target->site->name}\n" . "{$target->label}\n" . "Было: " . number_format($oldPrice, 0, '.', ' ') . " руб.\n" . "Стало: " . number_format($newPrice, 0, '.', ' ') . " руб. ({$pctChange}%)\n"; if ($ourPrice) { $diff = round(($newPrice - $ourPrice) / $ourPrice * 100, 1); $text .= "Наша цена: " . number_format($ourPrice, 0, '.', ' ') . " руб. " . ($diff > 0 ? "(мы дешевле на {$diff}%)" : "(они дешевле на " . abs($diff) . "%)") . "\n"; } $text .= "\n[Открыть страницу]({$target->url})"; $this->telegram->sendMessage([ 'chat_id' => config('telegram.price_watch_chat'), 'text' => $text, 'parse_mode' => 'Markdown', ]); } } כיצד להגדיר ניטור ב-5 שלבים?
לחץ להרחבת 5 השלבים
- הכן רשימת כתובות URL למוצרים למעקב.
- הגדר בוררי מחירים ומלאי לכל אתר.
- הגדר מרווחי בדיקה (מ-10 דקות עד 24 שעות).
- ציין ערוצי התראות (Telegram, Slack, דוא"ל).
- השק את המערכת ועקוב דרך לוח המחוונים.
לוח זמנים ליישום ודיוק
| שלב | זמן |
|---|---|
| סכמת נתונים + FlexiblePriceExtractor + LD-JSON | 1–2 ימים |
| CheckWatchTargetJob + dispatcher | 0.5 יום |
| התראות Telegram | 0.5 יום |
| ממשק ניהול + גרפים | יום אחד |
| מתאם Playwright לאתרי JS (אם נדרש) | +יום אחד |
| סה"כ | 3–4 ימי עבודה |
השוואה לבדיקה ידנית: ניטור אוטומטי טוב פי 100 במהירות ופי 10 בדיוק. עבור 1000 SKU, בדיקה ידנית אורכת 40 שעות בשבוע; אוטומטית אורכת 10 דקות. שיעור שגיאות נתונים הוא פחות מ-0.5%.
עבור אתרים מבוססי JavaScript, אנו משתמשים ב-Playwright — הוא מדמה דפדפן וממתין לרנדור. זה מאט את הבדיקה אך מבטיח דיוק. עבור SPA ודפים דינמיים, זה רכיב חובה.
תוצרי יישום סוהר
- עיצוב סכמת נתונים ומפענח לאתרים שלך.
- פריסה על השרת שלך או בענן.
- הגדרת התראות (Telegram, Slack, דוא"ל).
- אינטגרציה עם ה-CRM או ה-ERP שלך דרך API.
- תיעוד והדרכה לצוות שלך.
- אחריות תמיכה ל-3 חודשים לאחר ההשקה.
הערך את החיסכון עבור הנפח שלך — צור קשר לחישוב. קבל ייעוץ לפרויקט שלך — נחשב את העלות וזמן הביצוע תוך יום אחד.







