בוט לניתוח מוצרי ספק: איסוף נתונים אוטומטי

איסוף ידני של נתוני מוצרי ספקים הוא תהליך גוזל זמן ומועד לשגיאות, המוביל למחירים מיושנים ולפגיעה במוניטין. אנו בונים בוטים לפרסור (parsing) שאוספים אוטומטית קטלוגים, מחירים ומלאי מאתרי ספקים. הצוות שלנו מספק פרויקטים סוהריים—מבדיקת מקור ועד השקה ותמיכה שוטפת—ומבטיח פתרון אמין שגדל עם העסק שלכם.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
בוט לניתוח מוצרי ספק: איסוף נתונים אוטומטי
בינוני
~5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1365
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

מנהלים מבזבזים עד 3 שעות ביום בהעתקת נתונים מלוחות מחוונים של ספקים. הזנה ידנית מובילה לשיעור שגיאות של 15%, ומחירים מיושנים גורמים להפסדים ישירים. התוצאה: עסקאות אבודות ולקוחות לא מרוצים. אנו הופכים את איסוף המוצרים, המחירים והמלאי לאוטומטי ללא התערבות אנושית. במשך שנות פעילותנו בשוק, סיפקנו למעלה מ-30 פרויקטים, והפחתנו את זמן איסוף הנתונים ללקוחות ב-80%. לדוגמה, חנות מקוונת עם 5,000 פריטים (SKU) השקיעה 40 שעות חודשיות בעדכון מחירים; לאחר הטמעת הפרוסר, התהליך אורך 4 שעות באופן אוטונומי. חיסכון תקציבי של עד 30%, שבמונחים כספיים יכול להסתכם, למשל, ב-$2.2k–3.1k לשנה.

לפי ההגדרה, גירוד אתרים הוא חילוץ אוטומטי של נתונים מהאינטרנט. כך זה עובד בפועל.

מדוע איסוף נתונים ידני הוא יקר?

80% מהזמן מושקע במשימות מונוטוניות. 15% מהרשומות מכילות שגיאות עקב גורם אנושי. לקוחות אובדים עקב מחירים מיושנים או מוצרים חסרי מלאי. אוטומציה מפחיתה עלויות ב-30% ומבטלת לחלוטין שגיאות הזנה.

אילו נתונים אנו אוספים

סוג נתונים דוגמה מקור
שדות בסיסיים שם, מחיר, SKU דף רשימת מוצרים
מורחבים תיאור, מפרטים, תמונות דף פרטי מוצר
דינמיים מלאי, מצב זמינות בקשות AJAX או DOM

כיצד הפרוסר עובד וכיצד אנו עוקפים הגנות?

ארכיטקטורת פתרון טיפוסית:

  1. מתזמן (cron / Laravel Horizon) מריץ משימות לפי לוח זמנים.
  2. לקוח HTTP (Guzzle / Playwright) מביא דפי ספקים.
  3. פרוסר מחלץ נתונים באמצעות בוררי CSS או XPath.
  4. מנרמל מביא מחירים, תאריכים ומטבעות לפורמט אחיד.
  5. מסיר כפילויות בודק מוצרים לפי SKU ומעדכן או יוצר רשומות.
  6. התראות על שגיאות או שינויי מחירים משמעותיים.

ספקים מודרניים משתמשים ב-Cloudflare, CAPTCHA וטעינה דינמית. אנו מיישמים סיבוב פרוקסי מגורים, אמולציית דפדפן Playwright/Puppeteer עם תנועות עכבר אקראיות, השהיות של 500 אלפיות השנייה עד 2 שניות, והחלפת User-Agent. אם האתר משתמש ב-JavaScript, Playwright טוען את הדף כמו דפדפן אמיתי ומחזיר את ה-DOM המוכן.

מה עדיף: Guzzle או Playwright?

קריטריון Guzzle (PHP) Playwright (Node)
מהירות גבוהה (ללא דפדפן) בינונית (הפעלת דפדפן)
טיפול ב-JS לא נתמך אמולציה מלאה
משאבים מינימליים דורש יותר זיכרון RAM
מורכבות גירוד מספיק לאתרים סטטיים נדרש לאתרי SPA

Guzzle מעבד דפים סטטיים פי 2 מהר יותר מ-Playwright. גישה משולבת — Guzzle לסטטי, Playwright ל-SPA — מניבה את התוצאה הטובה ביותר.

יישום טכני

פרוסר בסיסי ב-PHP

// app/Services/Scrapers/SupplierScraper.php
use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;

class SupplierScraper
{
    private Client $client;

    public function __construct(
        private string $baseUrl,
        private array $proxyPool = []
    ) {
        $this->client = new Client([
            'timeout' => 15,
            'connect_timeout' => 5,
            'headers' => [
                'User-Agent' => $this->randomUserAgent(),
                'Accept-Language' => 'ru-RU,ru;q=0.9',
                'Accept' => 'text/html,application/xhtml+xml',
            ],
        ]);
    }

