Разработка бота-парсера товаров с сайтов поставщиков

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка бота-парсера товаров с сайтов поставщиков
Средняя
~5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка бота-парсера товаров с сайтов поставщиков

Парсер сайтов поставщиков автоматизирует получение данных о товарах, ценах и остатках без ручного копирования. Результат — структурированные данные в вашей базе, обновляемые по расписанию.

Архитектура парсера

Scheduler (cron / Horizon)
    → Scraper Job
        → HTTP Client (Guzzle / curl)
        → HTML Parser (Symfony DomCrawler / Goutte)
        → Data Normalizer
        → Duplicate Checker
        → Product Repository
        → Notification (если ошибки)

Стек инструментов

Задача Инструмент
HTTP-запросы Guzzle 7
Парсинг HTML Symfony DomCrawler + CSS Selector
JS-сайты Puppeteer (Node) / Playwright
Очереди Laravel Queue + Redis
Прокси Rotating proxy pool
Хранение PostgreSQL / MySQL

Базовый парсер на 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; // 5 минут на категорию

    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)); // 0.5–1.5 сек
        }
    }
}

Обход защиты и капчи

Ротация прокси:

// config/scraper.php
return [
    'proxy_pool' => [
        'http://user:[email protected]:3128',
        'http://user:[email protected]:3128',
    ],
    'request_delay_ms' => [500, 2000], // min, max
    'rotate_user_agent' => true,
];

Playwright для JS-сайтов:

Если сайт требует выполнения JavaScript:

// 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();

    // Только цены и остатки — каждые 4 часа
    $schedule->command('scraper:supplier --supplier=1 --prices-only')
        ->everyFourHours()
        ->withoutOverlapping();
}

Срок разработки

Базовый парсер одного поставщика (статический HTML, 5-10 полей): 3-5 рабочих дней, включая настройку очередей, расписания и базового мониторинга.