Представьте: интернет-магазин с 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) |
Процесс разработки и объём работ
- Анализ источника: изучаем структуру страниц, механизмы защиты (hotlinking, lazy loading).
- Проектирование парсера: выбираем стратегию извлечения, конфигурацию.
- Реализация: пишем код на PHP с использованием библиотек Symfony DomCrawler, Guzzle, Intervention Image.
- Тестирование: прогоняем на реальных данных, проверяем дедупликацию и корректность.
- Деплой: настраиваем инфраструктуру (очереди, S3, CDN) и расписание обновлений.
- Документация и поддержка: поставляем документацию по эксплуатации и 2 недели бесплатной поддержки после запуска.
Срок разработки
Типовой парсер для одного источника с хранением в S3 и дедупликацией — от 3 до 5 рабочих дней. Срок может увеличиться при нестандартной защите (капча, JavaScript-рендеринг) — в этом случае добавляется Puppeteer или Playwright. Стоимость рассчитывается индивидуально на основе объёма и сложности, но окупается в среднем за 1–2 месяца за счёт сокращения ручного труда.
Наша команда имеет сертификаты PHP и AWS, 7+ лет опыта в парсинге и более 50 успешных проектов. Свяжитесь для оценки вашего проекта — мы подготовим коммерческое предложение за 1 рабочий день. Закажите разработку парсера — получите детальный анализ ваших источников уже завтра.







