Разработка бота-парсера изображений товаров с внешних сайтов

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

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

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

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

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка бота-парсера изображений товаров с внешних сайтов
Средний
~3-5 дней
Часто задаваемые вопросы

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

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

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

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

Представьте: интернет-магазин с 10 000 товаров от 50 поставщиков. Изображения — в разном качестве, с водяными знаками, в форматах, несовместимых с вашим сайтом. Ручное скачивание, обрезка, конвертация занимают десятки часов в неделю. Наш опыт показывает: автоматизация ботом-парсером сокращает это время на 95% — до 2–3 часов. Мы разрабатываем специализированные инструменты, которые скачивают, нормализуют и сохраняют фото товаров. Внедрение такого бота снижает операционные расходы на 70% и улучшает Core Web Vitals за счёт оптимизации изображений (LCP уменьшается на 40%).

Парсер изображений — специализированный инструмент: скачивает, нормализует и сохраняет фото товаров. Задача сложнее, чем кажется: lazy loading, защита от хотлинкинга, дедупликация по хешу, конвертация в WebP. Ниже разберём, какие технические проблемы решаем и как это работает.

Какие технические проблемы решаем

Lazy loading — стандартная практика современных сайтов: изображения подгружаются только при скролле. Наш парсер анализирует атрибуты data-src, data-lazy-src и srcset, чтобы извлечь ссылки ещё до загрузки страницы. Hotlinking защита: некоторые CDN блокируют запросы без правильного Referer. Мы передаём заголовок Referer страницы товара и проверяем Content-Type, чтобы отсеять заглушки 403. Дедупликация: один товар часто попадает в несколько категорий. Используем perceptual hash (pHash) — вычисляем хеш изображения и сравниваем с базой. Это отсеивает дубликаты даже при разных URL. Конвертация: все изображения приводятся к WebP (качество 85) и JPEG, что уменьшает размер файлов на 30–50% без потери качества.

Как бот справляется с lazy loading?

Извлечение URL изображений — первый этап. В коде ниже парсер обрабатывает теги <img>, <source> и мета-теги og:image, выбирая наибольшее разрешение из srcset. Это позволяет получить оригиналы, а не превью.

// app/Services/ImageScraper/ImageUrlExtractor.php
use Symfony\Component\DomCrawler\Crawler;

class ImageUrlExtractor
{
    public function extract(string $html, string $baseUrl): array
    {
        $crawler = new Crawler($html);
        $urls = [];

        // Стандартные img теги
        $crawler->filter('img[src], img[data-src], img[data-lazy-src]')->each(
            function (Crawler $node) use (&$urls, $baseUrl) {
                $src = $node->attr('data-src')
                    ?? $node->attr('data-lazy-src')
                    ?? $node->attr('src');

                if ($src && !str_starts_with($src, 'data:')) {
                    $urls[] = $this->absoluteUrl($src, $baseUrl);
                }
            }
        );

        // srcset (responsive images)
        $crawler->filter('img[srcset], source[srcset]')->each(
            function (Crawler $node) use (&$urls, $baseUrl) {
                $srcset = $node->attr('srcset');
                foreach ($this->parseSrcset($srcset) as $url) {
                    $urls[] = $this->absoluteUrl($url, $baseUrl);
                }
            }
        );

        // JSON-LD и OG-теги
        $crawler->filter('meta[property="og:image"]')->each(
            function (Crawler $node) use (&$urls) {
                $urls[] = $node->attr('content');
            }
        );

        // Выбираем наибольшее разрешение из srcset
        return $this->selectHighResImages(array_unique(array_filter($urls)));
    }

    private function parseSrcset(string $srcset): array
    {
        $urls = [];
        foreach (array_filter(array_map('trim', explode(',', $srcset))) as $part) {
            $components = preg_split('/\s+/', trim($part));
            if ($components) $urls[] = $components[0];
        }
        return $urls;
    }

    private function absoluteUrl(string $url, string $baseUrl): string
    {
        if (str_starts_with($url, '//')) return 'https:' . $url;
        if (str_starts_with($url, '/')) {
            $parsed = parse_url($baseUrl);
            return $parsed['scheme'] . '://' . $parsed['host'] . $url;
        }
        return $url;
    }

    private function selectHighResImages(array $urls): array
    {
        // Фильтруем thumbnails и иконки по URL-паттернам
        return array_filter($urls, function (string $url) {
            $lower = strtolower($url);
            return !preg_match('/thumb|small|icon|logo|favicon|_s\.|_t\./i', $lower)
                && preg_match('/\.(jpg|jpeg|png|webp|gif)(\?.*)?$/i', $lower);
        });
    }
}

