Менеджери витрачають до 3 годин на день на копіювання даних із кабінетів постачальників. При ручному введенні помилки сягають 15%, а застарілі ціни ведуть до прямих збитків. Результат — втрачені угоди та незадоволені клієнти. Ми автоматизуємо збір товарів, цін та залишків без участі людини. За час роботи на ринку реалізували 30+ проєктів, скоротивши час збору даних для клієнтів на 80%. Наприклад, інтернет-магазин з асортиментом 5000 товарів щомісяця витрачав 40 годин на оновлення цін; після впровадження парсера процес займає 4 години автономно. Економія бюджету — до 30%, що в грошовому еквіваленті може становити, наприклад, 240 000 гривень на рік.
Згідно з визначенням, веб-скрапінг — це автоматичний збір даних з інтернету. Нижче — як це працює на практиці.
Чому ручний збір даних — це дорого?
80% часу йде на монотонні операції. Помилки в 15% записів через людський фактор. Втрата клієнтів через неактуальні ціни або відсутність товару на складі. Автоматизація знижує витрати на 30% і повністю виключає помилки введення.
Які дані ми збираємо
| Тип даних | Приклад | Джерело |
|---|---|---|
| Основні поля | Назва, ціна, SKU | Сторінка списку товарів |
| Розширені | Опис, характеристики, зображення | Картка товару |
| Динамічні | Залишки, статус наявності | AJAX-запити або DOM |
Як працює парсер і як ми обходимо захист?
Архітектура типового рішення:
- Планувальник (cron / Laravel Horizon) запускає завдання за розкладом.
- HTTP-клієнт (Guzzle / Playwright) завантажує сторінки постачальника.
- Парсер витягує дані через CSS-селектори або XPath.
- Нормалізатор приводить ціни, дати та валюти до єдиного формату.
- Дедуплікатор перевіряє товар за SKU та оновлює або створює запис.
- Сповіщення при помилках або значних змінах цін.
Сучасні постачальники використовують Cloudflare, капчі та динамічне завантаження. Ми застосовуємо ротацію резидентських проксі, емуляцію браузера Playwright/Puppeteer з випадковими рухами миші, затримки від 500 мс до 2 с і зміну User-Agent. Якщо сайт використовує JavaScript, Playwright завантажує сторінку як справжній браузер і повертає готовий DOM.
Що краще: Guzzle чи Playwright?
| Критерій | Guzzle (PHP) | Playwright (Node) |
|---|---|---|
| Швидкість | Висока (немає браузера) | Середня (запуск браузера) |
| Робота з JS | Не підтримує | Повна емуляція |
| Ресурси | Мінімальні | Вимагає більше ОЗУ |
| Складність парсингу | Достатньо для статики | Необхідний для 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)]; } } Job для фонової обробки
// 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-процес через proc_open або 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 | Смартфон XYZ |
| price | float | 19999.99 |
| in_stock | bool | true |
Процес розробки та терміни
- Аналітика (1-2 дні) — вивчаємо структуру сайту постачальника, визначаємо типи захисту, узгоджуємо поля.
- Проєктування (1 день) — обираємо стек, проєктуємо архітектуру (черги, розклад, схема даних).
- Реалізація (3-7 днів) — пишемо парсери, нормалізатори, дедуплікацію, інтеграцію з вашою системою.
- Тестування (1-2 дні) — прогоняємо на реальних даних, перевіряємо коректність та стійкість.
- Деплой (1 день) — налаштовуємо оточення, моніторинг, сповіщення, передаємо документацію.
Терміни орієнтовно
- Базовий парсер одного постачальника (статичний HTML, 5-10 полів): від 3 до 5 робочих днів.
- Парсер зі складним захистом або JS-рендерингом: від 7 до 14 днів.
- Інтеграція з кількома постачальниками в єдиний pipeline: від 10 робочих днів.
Вартість розраховується індивідуально після аудиту цільових сайтів.
Що входить у результат
- Парсер з відкритим вихідним кодом (PHP + Laravel або Node.js).
- Інтеграція з вашою БД через API або прямий імпорт.
- Налаштування розкладу та моніторингу.
- Документація з запуску та підтримки.
- Гарантія стабільної роботи 30 днів.
Типові помилки при розробці парсерів
- Ігнорування rate limit — сайт блокує IP після 100 запитів. Рішення: додавати випадкові затримки та ротацію.
- Жорстка прив'язка до структури HTML — найменша зміна дизайну ламає парсер. Рішення: використовувати data-атрибути або XPath.
- Відсутність обробки помилок — при збої одного постачальника падає вся черга. Рішення: ізолювати завдання та додати повторні спроби.
- Збір усіх даних в одну таблицю без нормалізації — дублі та пробіли. Рішення: використовувати SKU як унікальний ключ.
Зв'яжіться з нами для консультації — оцінимо ваш проєкт і запропонуємо рішення під ключ. Замовте розробку парсера та отримайте готове рішення, яке заощадить ваш час і гроші.







