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

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

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

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка бота-парсера зображень товарів з зовнішніх сайтів
Середня
~3-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

Розробка бота-парсера зображень товарів

Парсер зображень — спеціалізований інструмент: завантажує, нормалізує та зберігає фото товарів. Завдання складніше, ніж здається: lazy loading, захист від хотлінкінгу, видалення watermark'ів, дедупліка ція за хешем, конвертація в WebP.

Архітектура

Scraper → Image URL Extractor
        → Downloader (async, proxy)
        → Image Processor (resize, convert, strip meta)
        → Hash Deduplicator
        → Storage (S3/local) + CDN
        → DB (product_images table)

Витягування URL зображень

// 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 (адаптивні зображення)
        $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);
        });
    }
}

Асинхронне завантаження

// 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;
    }
}

Обробка зображень

// 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 видаляє їх автоматично при кодуванні

        // Конвертація в 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 та БД

// 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++,
            ]);
        }
    }
}

Захист від hotlinking на стороні джерела

Деякі сайти повертають заглушку замість реального зображення при відсутності правильного Referer. Рішення:

// При завантаженні завжди передаємо Referer сторінки товара
$downloader->setReferer($productPageUrl);

// Перевіряємо Content-Type та мінімальний розмір
// Якщо отримали < 5KB — вірогідно, заглушка "403 Forbidden Image"

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

Парсер зображень для одного джерела з зберіганням в S3: 3-5 робочих днів, включаючи дедупліка цію та конвертацію в WebP.