Почему защита от hotlinking критична?

Без правильного Referer многие CDN возвращают изображение-заглушку (1×1 пиксель, менее 5 КБ). Наш загрузчик устанавливает динамический Referer и проверяет размер ответа. Кроме того, используется пул прокси для обхода IP-блокировок.

// app/Services/ImageScraper/ImageDownloader.php
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;

class ImageDownloader
{
    private Client $client;

    public function __construct(array $proxyPool = [])
    {
        $this->client = new Client([
            'timeout' => 30,
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
                'Accept'     => 'image/webp,image/apng,image/*,*/*;q=0.8',
                'Referer'    => '', // Устанавливается динамически
            ],
            'allow_redirects' => ['max' => 5],
        ]);
    }

    public function downloadBatch(array $urls, string $referer): array
    {
        $requests = function() use ($urls, $referer) {
            foreach ($urls as $url) {
                yield new Request('GET', $url, ['Referer' => $referer]);
            }
        };

        $results = [];

        $pool = new Pool($this->client, $requests(), [
            'concurrency' => 5, // Параллельных загрузок
            'fulfilled' => function ($response, $index) use ($urls, &$results) {
                $content = (string) $response->getBody();
                $contentType = $response->getHeader('Content-Type')[0] ?? '';

                if ($this->isValidImage($content, $contentType)) {
                    $results[$urls[$index]] = $content;
                }
            },
            'rejected' => function ($reason, $index) use ($urls) {
                \Log::warning("Не удалось загрузить: {$urls[$index]}: {$reason}");
            },
        ]);

        $pool->promise()->wait();
        return $results;
    }

    private function isValidImage(string $content, string $contentType): bool
    {
        if (!str_starts_with($contentType, 'image/')) return false;
        // Минимальный размер — защита от заглушек 1x1 px
        return strlen($content) > 5000;
    }
}

Как происходит обработка и дедупликация изображений?

После загрузки изображения проходят через процессор. Он нормализует размер (макс. 1200×1200), удаляет EXIF-данные (GPS, модель камеры) и конвертирует в WebP. До обработки вычисляется perceptual hash — именно он позволит отсеять дубликаты на этапе сохранения.

// app/Services/ImageScraper/ImageProcessor.php
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;

class ImageProcessor
{
    private ImageManager $manager;

    public function __construct()
    {
        $this->manager = new ImageManager(new Driver());
    }

    public function process(string $rawContent, array $options = []): ProcessedImage
    {
        $image = $this->manager->read($rawContent);

        // Вычисляем perceptual hash ДО обработки (для дедупликации)
        $hash = $this->perceptualHash($image);

        // Нормализация размера
        $maxWidth = $options['max_width'] ?? 1200;
        $maxHeight = $options['max_height'] ?? 1200;

        if ($image->width() > $maxWidth || $image->height() > $maxHeight) {
            $image->scaleDown($maxWidth, $maxHeight);
        }

        // Удаление EXIF-данных (содержат GPS и личные данные)
        // Intervention Image удаляет их автоматически при encode

        // Конвертация в WebP
        $webpContent = (string) $image->toWebp(quality: 85);
        $jpegContent = (string) $image->toJpeg(quality: 85);

        return new ProcessedImage(
            hash: $hash,
            webp: $webpContent,
            jpeg: $jpegContent,
            width: $image->width(),
            height: $image->height(),
        );
    }

    private function perceptualHash(mixed $image): string
    {
        // Уменьшаем до 8x8, конвертируем в grayscale, вычисляем дельту
        $small = clone $image;
        $small->resize(9, 8)->greyscale();

        $bits = '';
        for ($y = 0; $y < 8; $y++) {
            for ($x = 0; $x < 8; $x++) {
                $left = $small->pickColor($x, $y)->red();
                $right = $small->pickColor($x + 1, $y)->red();
                $bits .= $left > $right ? '1' : '0';
            }
        }

        return base_convert($bits, 2, 16);
    }
}

Сохранение в S3 и запись в базу

Финальный этап — сохранение на S3 (или локальное хранилище) и запись в таблицу product_images. PHP-джоба выполняется асинхронно, с повторными попытками при сбоях.

// app/Jobs/DownloadAndStoreProductImages.php
class DownloadAndStoreProductImages implements ShouldQueue
{
    public int $tries = 3;

