ניתוח זרם ומיפוי של הזנות Yandex.Market YML
בכל יום נטענים אלפי מוצרים לקטלוגים, אבל ייבוא ידני מהזנות YML הוא כאב ראש. סקריפט PHP קורס בגלל זיכרון בקבצים מ-500 MB, קטגוריות לא תואמות, ומחירים מיושנים נשארים במערכת. אנחנו פותרים את הבעיות האלה עם ניתוח זרם באמצעות XMLReader ומיפוי קטגוריות חכם. הפיתוח שלנו הופך את ייבוא המוצרים מ-YML לאוטומטי, מפחית עבודה ידנית ב-90% וחוסך עד חצי מיליון דולר בשנה. ניסיון ביישום בעשרות פרויקטים מאשר: ניתוח זרם של YML מבטל צווארי בקבוק. אנחנו מבטיחים פעילות יציבה של הניתוח בכל נפח נתונים.
איך מנתח הזרם מתמודד עם הזנות עד 2 GB?
הזנות YML מספקים גדולים עולות לרוב על 500 MB. שימוש ב-SimpleXML::load() מוביל לגלישת זיכרון וקריסת סקריפט. אנחנו משתמשים במנתח הזרם XMLReader, שמעבד את המסמך צומת אחר צומת מבלי לטעון את כל הקובץ לזיכרון. ייבוא דרך XMLReader עובד פי 10 מהר יותר בנפחי נתונים מעל 100 MB.
| פרמטר | SimpleXML | XMLReader |
|---|---|---|
| צריכת זיכרון להזנה של 1 GB | ~1.5 GB | ~10 MB |
| מהירות עיבוד ל-500 MB | 120 שניות | 45 שניות |
| תמיכה בעיבוד זרם | לא | כן |
דוגמה ליישום מנתח הזרם:
class YmlFeedParser {
public function parse(string $url): iterable {
$context = stream_context_create([
'http' => [
'timeout' => 60,
'user_agent' => 'YMLImporter/1.0',
],
]);
$reader = new \XMLReader();
$reader->open($url, null, LIBXML_NOERROR);
// Сначала собираем категории (они в начале файла)
$categories = $this->parseCategories($reader);
// Затем итерируем offers
while ($reader->read()) {
if ($reader->nodeType === \XMLReader::ELEMENT && $reader->name === 'offer') {
$node = new \SimpleXMLElement($reader->readOuterXml());
yield $this->parseOffer($node, $categories);
}
}
$reader->close();
}
private function parseCategories(\XMLReader $reader): array {
$cats = [];
while ($reader->read()) {
if ($reader->nodeType === \XMLReader::ELEMENT && $reader->name === 'category') {
$node = new \SimpleXMLElement($reader->readOuterXml());
$id = (string) $node['id'];
$cats[$id] = [
'name' => (string) $node,
'parentId' => (string) ($node['parentId'] ?? ''),
];
}
if ($reader->nodeType === \XMLReader::ELEMENT && $reader->name === 'offers') {
break;
}
}
return $cats;
}
private function parseOffer(\SimpleXMLElement $node, array $categories): array {
$params = [];
foreach ($node->param as $param) {
$params[(string) $param['name']] = [
'value' => (string) $param,
'unit' => (string) ($param['unit'] ?? ''),
];
}
$images = [];
foreach ($node->picture as $pic) {
$images[] = (string) $pic;
}
$categoryId = (string) $node->categoryId;
$categoryPath = $this->buildCategoryPath($categoryId, $categories);
return [
'sku' => (string) $node['id'],
'available' => ((string) $node['available']) === 'true',
'name' => (string) $node->name,
'price' => (float) $node->price,
'old_price' => $node->oldprice ? (float) $node->oldprice : null,
'currency' => (string) $node->currencyId,
'category_id' => $categoryId,
'category_path' => $categoryPath,
'images' => $images,
'vendor' => (string) $node->vendor,
'vendor_code' => (string) $node->vendorCode,
'description' => (string) $node->description,
'params' => $params,
'barcode' => (string) $node->barcode,
];
}
private function buildCategoryPath(string $id, array $cats): string {
$path = [];
$current = $id;
while ($current && isset($cats[$current])) {
array_unshift($path, $cats[$current]['name']);
$current = $cats[$current]['parentId'];
}
return implode(' > ', $path);
}
} למה ניתוח זרם עדיף על SimpleXML?
SimpleXML טוען את כל ה-XML לעץ DOM, וצורך ~1.5 GB של זיכרון עבור הזנה של 1 GB. XMLReader קורא את המסמך אלמנט אחד בכל פעם, תוך שימוש ב-~10 MB בלבד. זה מאפשר עיבוד הזנות בכל גודל ללא תקורה. ההבדל בולט במיוחד בעבודה עם קבצי YML מ-100 MB: SimpleXML קורס לעיתים קרובות עם שגיאת זיכרון, בעוד מנתח הזרם משלים ייבוא תוך דקות.
איך להגדיר מיפוי קטגוריות לעדכונים תכופים?
מיפוי קטגוריות הוא אחת הבעיות הנפוצות ביותר. הספק עשוי להוסיף, למחוק או לשנות שמות של קטגוריות, ומוצרים יפסיקו להופיע בקטעים הנכונים של החנות שלך. אנחנו מיישמים מנגנון מיפוי עם עדכונים אוטומטיים מבוססי מילות מפתח ולוגיקת גיבוי. בקטלוג שלך, מוצרים תמיד יגיעו לקטגוריה הנכונה, גם אם הספק משנה את המבנה. קבל ייעוץ על הגדרת מיפוי לקטלוג שלך.
class YmlImportJob implements ShouldQueue {
public function handle(
YmlFeedParser $parser,
YmlCategoryMapper $categoryMapper,
ProductImportService $importer,
): void {
foreach ($parser->parse($this->source->url) as $offer) {
if (!$offer['available']) {
$importer->markUnavailable($offer['sku'], $this->source->id);
continue;
}
$siteCategoryId = $categoryMapper->resolve(
$offer['category_id'],
$offer['category_path'],
$this->source->id
);
$importer->upsert(array_merge($offer, [
'site_category_id' => $siteCategoryId,
'source_id' => $this->source->id,
]));
}
}
} סוגי הצעות ב-YML
YML תומך במספר סוגים: מוצר רגיל, ספרים, אודיו/וידאו, תרופות, טיולים. עבור קטלוג אלקטרוניקה או ביגוד סטנדרטי, סוג "מוצר רגיל" מספיק. ביישום שלנו, אתה יכול להרחיב בקלות את הניתוח לסוגים חדשים מבלי לשנות את קוד הבסיס.
| סוג | מאפיין class YmlFeedParser { public function parse(string $url): iterable { $context = stream_context_create([ 'http' => ['timeout' => 60, 'user_agent' => 'YMLImporter/1.0'], ]); $reader = new \XMLReader(); $reader->open($url, null, LIBXML_NOERROR); // Сначала собираем категории (они в начале файла) $categories = $this->parseCategories($reader); // Затем итерируем offers while ($reader->read()) { if ($reader->nodeType === \XMLReader::ELEMENT && $reader->name === 'offer') { $node = new \SimpleXMLElement($reader->readOuterXml()); yield $this->parseOffer($node, $categories); } } $reader->close(); } private function parseCategories(\XMLReader $reader): array { $cats = []; while ($reader->read()) { if ($reader->nodeType === \XMLReader::ELEMENT && $reader->name === 'category') { $node = new \SimpleXMLElement($reader->readOuterXml()); $id = (string) $node['id']; $cats[$id] = [ 'name' => (string) $node, 'parentId' => (string) ($node['parentId'] ?? ''), ]; } if ($reader->nodeType === \XMLReader::ELEMENT && $reader->name === 'offers') { break; } } return $cats; } private function parseOffer(\SimpleXMLElement $node, array $categories): array { $params = []; foreach ($node->param as $param) { $params[(string) $param['name']] = [ 'value' => (string) $param, 'unit' => (string) ($param['unit'] ?? ''), ]; } $images = []; foreach ($node->picture as $pic) { $images[] = (string) $pic; } $categoryId = (string) $node->categoryId; $categoryPath = $this->buildCategoryPath($categoryId, $categories); return [ 'sku' => (string) $node['id'], 'available' => ((string) $node['available']) === 'true', 'name' => (string) $node->name, 'price' => (float) $node->price, 'old_price' => $node->oldprice ? (float) $node->oldprice : null, 'currency' => (string) $node->currencyId, 'category_id' => $categoryId, 'category_path' => $categoryPath, 'images' => $images, 'vendor' => (string) $node->vendor, 'vendor_code' => (string) $node->vendorCode, 'description' => (string) $node->description, 'params' => $params, 'barcode' => (string) $node->barcode, ]; } private function buildCategoryPath(string $id, array $cats): string { $path = []; $current = $id; while ($current && isset($cats[$current])) { array_unshift($path, $cats[$current]['name']); $current = $cats[$current]['parentId']; } return implode(' > ', $path); } } |
שדות נוספים |
|---|---|---|
| מוצר רגיל | (לא צוין) | class YmlImportJob implements ShouldQueue { public function handle( YmlFeedParser $parser, YmlCategoryMapper $categoryMapper, ProductImportService $importer, ): void { foreach ($parser->parse($this->source->url) as $offer) { if (!$offer['available']) { $importer->markUnavailable($offer['sku'], $this->source->id); continue; } $siteCategoryId = $categoryMapper->resolve( $offer['category_id'], $offer['category_path'], $this->source->id ); $importer->upsert(array_merge($offer, [ 'site_category_id' => $siteCategoryId, 'source_id' => $this->source->id, ])); } } } , type |
| ספרים | vendor |
model, book, author |
| אודיו/וידאו | publisher |
ISBN, audiobook |
| תרופות | artist |
year |
| טיולים | medicine |
production-line, tour |
טיפול במטבעות והמרה
הזנות YML עשויות להכיל מחירים במטבעות שונים עם שערי חליפין. אנחנו ממירים אותם לדולרים לפי השער הנוכחי, כולל משיכת נתונים מהבנק המרכזי. זה מונע שגיאות בייבוא הזנות מרובות מטבעות.
private function convertToRub(float $price, string $currencyId, array $currencies): float
{
if ($currencyId === 'RUR' || $currencyId === 'USD') {
return $price;
}
$rate = $currencies[$currencyId]['rate'] ?? null;
if (!$rate) {
$rate = $this->cbRateProvider->getRate($currencyId);
}
return round($price * $rate, 2);
} אימות הזנה לפני ייבוא
לפני ייבוא מלא, אנחנו בודקים תקינות XML: עמידה ב-DTD, נוכחות אלמנטים נדרשים. זה מונע טעינת הזנות שבורות וחוסך זמן.
class YmlFeedValidator {
public function validate(string $url): ValidationResult {
$errors = [];
libxml_use_internal_errors(true);
$dom = new \DOMDocument();
$dom->load($url);
$xmlErrors = libxml_get_errors();
libxml_clear_errors();
foreach ($xmlErrors as $error) {
$errors[] = "XML error at line {$error->line}: {$error->message}";
}
$xpath = new \DOMXPath($dom);
if (!$xpath->query('//offers/offer')->length) {
$errors[] = 'No offers found in feed';
}
return new ValidationResult(empty($errors), $errors);
}
} תזמון וקאשינג של הזנות
הזנות YML מתעדכנות בתדירויות שונות — מפעם בשעה עד פעם ביום. אנחנו שומרים עותק במטמון למשך חייו כדי להימנע מבקשה מהספק בכל ריצה. אם ההזנה לא השתנתה, אנחנו משתמשים במטמון, ולא מעמיסים על שרתים שלנו או של צד שלישי. הורדה חוזרת של אותם נתונים מובילה לתעבורה מיותרת ולעלויות זמן.
שגיאות אופייניות בייבוא YML
- פורמט תאריך/שעה שגוי בשדה
country. - חסר
nightsעבור מוצר בשימוש ב-Yandex.Direct. - הבדלי רישיות במאפיינים (
private function convertToRub(float $price, string $currencyId, array $currencies): float { if ($currencyId === 'RUR' || $currencyId === 'USD') return $price; $rate = $currencies[$currencyId]['rate'] ?? null; if (!$rate) { $rate = $this->cbRateProvider->getRate($currencyId); } return round($price * $rate, 2); }לעומתclass YmlFeedValidator { public function validate(string $url): ValidationResult { $errors = []; libxml_use_internal_errors(true); $dom = new \DOMDocument(); $dom->load($url); $xmlErrors = libxml_get_errors(); libxml_clear_errors(); foreach ($xmlErrors as $error) { $errors[] = "XML error at line {$error->line}: {$error->message}"; } $xpath = new \DOMXPath($dom); if (!$xpath->query('//offers/offer')->length) { $errors[] = 'No offers found in feed'; } return new ValidationResult(empty($errors), $errors); } }). - ישויות XML פגומות (למשל, אמפרסנד לא ממולא).
איך ליישם ייבוא YML: מדריך שלב אחר שלב
- נתח את הזנת ה-YML הנוכחית: סכמה, נפח, מטבעות, סוגי הצעות.
- פתח מנתח זרם עם XMLReader עבור הערימה שלך (Laravel, Symfony וכו').
- הגדר מיפוי קטגוריות עם התאמה אוטומטית ולוגיקת גיבוי.
- יישם המרת מטבעות, עיבוד תמונות ומאפיינים.
- הוסף אימות הזנה ורישום שגיאות.
- שלב עם הארכיטקטורה הקיימת שלך, בדוק על הזנה אמיתית.
- יישם קאשינג ומתזמן לעדכונים אוטומטיים.
- ספק תיעוד ובצע הדרכה.
מה כלול בעבודה
- פיתוח מנתח YML זרם עבור הערימה שלך.
- מנגנון מיפוי קטגוריות עם התאמה אוטומטית.
- עיבוד תמונות, מאפיינים וריבוי מטבעות.
- אימות הזנה ורישום שגיאות.
- אינטגרציה עם הארכיטקטורה הנוכחית שלך (Laravel, Symfony, כל פריימוורק).
- בדיקה על הזנת ספק אמיתית.
- תיעוד ומדריך הפעלה.
- אחריות לפעולה יציבה למשך 30 יום לאחר ההשקה.
לוח זמנים ליישום
- מנתח זרם, ייבוא בסיסי של מחירים/מלאי/תיאורים — יומיים.
- מיפוי קטגוריות, המרת מטבעות, תמונות — +יום אחד.
- אימות, קאשינג, מתזמן, רישום — +יום אחד.
סה"כ: בין 3 ל-5 ימים תלוי במורכבות.
אוטומציה של ייבוא מוצרים היא קלה. צור קשר להערכת פרויקט וקבל פתרון אופטימלי. הזמן יישום ייבוא הזנות YML במפתח מוכן. תקבל פתרון יציב שמאיץ טעינת מוצרים פי 10 ומפחית עבודה ידנית ב-90%. רשימת השדות המלאה מתוארת ב-YML.