    public function scrapeProductList(string $categoryUrl): array
    {
        $html = $this->fetchWithRetry($categoryUrl);
        $crawler = new Crawler($html);
        return $crawler->filter('.product-card')->each(function (Crawler $node) {
            return [
                'url' => $node->filter('a.product-link')->attr('href'),
                'title' => trim($node->filter('.product-title')->text()),
                'price' => $this->parsePrice($node->filter('.price')->text()),
                'sku' => $node->filter('[data-sku]')->attr('data-sku'),
            ];
        });
    }

    public function scrapeProductDetail(string $productUrl): array
    {
        $html = $this->fetchWithRetry($this->baseUrl . $productUrl);
        $crawler = new Crawler($html);
        return [
            'title' => $crawler->filter('h1.product-name')->text(),
            'description' => $crawler->filter('.description')->html(),
            'price' => $this->parsePrice($crawler->filter('.current-price')->text()),
            'images' => $crawler->filter('.gallery img')->each(
                fn(Crawler $img) => $img->attr('src')
            ),
            'specs' => $this->extractSpecs($crawler),
            'in_stock' => $crawler->filter('.in-stock')->count() > 0,
            'sku' => $crawler->filter('[itemprop="sku"]')->text(''),
        ];
    }

    private function extractSpecs(Crawler $crawler): array
    {
        $specs = [];
        $crawler->filter('.specs-table tr')->each(function (Crawler $row) use (&$specs) {
            $key = trim($row->filter('td:first-child')->text(''));
            $val = trim($row->filter('td:last-child')->text(''));
            if ($key && $val) {
                $specs[$key] = $val;
            }
        });
        return $specs;
    }

    private function fetchWithRetry(string $url, int $attempts = 3): string
    {
        $proxy = $this->proxyPool ? $this->randomProxy() : null;
        for ($i = 0; $i < $attempts; $i++) {
            try {
                $options = $proxy ? ['proxy' => $proxy] : [];
                $response = $this->client->get($url, $options);
                return (string) $response->getBody();
            } catch (\Exception $e) {
                if ($i === $attempts - 1) throw $e;
                sleep(rand(2, 5));
            }
        }
    }

    private function parsePrice(string $text): float
    {
        return (float) preg_replace('/[^\d.,]/', '', str_replace(',', '.', $text));
    }

    private function randomUserAgent(): string
    {
        $agents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
        ];
        return $agents[array_rand($agents)];
    }
}

משימה לעיבוד רקע

// app/Jobs/ScrapeSupplierProducts.php
class ScrapeSupplierProducts implements ShouldQueue
{
    use Queueable;

    public int $tries = 2;
    public int $timeout = 300;

    public function __construct(
        private int $supplierId,
        private string $categoryUrl
    ) {}

    public function handle(SupplierScraper $scraper, ProductImportService $importer): void
    {
        $products = $scraper->scrapeProductList($this->categoryUrl);

        foreach ($products as $productPreview) {
            ScrapeSupplierProductDetail::dispatch(
                $this->supplierId,
                $productPreview['url']
            )->onQueue('scraper-detail');

            usleep(rand(500000, 1500000));
        }
    }
}

Playwright לאתרים כבדי JS

// scraper/playwright-worker.js
const { chromium } = require('playwright');

async function scrapeProduct(url) {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
    viewport: { width: 1366, height: 768 },
  });
  const page = await context.newPage();
  await page.goto(url, { waitUntil: 'networkidle' });
  const product = await page.evaluate(() => ({
    title: document.querySelector('h1')?.textContent,
    price: document.querySelector('.price')?.textContent,
    images: [...document.querySelectorAll('.gallery img')].map(i => i.src),
  }));
  await browser.close();
  return product;
}

PHP קורא לתהליך Node באמצעות // app/Services/Scrapers/SupplierScraper.php use GuzzleHttp\Client; use Symfony\Component\DomCrawler\Crawler; class SupplierScraper { private Client $client; public function __construct( private string $baseUrl, private array $proxyPool = [] ) { $this->client = new Client([ 'timeout' => 15, 'connect_timeout' => 5, 'headers' => [ 'User-Agent' => $this->randomUserAgent(), 'Accept-Language' => 'ru-RU,ru;q=0.9', 'Accept' => 'text/html,application/xhtml+xml', ], ]); } public function scrapeProductList(string $categoryUrl): array { $html = $this->fetchWithRetry($categoryUrl); $crawler = new Crawler($html); return $crawler->filter('.product-card')->each(function (Crawler $node) { return [ 'url' => $node->filter('a.product-link')->attr('href'), 'title' => trim($node->filter('.product-title')->text()), 'price' => $this->parsePrice($node->filter('.price')->text()), 'sku' => $node->filter('[data-sku]')->attr('data-sku'), ]; }); } public function scrapeProductDetail(string $productUrl): array { $html = $this->fetchWithRetry($this->baseUrl . $productUrl); $crawler = new Crawler($html); return [ 'title' => $crawler->filter('h1.product-name')->text(), 'description' => $crawler->filter('.description')->html(), 'price' => $this->parsePrice($crawler->filter('.current-price')->text()), 'images' => $crawler->filter('.gallery img')->each( fn(Crawler $img) => $img->attr('src') ), 'specs' => $this->extractSpecs($crawler), 'in_stock' => $crawler->filter('.in-stock')->count() > 0, 'sku' => $crawler->filter('[itemprop="sku"]')->text(''), ]; } private function extractSpecs(Crawler $crawler): array { $specs = []; $crawler->filter('.specs-table tr')->each(function (Crawler $row) use (&$specs) { $key = trim($row->filter('td:first-child')->text('')); $val = trim($row->filter('td:last-child')->text('')); if ($key && $val) { $specs[$key] = $val; } }); return $specs; } private function fetchWithRetry(string $url, int $attempts = 3): string { $proxy = $this->proxyPool ? $this->randomProxy() : null; for ($i = 0; $i < $attempts; $i++) { try { $options = $proxy ? ['proxy' => $proxy] : []; $response = $this->client->get($url, $options); return (string) $response->getBody(); } catch (\Exception $e) { if ($i === $attempts - 1) throw $e; sleep(rand(2, 5)); } } } private function parsePrice(string $text): float { return (float) preg_replace('/[^\d.,]/', '', str_replace(',', '.', $text)); } private function randomUserAgent(): string { $agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15', ]; return $agents[array_rand($agents)]; } } או מיקרוסרוויס HTTP.