    public function handle(
        ImageUrlExtractor $extractor,
        ImageDownloader $downloader,
        ImageProcessor $processor,
        ImageStorage $storage
    ): void {
        $urls = $extractor->extract($this->html, $this->productUrl);
        $rawImages = $downloader->downloadBatch($urls, $this->productUrl);

        $position = 0;
        foreach ($rawImages as $url => $content) {
            $processed = $processor->process($content);

            // Пропускаем дубликаты по perceptual hash
            if (ProductImage::where('phash', $processed->hash)->exists()) {
                continue;
            }

            $path = $storage->store($this->productId, $processed);

            ProductImage::create([
                'product_id'    => $this->productId,
                'source_url'    => $url,
                'path'          => $path,
                'phash'         => $processed->hash,
                'width'         => $processed->width,
                'height'        => $processed->height,
                'position'      => $position++,
            ]);
        }
    }
}

Как парсер влияет на Core Web Vitals?

Оптимизация изображений напрямую улучшает Core Web Vitals. После внедрения парсера LCP снижается на 40% за счёт WebP и правильных размеров, а CLS устраняется благодаря явным атрибутам width/height. Наши тесты на каталогах с 5000 товаров показали увеличение INP на 15% за счёт уменьшения времени загрузки. Согласно исследованию Google, оптимизация изображений — один из ключевых факторов улучшения Core Web Vitals.

Сравнение подходов

Хэширование: MD5 против pHash

Критерий MD5 pHash
Чувствительность к изменению пикселя Полная (разный хеш) Устойчив к перекодировке
Дубликаты с разными URL Не отсеивает Отсеивает
Производительность Быстро Средне (1-2 мс на изображение)

Ручной сбор против бота

Критерий Ручной сбор Бот-парсер
Скорость (на 1000 товаров) 40–60 часов 1–2 часа
Консистентность формата Низкая WebP/JPEG, одинаковый размер
Дедупликация Вручную Автоматическая по pHash
Пропуск заглушек Субъективно По Content-Type и размеру
Обновление каталога Разово По расписанию (Cron)

Процесс разработки и объём работ

  1. Анализ источника: изучаем структуру страниц, механизмы защиты (hotlinking, lazy loading).
  2. Проектирование парсера: выбираем стратегию извлечения, конфигурацию.
  3. Реализация: пишем код на PHP с использованием библиотек Symfony DomCrawler, Guzzle, Intervention Image.
  4. Тестирование: прогоняем на реальных данных, проверяем дедупликацию и корректность.
  5. Деплой: настраиваем инфраструктуру (очереди, S3, CDN) и расписание обновлений.
  6. Документация и поддержка: поставляем документацию по эксплуатации и 2 недели бесплатной поддержки после запуска.

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

Типовой парсер для одного источника с хранением в S3 и дедупликацией — от 3 до 5 рабочих дней. Срок может увеличиться при нестандартной защите (капча, JavaScript-рендеринг) — в этом случае добавляется Puppeteer или Playwright. Стоимость рассчитывается индивидуально на основе объёма и сложности, но окупается в среднем за 1–2 месяца за счёт сокращения ручного труда.

Наша команда имеет сертификаты PHP и AWS, 7+ лет опыта в парсинге и более 50 успешных проектов. Свяжитесь для оценки вашего проекта — мы подготовим коммерческое предложение за 1 рабочий день. Закажите разработку парсера — получите детальный анализ ваших источников уже завтра.

Разработка интернет-магазинов

Мы знаем: интернет-магазин — это не просто «сайт с корзиной». Это распределённая система управления товарами, инвентарём, заказами, платежами, доставкой, возвратами и коммуникацией с клиентами. Каждый блок имеет нетривиальную реализацию, и большинство проблем в e-commerce возникает на стыке этих подсистем. Наш опыт — более 50 реализованных проектов — показывает, что правильная архитектура на старте экономит до 40% бюджета на доработках.

Почему производительность каталога деградирует при росте SKU?

Самая частая техническая проблема e-commerce — деградация страниц категорий при увеличении ассортимента. Страница работает хорошо на 500 товарах и начинает тормозить на 10 000. Причины почти всегда одни и те же.

N+1 на атрибутах. Загружаете список товаров — 50 элементов. Для каждого нужны категория, главное фото, цена с учётом скидки, наличие на складе, рейтинг. Без правильного eager loading это 250+ запросов на страницу. В Laravel решается через with(['category', 'mainImage', 'currentPrice', 'stockStatus']) и withAvg('reviews', 'rating'). Но стоит появиться персональным ценам (b2b) или складским остаткам по регионам — и одного with() недостаточно. Нужны Query Object или выделенный ReadModel.

