Менеджери витрачають до 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 як унікальний ключ.
Зв'яжіться з нами для консультації — оцінимо ваш проєкт і запропонуємо рішення під ключ. Замовте розробку парсера та отримайте готове рішення, яке заощадить ваш час і гроші.