הסרת כפילויות ועדכון

// app/Services/ProductImportService.php
class ProductImportService {
    public function upsert(int $supplierId, array $data): void {
        $product = SupplierProduct::updateOrCreate(
            [
                'supplier_id' => $supplierId,
                'supplier_sku' => $data['sku'],
            ],
            [
                'title' => $data['title'],
                'price' => $data['price'],
                'in_stock' => $data['in_stock'],
                'description' => $data['description'],
                'images' => json_encode($data['images']),
                'specs' => json_encode($data['specs']),
                'scraped_at' => now(),
            ]
        );

        if ($product->wasChanged('price')) {
            $change = abs($product->price - $product->getOriginal('price'));
            if ($change / $product->getOriginal('price') > 0.05) {
                PriceChangedNotification::dispatch($product);
            }
        }
    }
}

הגדרת לוח זמנים

// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
    $schedule->command('scraper:supplier --supplier=1')
        ->dailyAt('03:00')
        ->withoutOverlapping();

    $schedule->command('scraper:supplier --supplier=1 --prices-only')
        ->everyFourHours()
        ->withoutOverlapping();
}
דוגמה למבנה נתונים לגירוד
שדה סוג דוגמה
sku string ART-12345
title string Smartphone XYZ
price float 19999.99
in_stock bool true

תהליך הפיתוח ולוחות זמנים

  1. ניתוח (1-2 ימים) — חקר מבנה אתר הספק, זיהוי סוגי הגנה, הסכמה על שדות.
  2. עיצוב (יום אחד) — בחירת מחסנית טכנולוגית, תכנון ארכיטקטורה (תורים, לוח זמנים, סכמת נתונים).
  3. יישום (3-7 ימים) — כתיבת פרוסרים, מנרמלים, הסרת כפילויות, אינטגרציה עם המערכת שלך.
  4. בדיקות (1-2 ימים) — הרצה על נתונים אמיתיים, אימות נכונות ועמידות.
  5. פריסה (יום אחד) — הגדרת סביבה, ניטור, התראות, מסירת תיעוד.

לוחות זמנים משוערים

  • פרוסר בסיסי לספק אחד (HTML סטטי, 5-10 שדות): בין 3 ל-5 ימי עסקים.
  • פרוסר עם הגנה מורכבת או רינדור JS: בין 7 ל-14 ימים.
  • אינטגרציה עם מספר ספקים לצינור נתונים אחד: החל מ-10 ימי עסקים.

העלות מחושבת באופן אישי לאחר ביקורת על אתרי היעד.

מה כלול בתוצאה

  • פרוסר עם קוד פתוח (PHP + Laravel או Node.js).
  • אינטגרציה עם מסד הנתונים שלך באמצעות API או ייבוא ישיר.
  • הגדרת לוח זמנים וניטור.
  • תיעוד להשקה ותחזוקה.
  • אחריות ל-30 יום על פעילות יציבה.

טעויות נפוצות בפיתוח פרוסרים

  • התעלמות ממגבלת קצב — האתר חוסם את כתובת ה-IP שלך לאחר 100 בקשות. פתרון: הוספת השהיות אקראיות וסיבוב.
  • קידוד קשיח של מבנה HTML — שינוי עיצובי קטן שובר את הפרוסר. פתרון: שימוש ב-data-attributes או XPath.
  • חוסר טיפול בשגיאות — כשל ספק אחד מפיל את כל התור. פתרון: בידוד משימות והוספת ניסיונות חוזרים.
  • אחסון כל הנתונים בטבלה אחת ללא נורמליזציה — כפילויות ופערים. פתרון: שימוש ב-SKU כמפתח ייחודי.

צרו קשר לייעוץ — נעריך את הפרויקט שלכם ונציע פתרון סוהר. הזמינו פיתוח פרוסר וקבלו פתרון מוכן שחוסך לכם זמן וכסף.