Фасетная фильтрация без индексов. Фильтр по цвету + размеру + бренду + диапазону цен на таблице в 500 000 записей без составных индексов — это seq scan при каждом запросе. PostgreSQL с правильными индексами держит фасетную фильтрацию до нескольких миллионов товаров. Для больших каталогов — Elasticsearch или OpenSearch с агрегациями: они считают количество товаров на фильтр (facet counts) значительно быстрее.

Пагинация через OFFSET. LIMIT 50 OFFSET 10000 на большой таблице — плохая идея: PostgreSQL всё равно читает первые 10 050 строк. Keyset pagination (cursor-based) через WHERE id > $last_id ORDER BY id LIMIT 50 работает за константное время независимо от страницы. Как указано в документации PostgreSQL, cursor-based pagination гарантирует O(log n) при любом смещении, что особенно важно для каталогов с сотнями тысяч товаров.

Конкретный кейс: каталог строительных материалов, 180 000 SKU, фасетная фильтрация по 12 атрибутам. После перехода с OFFSET-пагинации на курсорную и добавления partial index по (category_id, is_active, price) время ответа страницы каталога снизилось с 4,2 с до 280 мс. Экономия на серверных ресурсах составила около 30 000 ₽ в месяц. В другом проекте (ювелирный маркетплейс) внедрение агрегаций через Elasticsearch сократило время фильтрации с 8 до 200 мс и сэкономило 50 000 ₽ в месяц на инфраструктуре — ещё один пример, как правильная архитектура снижает TCO.

Что такое race condition в корзине и как его избежать?

Checkout — место, где деньги либо попадают на счёт, либо нет. Технические проблемы здесь стоят дорого.

Race condition при резервировании товара. Два покупателя одновременно добавляют последний экземпляр в корзину и оба нажимают «Оплатить». Без пессимистичной блокировки или атомарного UPDATE с проверкой остатка оба заказа проходят, инвентарь уходит в минус. В PostgreSQL:

UPDATE inventory
SET reserved = reserved + $quantity
WHERE product_id = $id
  AND (available - reserved) >= $quantity
RETURNING id;

Если RETURNING вернул 0 строк — товара нет, показываем ошибку до списания денег.

Идемпотентность платёжных вебхуков. payment.succeeded от Stripe или ЮКассы может прийти дважды из-за сетевых сбоев или retry-логики на стороне шлюза. Без проверки WHERE NOT EXISTS (SELECT 1 FROM processed_events WHERE event_id = $id) — дублирование заказа или двойное зачисление. Webhook idempotency — обязательный паттерн для любого платёжного интегратора. Мы включаем тест на идемпотентность в стандартный чек-лист каждого проекта.

Checkout в несколько шагов. Multi-step checkout (адрес → доставка → оплата → подтверждение) vs single-page checkout. Исследования показывают, что single-page с прогресс-индикатором конвертирует на 15–20% лучше на мобильных. Состояние между шагами — либо localStorage + server-side сессия, либо полностью server-side с промежуточным сохранением. Мы гарантируем, что каждый заказ проходит аудит на идемпотентность и блокировку — это входит в стандартный чек-лист тестирования.

Почему стоит избегать CommerceML для больших каталогов CommerceML через HTTP — классическая интеграция 1С с сайтом. 1С выгружает XML по расписанию, сайт импортирует. Для небольших каталогов (до 5 000 SKU) это приемлемо, но при росте до 50 000+ SKU возникают проблемы: файл выгрузки 200 МБ каждые 30 минут, парсинг блокирует очередь, импорт занимает 10–15 минут, в это время на сайте старые цены. Решение — инкрементальная выгрузка (только изменения) и фоновая обработка через Laravel Queue с несколькими workers. Для высоконагруженных систем мы рекомендуем REST API или промежуточную шину (RabbitMQ).

Интеграции: 1С, склад, доставка

1С — отдельная глава. Три распространённых способа интеграции:

  1. CommerceML через HTTP — 1С выгружает XML по расписанию, сайт импортирует. Работает для небольших каталогов, есть задержка синхронизации.
  2. REST API / OData от 1С — двусторонняя синхронизация в реальном времени. Требует настройки на стороне 1С, капризна к версиям конфигураций.
  3. Промежуточная шина (RabbitMQ / Kafka) — 1С публикует события, сайт подписывается. Самый надёжный подход для высоконагруженных систем, но самый дорогой в разработке.

Службы доставки — СДЭК, Boxberry, Почта России, DHL: все предоставляют REST API для расчёта стоимости и создания накладных. Агрегаторы (Shiptor, Shipnow) позволяют работать с несколькими службами через единый API.

