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







