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







