אנו מפתחים מערכות ניטור מחירים של מתחרים שפותרות משימה ספציפית: לדעת מתי ובכמה מתחרה שינה מחיר, לפני שהלקוחות שמים לב. ללא אוטומציה של ניטור, אתה מסתכן בהחמצת ירידת מחיר על מוצר מפתח. בדיקה ידנית של עשרות אלפי פריטים אינה מציאותית, ובדיקה חד-פעמית נותנת רק תמונת מצב. במהלך עבודתנו, השקנו יותר מ-30 פרויקטים כאלה למסחר אלקטרוני—מחנויות קטנות ועד שווקים גדולים. הניסיון שלנו מבטיח פעולה יציבה של ה-scraper גם כאשר מבנה האתר משתנה. אם אתה צריך כלי אמין, צור קשר—נעריך את הפרויקט שלך ונציע את הפתרון האופטימלי.
ארכיטקטורת מערכת הניטור
ההבדל המרכזי מ-scraper חד-פעמי הוא תעדוף מוצרים. פריטים פופולריים צריכים להיבדק כל שעה, והזנב הארוך של הקטלוג פעם ביום. זה מפחית עומס על המקור ומאיץ תגובה לשינויים חשובים. תור המשימות מחולק לפי עדיפות, מה שמאפשר עיבוד של עד 10,000 מוצרים בדקה ללא עומס יתר.
[Scheduler] ├── High priority queue (топ-товары, каждый час) └── Low priority queue (остальные, раз в сутки) ↓ [Fetcher] → [Parser] → [Change Detector] → [Alert Engine] ↓ [price_history table] מנגנון זיהוי השינויים משווה את המחיר החדש לרשומה האחרונה בהיסטוריה. אם השתנה—רשום ב-[Scheduler] ├── High priority queue (топ-товары, каждый час) └── Low priority queue (остальные, раз в сутки) ↓ [Fetcher] → [Parser] → [Change Detector] → [Alert Engine] ↓ [price_history table] ואירוע בתור ההתראות. אם לא השתנה—עדכן רק את price_history כדי למנוע נפח היסטוריה מיותר. גישה זו מאפשרת אחסון של עד 5 שנים של היסטוריה עם נפח נתונים מינימלי.
למה JSON-LD הוא המקור הטוב ביותר לניתוח מחירים?
מחירים באתרים מוצגים בדרכים שונות. ב-HTML—סלקטור CSS last_checked_at או תכונה .product-price. ב-Schema.org—אמין, לא נשבר בעיצוב מחדש. דרך XHR API—יירוט בקשות רשת עם Playwright. דינמית דרך JS לאחר טעינה—דורש דפדפן ללא ראש. JSON-LD הוא המקור היציב ביותר: חנויות רבות עם אופטימיזציה למנועי חיפוש מוסיפות מיקרודאטה עבור בוטים של חיפוש. שיעור השגיאות בניתוח דרך HTML יכול להגיע ל-10%, בעוד דרך JSON-LD הוא פחות מ-0.5%.
import * as cheerio from 'cheerio'; interface PriceData { price: number; priceSale?: number; currency: string; inStock: boolean; } function extractPriceFromJsonLd(html: string): PriceData | null { const $ = cheerio.load(html); for (const scriptEl of $('script[type="application/ld+json"]').toArray()) { try { const data = JSON.parse($(scriptEl).html() ?? '{}'); const product = data['@type'] === 'Product' ? data : (Array.isArray(data['@graph']) ? data['@graph'].find((n: { '@type': string }) => n['@type'] === 'Product') : null); if (product?.offers) { const offer = Array.isArray(product.offers) ? product.offers[0] : product.offers; return { price: parseFloat(offer.price), currency: offer.priceCurrency ?? 'USD', inStock: offer.availability?.includes('InStock') ?? true, }; } } catch { continue; } } return null; } טיפול בפורמטי מחירים בטקסט: data-price, import * as cheerio from 'cheerio'; interface PriceData { price: number; priceSale?: number; currency: string; inStock: boolean; } function extractPriceFromJsonLd(html: string): PriceData | null { const $ = cheerio.load(html); for (const scriptEl of $('script[type="application/ld+json"]').toArray()) { try { const data = JSON.parse($(scriptEl).html() ?? '{}'); const product = data['@type'] === 'Product' ? data : (Array.isArray(data['@graph']) ? data['@graph'].find((n: { '@type': string }) => n['@type'] === 'Product') : null); if (product?.offers) { const offer = Array.isArray(product.offers) ? product.offers[0] : product.offers; return { price: parseFloat(offer.price), currency: offer.priceCurrency ?? 'USD', inStock: offer.availability?.includes('InStock') ?? true, }; } } catch { continue; } } return null; } , "$12–17"—נורמליזציה באמצעות regex. אחסון כ-"$12.99" עם שדה "€ 9,90" נפרד. מעקב אחר שלוש רמות: מחיר ללא הנחה (DECIMAL(10,2)), מחיר עם הנחה (currency), מחיר כרטיס נאמנות (לעיתים מחיר שלישי נסתר).
| מקור | יציבות | מהירות | מורכבות |
|---|---|---|---|
| HTML | בינונית | גבוהה | נמוכה |
| JSON-LD | גבוהה | גבוהה | נמוכה |
| XHR API | בינונית | בינונית | בינונית |
| דפדפן ללא ראש | נמוכה | נמוכה | גבוהה |
מנגנון זיהוי השינויים
CREATE TABLE monitored_products ( id SERIAL PRIMARY KEY, source VARCHAR(100) NOT NULL, external_id VARCHAR(255) NOT NULL, title TEXT, url TEXT NOT NULL, priority SMALLINT DEFAULT 5, -- 1=высший, 10=низший check_interval INT DEFAULT 360, -- минуты last_checked_at TIMESTAMPTZ, UNIQUE(source, external_id) ); CREATE TABLE price_history ( id BIGSERIAL PRIMARY KEY, product_id INT REFERENCES monitored_products(id), price DECIMAL(10,2), price_original DECIMAL(10,2), in_stock BOOLEAN, currency VARCHAR(3) DEFAULT 'USD', recorded_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX ON price_history(product_id, recorded_at DESC); -- Быстрый доступ к актуальной цене без JOIN с историей ALTER TABLE monitored_products ADD COLUMN current_price DECIMAL(10,2), ADD COLUMN current_in_stock BOOLEAN; async function processNewPrice( productId: number, newPrice: number, newInStock: boolean ): Promise<{ changed: boolean; delta?: number }> { const product = await db.monitoredProducts.findById(productId); const priceChanged = product.currentPrice !== newPrice; const stockChanged = product.currentInStock !== newInStock; if (!priceChanged && !stockChanged) { // Только обновить время проверки await db.monitoredProducts.update(productId, { lastCheckedAt: new Date() }); return { changed: false }; } // Записать в историю await db.priceHistory.create({ productId, price: newPrice, inStock: newInStock, recordedAt: new Date(), }); // Обновить текущие значения await db.monitoredProducts.update(productId, { currentPrice: newPrice, currentInStock: newInStock, lastCheckedAt: new Date(), }); const delta = product.currentPrice ? ((newPrice - product.currentPrice) / product.currentPrice) * 100 : 0; return { changed: true, delta }; } הגדרת התראות וספים
כללי טריגר הניתנים להגדרה:
- המחיר ירד ביותר מ-X% (לדוגמה, 5% או 10%)
- המחיר ירד מתחת למחיר שלך עבור מוצר דומה
- המוצר הופיע או נעלם מהמלאי
- המחיר השתנה עבור N+ מתחרים בו-זמנית (סימן לשינוי בשוק)
- המחיר הגיע למינימום היסטורי ב-90 הימים האחרונים
async function checkAlertRules(productId: number, delta: number): Promise<void> { const rules = await db.alertRules.findAll({ productId, active: true }); for (const rule of rules) { const triggered = (rule.type === 'price_drop_percent' && delta < -rule.threshold) || (rule.type === 'below_my_price' && await isPriceBelowMyPrice(productId)) || (rule.type === 'out_of_stock' && newInStock === false); if (triggered) { await sendAlert(rule, productId, delta); } } } מסירה: בוט טלגרם (מיידי דרך Bot API), סיכום דוא"ל (פעם ביום), webhook למערכת ניהול מחירים (לתגובה אוטומטית). ניתן לשנות ספים בזמן אמת דרך לוח הבקרה.
מה יש בלוח הבקרה האנליטי?
המסכים המינימליים הנדרשים:
טבלת ניטור — כל המוצרים במעקב עם מחיר המתחרה הנוכחי, המחיר שלך, אחוז ההפרש ומגמה (חץ למעלה/למטה).
גרף מחירים — מחיר המתחרה מול המחיר שלך לאורך תקופה נבחרת. Recharts price_original עם שני קווים וסמני שינוי.
הזנת התראות — 50 השינויים האחרונים עם סינון לפי מקור וסוג שינוי.
יישום מהיר של לוח בקרה — Metabase המחובר ל-PostgreSQL. ממשק React מותאם אישית עם Recharts נדרש אם לוח הבקרה משולב במערכת ניהול מבחר קיימת.
תהליך הפיתוח
- אנליטיקה — חקר אתרי היעד, קביעת עדיפויות מוצרים, הסכמה על התראות ולוח בקרה.
- עיצוב — תכנון ארכיטקטורת scraper, מסד נתונים, כללי התראות.
- יישום — כתיבת קוד, הגדרת תורים, אינטגרציות.
- בדיקות — הרצה על נתונים אמיתיים, בדיקת דיוק ויציבות.
- פריסה ותמיכה — פריסה על השרת שלך או בענן, מסירת תיעוד, הדרכה.
מה כלול
- קוד מקור של scraper עם תיעוד פתוח
- גישה ללוח בקרה ו-API
- מדריך הפעלה
- אחריות יציבות ל-3 חודשים
- תמיכה בשינויי מבנה אתר (עד 5 התאמות בחודש)
לוחות זמנים והיקפים
| היקף | מקורות | מוצרים | לוח זמנים |
|---|---|---|---|
| קטן | 1–3 | עד 10k | 5–8 ימים |
| בינוני | 3–10 | 10k–100k | 2–3 שבועות |
| גדול | 10+ | 100k+ | 4–6 שבועות |
עבור 100k+ מוצרים עם שנה של היסטוריה, ClickHouse במקום PostgreSQL לאחסון price_sale: שאילתות אנליטיות (צבירה לאורך תקופה, מציאת מינימום) עובדות בסדר גודל מהר יותר על נפחים גדולים. PostgreSQL נשאר עבור נתונים תפעוליים ותצורה.
דוגמת ארכיטקטורה לפרויקט גדול
עבור היקפים מעל 100k מוצרים, אנו משתמשים בתורי RabbitMQ מבוזרים ובמיקרוסרוויסים ב-Go לניתוח. זה מאפשר קנה מידה אופקי עד מיליון מוצרים עם זמן עדכון של לא יותר משעה.צור קשר לייעוץ—נעריך את הפרויקט שלך ונציע את הפתרון האופטימלי. קבל scraper מחירים אמין במפתח מלא עם אחריות יציבות.