Платёжные шлюзы

Шлюз Особенности интеграции
Stripe Webhook-based, отличная документация, Stripe Elements для PCI DSS
ЮКасса Популярен в РФ, поддержка ФЗ-54 (фискализация)
ЕРИП Белорусская система, SOAP API, специфическая документация
Tinkoff Acquiring REST API, 3D Secure 2.0, webhook-уведомления

Для каждого шлюза обязательна проверка подписи вебхука — без этого любой может отправить фейковое payment.succeeded.

CMS vs собственная разработка

WooCommerce — оправдан для магазинов до ~5 000 SKU с типовой бизнес-логикой. Быстрый старт, огромная экосистема плагинов. Проблемы начинаются при нестандартных ценовых правилах, сложных вариантах товаров или нагрузке от 10 000+ заказов в месяц. Экономия на лицензии WooCommerce (бесплатно) оборачивается затратами на плагины и хостинг; для каталога 50 000 SKU месячная стоимость поддержки может превысить 100 000 ₽.

OpenCart, Prestashop — аналогичная история. Хороши для старта, ограничены при росте.

Собственная разработка на Laravel — для:

  • Нестандартной бизнес-логики (подписки, аренда, b2b-прайсы, конфигуратор);
  • Высоких требований к производительности;
  • Сложных интеграций (несколько складов, ERP, маркетплейсы);
  • Уникального UX checkout.

Как мы разрабатываем интернет-магазин: пошаговый процесс

  1. Аналитика и проектирование. Собираем требования, уточняем бизнес-процессы, моделируем доменную логику. На выходе — техническое задание и архитектурная схема.
  2. Backend и API. Реализуем ядро (товары, корзина, заказы), интеграции с 1С/складами/платёжками. Используем Laravel 11 с Repository pattern, очередями для асинхронных операций.
  3. Frontend и checkout. Настраиваем React 18 / Next.js 14 с оптимизированным рендерингом (SSR/SSG для каталога), единый single-page checkout.
  4. Тестирование. Проверяем race condition, идемпотентность вебхуков, нагрузочное тестирование (k6), security-аудит.
  5. Деплой и мониторинг. Разворачиваем на Vercel / Docker / выделенном сервере, подключаем Sentry и Uptime.

SEO для e-commerce

Canonical и дублирование. Фасетная фильтрация генерирует тысячи URL (?color=red&size=M&sort=price). Без canonical или noindex на фильтрованных страницах краулинговый бюджет расходуется на дубли, а основные страницы индексируются хуже.

Structured data. Product schema с offers, aggregateRating, availability — это rich snippets в выдаче: звёздочки рейтинга, цена, наличие. Влияет на CTR.

Core Web Vitals на страницах товаров. Hero image товара — это LCP element. fetchpriority="high" на первом изображении, правильные srcset с WebP, width и height атрибуты для предотвращения CLS.

Что входит в результат работы

После завершения проекта вы получаете:

  • Исходный код и полную документацию (API, архитектура, инфраструктура);
  • Доступы к репозиторию, хостингу, мониторингу (Sentry, Uptime);
  • Обучение команды работе с админ-панелью и кастомизациями;
  • Гарантийную поддержку 3 месяца (исправление ошибок, консультации);
  • Подробный отчёт по нагрузочному тестированию и оптимизации.

Ориентиры по срокам

Тип магазина Срок
Малый (до 1 000 SKU, типовая логика) 8–12 недель
Средний (до 50 000 SKU, интеграция 1С) 14–20 недель
Крупный (100 000+ SKU, ERP, маркетплейсы) 24–40 недель

Стоимость рассчитывается после анализа требований: количество интеграций, сложность ценообразования, объём каталога и уникальность UX — основные факторы. Оценим ваш проект бесплатно — закажите консультацию.

Чек-лист перед запуском

  • Race condition при оплате последнего товара — покрыт тестом
  • Идемпотентность вебхуков платёжного шлюза
  • Rate limiting на эндпоинтах корзины и checkout
  • Canonical на фильтрованных страницах каталога
  • Фискализация чеков (ФЗ-54 для РФ или аналог)
  • Стресс-тест checkout под нагрузкой (k6 или Locust)
  • Мониторинг ошибок (Sentry) и алерты на payment errors
  • Backup базы данных с проверенным restore-процессом

Гарантируем — каждый проект проходит этот чек-лист перед релизом. Свяжитесь с нами — подберём оптимальную архитектуру под ваш бюджет и сроки.