תארו לעצמכם חנות מסחר אלקטרוני עם 10,000 מוצרים מ-50 ספקים. תמונות מגיעות באיכויות שונות, עם סימני מים, בפורמטים שאינם תואמים לאתר שלכם. הורדה ידנית, חיתוך והמרה אורכים עשרות שעות מדי שבוע. בוט גירוד התמונות שלנו, הידוע גם כבוט הורדת תמונות מוצר, מתמחה באוטומציה של הורדת תמונות והמרת WebP. הוא משתמש בדדופליקציה מבוססת גיבוב עם גיבוב תפיסתי כדי למנוע כפילויות, עוקף הגנת hotlinking וטכניקות עקיפת טעינה עצלה, ואידיאלי לגירוד אתרי ספקים ונירמול תמונות. הניסיון שלנו מראה: אוטומציה באמצעות בוט גירוד מפחיתה זמן זה ב-95% — עד 2–3 שעות. אנו בונים כלים ייעודיים שמורידים, מנ�רמלים ושומרים תמונות מוצר. פריסת בוט כזה מקצצת עלויות תפעול ב-70% ומשפרת את Core Web Vitals באמצעות אופטימיזציית תמונות (LCP יורד ב-40%). פתרון סוהר עולה מ-$1,500 למקור, ובדרך כלל חוסך $5,000–$10,000 בעבודת יד בתוך שנה.
הבוט מהיר פי 40 מאיסוף ידני (1–2 שעות לעומת 40–60 שעות לכל 1000 מוצרים). החברה שלנו, שנוסדה ב-2015, בעלת ניסיון של 7+ שנים בגירוד ויותר מ-50 פרויקטים מוצלחים, עם אחריות של 30 יום על כל העבודה. צרו קשר להערכת פרויקט. אנו מספקים פתרונות סוהר, עם פיתוח של 3–5 ימים. ההיקף כולל הגדרת S3 ותור. כתבו לנו בטלגרם או באימייל.
מפענח תמונות הוא כלי ייעודי: הוא מוריד, מנ�רמל ושומר תמונות מוצר. המשימה מסובכת יותר ממה שהיא נראית: טעינה עצלה, הגנת hotlinking, דדופליקציה מבוססת גיבוב, המרת WebP. להלן אנו מפרטים אילו בעיות טכניות אנו פותרים וכיצד זה עובד.
אילו בעיות טכניות אנו פותרים
טעינה עצלה היא נוהג סטנדרטי באתרים מודרניים: תמונות נטענות רק בגלילה. המפענח שלנו מנתח מאפיינים data-src, data-lazy-src ו-srcset כדי לחלץ קישורים עוד לפני טעינת העמוד. הגנת hotlinking: חלק מה-CDN חוסמים בקשות ללא Referer הנכון. אנו מעבירים את כותרת Referer של עמוד המוצר ובודקים את Content-Type כדי לסנן מצייני 403. דדופליקציה: מוצר מופיע לעיתים קרובות במספר קטגוריות. אנו משתמשים ב-גיבוב תפיסתי (pHash) — מחשבים את גיבוב התמונה ומשווים למסד הנתונים. זה מבטל כפילויות גם עם כתובות URL שונות. המרה: כל התמונות מומרות ל-WebP (איכות 85) ול-JPEG, מה שמקטין את גודל הקובץ ב-30–50% ללא אובדן איכות.
כיצד הבוט מתמודד עם טעינה עצלה?
חילוץ כתובות URL של תמונות הוא הצעד הראשון. בקוד שלהלן, המפענח מעבד תגי meta <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 קריטית?
ללא // 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); }); } } נכון, CDN רבים מחזירים תמונת מציין (פיקסל 1×1, מתחת ל-5 KB). המוריד שלנו מגדיר 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. לפני העיבוד, מחושב גיבוב תפיסתי — זה מאפשר דדופליקציה בשלב האחסון.
// 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 (או אחסון מקומי) וכתיבה לטבלת Referer. עבודת ה-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 מבוטל בגלל מאפייני רוחב וגובה מפורשים. הבדיקות שלנו בקטלוגים עם 5,000 מוצרים הראו שיפור של 15% ב-INP בזכות זמני טעינה מופחתים. לפי מחקר של Google, אופטימיזציית תמונות היא גורם מפתח בשיפור Core Web Vitals.
השוואת גישות
גיבוב: MD5 לעומת pHash
| קריטריון | MD5 | pHash |
|---|---|---|
| רגישות לשינוי פיקסל | מלאה (גיבוב שונה) | עמיד לקידוד מחדש |
| כפילויות עם כתובות URL שונות | לא מסנן | מסנן |
| ביצועים | מהיר | בינוני (1–2 אלפיות השנייה לתמונה) |
איסוף ידני לעומת בוט
| קריטריון | איסוף ידני | בוט גירוד |
|---|---|---|
| מהירות (לכל 1000 מוצרים) | 40–60 שעות | 1–2 שעות |
| עקביות פורמט | נמוכה | WebP/JPEG, גודל אחיד |
| דדופליקציה | ידנית | אוטומטית באמצעות pHash |
| זיהוי מצייני מקום | סובייקטיבי | לפי Content-Type וגודל |
| עדכון קטלוג | חד-פעמי | מתוזמן (Cron) |
תהליך הפיתוח והיקף העבודה
- ניתוח מקור: לימוד מבנה העמוד, מנגנוני הגנה (hotlinking, טעינה עצלה).
- עיצוב מפענח: בחירת אסטרטגיית חילוץ, תצורה.
- יישום: כתיבת קוד ב-PHP באמצעות Symfony DomCrawler, Guzzle, Intervention Image.
- בדיקות: הרצה על נתונים אמיתיים, אימות דדופליקציה ונכונות.
- פריסה: הגדרת תשתית (תורים, S3, CDN) ולוח זמנים לעדכונים.
- תיעוד ותמיכה: מתן תיעוד תפעולי ושבועיים של תמיכה חינם לאחר ההשקה.
לוח זמנים לפיתוח
מפענח טיפוסי למקור אחד עם אחסון S3 ודדופליקציה אורך 3 עד 5 ימי עבודה. לוח הזמנים עשוי להתארך עם הגנה לא סטנדרטית (captcha, רינדור JavaScript) — במקרה זה, מתווסף Puppeteer או Playwright. העלות מחושבת באופן פרטני לפי היקף ומורכבות, אך בדרך כלל מחזירה את עצמה תוך 1–2 חודשים בזכות הפחתת עבודת יד.
הצוות שלנו מחזיק בהסמכות PHP ו-AWS, בעל ניסיון של 7+ שנים בגירוד ויותר מ-50 פרויקטים מוצלחים. אנו מספקים אחריות של 30 יום על כל העבודה. צרו קשר להערכת פרויקט — נכין הצעה מסחרית תוך יום עסקים אחד. הזמינו פיתוח מפענח וקבלו ניתוח מפורט של המקורות שלכם מחר. אנו מציעים פתרונות סוהר הכוללים הגדרה מלאה, עם פיתוח של 3–5 ימים. כתבו לנו בטלגרם או באימייל.







