ייבוא מ-Google Merchant Feed: Namespace, וריאנטים, הסרת כפילויות
בעת ייבוא מוצרים מ-Google Merchant Feed (GMF), מפתחים נתקלים לעיתים קרובות בבעיות לא טריוויאליות: XML namespace, וריאנטים באמצעות item_group_id, והסרת כפילויות לפי GTIN. הצוות שלנו ביצע למעלה מ-50 אינטגרציות לחנויות בגדלים שונים — מחנויות קטנות עם 500 מוצרים ועד לחנויות גדולות עם 500,000 פריטים. באמצעות מקרה אמיתי של ספק סיטונאי אלקטרוניקה, נפרק את המורכבויות הללו ונספק פתרונות קוד מוכנים המבטיחים סנכרון קטלוג אמין.
GMF הוא פורמט XML המבוסס על RSS 2.0 עם שדות מה-namespace g:. ספקים ומפיצים מכינים אותו עבור Google Shopping, אך עבור חנות מקוונת זהו מקור מצוין לנתונים מובנים: שדות חובה id, title, description, price, availability — כל מה שצריך עבור קטלוג. עם זאת, ללא parser מתאים וטיפול במקרי קצה רבים, הייבוא הופך לכאב ראש. לפי הנתונים שלנו, כ-30% מהפידים מכילים שגיאות namespace, ל-15% חסר GTIN, ול-10% מטבע שגוי. עבור מקרים כאלה, הכנו בדיקות אוטומטיות ואסטרטגיות גיבוי המבוססות על עשור של ניסיון עם פידים.
Google Merchant Center דורש שפידים יעמדו במפרט RSS 2.0 עם הרחבות ב-namespace g:. מקור: תיעוד רשמי של Google.
טיפול ב-Namespace בפידים של Google Merchant
כל השדות שייכים ל-namespace http://base.google.com/ns/1.0. אם לא מתחשבים בכך, SimpleXML לא יוכל לקרוא את הערכים. להלן parser עובד עם טיפול נכון ב-namespace, שנבדק על אלפי פידים.
class GoogleMerchantFeedParser {
private const G_NS = 'http://base.google.com/ns/1.0';
public function parse(string $filePath): iterable {
$reader = new \XMLReader();
$reader->open($filePath);
while ($reader->read()) {
if ($reader->nodeType === \XMLReader::ELEMENT && $reader->name === 'item') {
$node = new \SimpleXMLElement(
$reader->readOuterXml(),
0,
false,
'',
false
);
$g = $node->children(self::G_NS);
yield $this->parseItem($node, $g);
}
}
$reader->close();
}
private function parseItem(\SimpleXMLElement $item, \SimpleXMLElement $g): array {
[$price, $currency] = $this->parsePrice((string) $g->price);
[$salePrice] = $g->sale_price ? $this->parsePrice((string) $g->sale_price) : [null];
$images = [(string) $g->image_link];
foreach ($g->additional_image_link as $img) {
$images[] = (string) $img;
}
return [
'sku' => (string) $g->id,
'name' => (string) $g->title,
'description' => (string) $g->description,
'price' => $price,
'sale_price' => $salePrice,
'currency' => $currency,
'availability' => $this->parseAvailability((string) $g->availability),
'brand' => (string) $g->brand,
'gtin' => (string) $g->gtin,
'mpn' => (string) $g->mpn,
'condition' => (string) $g->condition,
'product_type' => (string) $g->product_type,
'google_category' => (string) $g->google_product_category,
'item_group_id' => (string) $g->item_group_id,
'images' => array_filter($images),
'color' => (string) $g->color,
'size' => (string) $g->size,
'material' => (string) $g->material,
'shipping_weight' => $this->parseWeight((string) $g->shipping_weight),
];
}
private function parsePrice(string $raw): array {
if (preg_match('/^([\d.,]+)\s+([A-Z]{3})$/', trim($raw), $m)) {
return [(float) str_replace(',', '.', $m[1]), $m[2]];
}
return [(float) $raw, 'USD'];
}
private function parseAvailability(string $raw): string {
return match (strtolower(trim($raw))) {
'in stock' => 'in_stock',
'out of stock' => 'out_of_stock',
'preorder', 'pre-order' => 'preorder',
'backorder' => 'backorder',
default => 'unknown',
};
}
private function parseWeight(string $raw): ?float {
if (!$raw) return null;
if (preg_match('/^([\d.,]+)\s*(g|kg|lb|oz)$/i', trim($raw), $m)) {
$value = (float) str_replace(',', '.', $m[1]);
return match (strtolower($m[2])) {
'kg' => $value,
'g' => $value / 1000,
'lb' => $value * 0.453592,
'oz' => $value * 0.0283495,
};
}
return null;
}
} מיפוי קטגוריות מוצרים של Google
Google משתמשת במזהים מספריים מהטקסונומיה שלה (לדוגמה, 142 = "Electronics > Audio > Headphones"). הטקסונומיה מתפרסמת כקובץ טקסט ומכילה כ-6,000 קטגוריות. אנו מתחזקים גרסה מעודכנת וממפים אותה אוטומטית לקטגוריות האתר. ניתוח עם XMLReader מהיר פי 2-3 מ-SimpleXML על פידים גדולים — קריטי לביצועים.
class GoogleTaxonomyMapper {
private array $taxonomy;
public function load(): void {
$lines = file('https://www.google.com/basepages/producttype/taxonomy-with-ids.ru-RU.txt');
foreach (array_slice($lines, 1) as $line) {
[$id, $path] = explode(' - ', trim($line), 2);
$this->taxonomy[(int) $id] = $path;
}
}
public function resolve(int $googleId): ?int {
$path = $this->taxonomy[$googleId] ?? null;
if (!$path) return null;
return CategoryMapping::where('google_taxonomy_id', $googleId)->value('site_category_id');
}
} קיבוץ מוצרי וריאנט באמצעות item_group_id
השדה class GoogleMerchantFeedParser { private const G_NS = 'http://base.google.com/ns/1.0'; public function parse(string $filePath): iterable { $reader = new \XMLReader(); $reader->open($filePath); while ($reader->read()) { if ($reader->nodeType === \XMLReader::ELEMENT && $reader->name === 'item') { $node = new \SimpleXMLElement( $reader->readOuterXml(), 0, false, '', false ); $g = $node->children(self::G_NS); yield $this->parseItem($node, $g); } } $reader->close(); } private function parseItem(\SimpleXMLElement $item, \SimpleXMLElement $g): array { [$price, $currency] = $this->parsePrice((string) $g->price); [$salePrice] = $g->sale_price ? $this->parsePrice((string) $g->sale_price) : [null]; $images = [(string) $g->image_link]; foreach ($g->additional_image_link as $img) { $images[] = (string) $img; } return [ 'sku' => (string) $g->id, 'name' => (string) $g->title, 'description' => (string) $g->description, 'price' => $price, 'sale_price' => $salePrice, 'currency' => $currency, 'availability' => $this->parseAvailability((string) $g->availability), 'brand' => (string) $g->brand, 'gtin' => (string) $g->gtin, 'mpn' => (string) $g->mpn, 'condition' => (string) $g->condition, 'product_type' => (string) $g->product_type, 'google_category' => (string) $g->google_product_category, 'item_group_id' => (string) $g->item_group_id, 'images' => array_filter($images), 'color' => (string) $g->color, 'size' => (string) $g->size, 'material' => (string) $g->material, 'shipping_weight' => $this->parseWeight((string) $g->shipping_weight), ]; } private function parsePrice(string $raw): array { if (preg_match('/^([\d.,]+)\s+([A-Z]{3})$/', trim($raw), $m)) { return [(float) str_replace(',', '.', $m[1]), $m[2]]; } return [(float) $raw, 'USD']; } private function parseAvailability(string $raw): string { return match (strtolower(trim($raw))) { 'in stock' => 'in_stock', 'out of stock' => 'out_of_stock', 'preorder', 'pre-order' => 'preorder', 'backorder' => 'backorder', default => 'unknown', }; } private function parseWeight(string $raw): ?float { if (!$raw) return null; if (preg_match('/^([\d.,]+)\s*(g|kg|lb|oz)$/i', trim($raw), $m)) { $value = (float) str_replace(',', '.', $m[1]); return match (strtolower($m[2])) { 'kg' => $value, 'g' => $value / 1000, 'lb' => $value * 0.453592, 'oz' => $value * 0.0283495, }; } return null; } } מקבץ וריאנטים של אותו מוצר (צבעים/מידות שונים). אנו מקבצים אותם, יוצרים מוצר אב ווריאנטים ילדים לפי מאפייני צבע/מידה. זה מאפשר הצגה נכונה של גרסאות בקטלוג וניהול נפרד של מחירים ומלאי עבור כל וריאנט.
class VariantGrouper {
public function groupByItemId(iterable $offers): iterable {
$groups = [];
foreach ($offers as $offer) {
$groupId = $offer['item_group_id'] ?: $offer['sku'];
$groups[$groupId][] = $offer;
}
foreach ($groups as $groupId => $variants) {
if (count($variants) === 1) {
yield ['type' => 'simple', 'data' => $variants[0]];
} else {
yield ['type' => 'variable', 'group_id' => $groupId, 'variants' => $variants];
}
}
}
} אחסון GTIN וחיפוש לפי ברקוד
GTIN (EAN-13, UPC, ISBN) הוא מזהה גלובלי המאפשר התאמה חד-משמעית של מוצר מהפיד לרשומה קיימת בקטלוג. עדיפות: GTIN > SKU > MPN. אנו מגדירים הסרת כפילויות כדי למנוע כפילויות. דוגמת שאילתה:
$product = Product::where('gtin', $offer['gtin'])
->orWhere('sku', $offer['sku'])
->orWhere('mpn', $offer['mpn'])
->first(); טיפול בפידים דחוסים
Google ממליצה לדחוס פידים גדולים. אנו מזהים אוטומטית את סיומת ה-.gz ומבצעים דחיסה תוך כדי קריאה באמצעות gzopen, וכותבים לקובץ זמני לצורך ניתוח后续.
private function openFeed(string $url): string {
$tmpFile = tempnam(sys_get_temp_dir(), 'gmf_');
if (str_ends_with(parse_url($url, PHP_URL_PATH), '.gz')) {
$gz = gzopen($url, 'rb');
$fp = fopen($tmpFile, 'wb');
while (!gzeof($gz)) fwrite($fp, gzread($gz, 8192));
gzclose($gz);
fclose($fp);
} else {
copy($url, $tmpFile);
}
return $tmpFile;
} השוואת שיטות ניתוח
| פרמטר | XMLReader | SimpleXML |
|---|---|---|
| גישה | streaming, דמוי SAX | DOM (טוען הכל לזיכרון) |
| צריכת זיכרון | O(1) | O(N) |
| מהירות על 100 אלף פריטים | ~1.5 שניות | ~4 שניות |
| יכולת כתיבה | לא | כן |
שדות חובה ב-Google Merchant Feed
| שדה | תיאור | דוגמה |
|---|---|---|
| id | מזהה ייחודי | "12345" |
| title | שם המוצר | "קומקום חשמלי BOSH" |
| description | תיאור המוצר | "קומקום 1.5 ליטר" |
| link | כתובת דף המוצר | כתובת דף המוצר |
| image_link | כתובת התמונה הראשית | כתובת התמונה |
| availability | זמינות (במלאי, אזל מהמלאי) | "במלאי" |
| gtin | ברקוד גלובלי | "4601234567890" |
| brand | מותג | "BOSH" |
| item_group_id | מזהה קבוצה לוריאנטים | "group_123" |
מדריך אינטגרציה שלב אחר שלב
- קבלת גישה לפיד (URL או FTP). Google Merchant Center מספק קישור לפיד לאחר העלאתו.
- ניתוח הפיד באמצעות XMLReader עם namespace
class GoogleTaxonomyMapper { private array $taxonomy; public function load(): void { $lines = file('https://www.google.com/basepages/producttype/taxonomy-with-ids.ru-RU.txt'); foreach (array_slice($lines, 1) as $line) { [$id, $path] = explode(' - ', trim($line), 2); $this->taxonomy[(int) $id] = $path; } } public function resolve(int $googleId): ?int { $path = $this->taxonomy[$googleId] ?? null; if (!$path) return null; return CategoryMapping::where('google_taxonomy_id', $googleId)->value('site_category_id'); } }— כפי שמוצג בקוד לעיל. - מיפוי קטגוריות: טעינת הטקסונומיה של Google ומיפוי לקטגוריות האתר שלך.
- ביצוע הסרת כפילויות: עדיפות לפי GTIN, ולאחר מכן לפי SKU או MPN.
- קיבוץ וריאנטים של מוצרים לפי השדה
g:item_group_id. - טעינת מוצרים לקטלוג, יצירה או עדכון של רשומות.
- הגדרת עדכונים תקופתיים (יומיים או מתוזמנים) באמצעות אותו parser.
עבור פידים עם מעל 200,000 מוצרים, אנו ממליצים לשמור במטמון את מיפויי הקטגוריות ב-Redis ולבטל רישום לוג במהלך ייבוא אצווה. זה מניב שיפור מהירות נוסף של 20-30%.
מה כלול באינטגרציה
אנו מציעים יישום ייבוא סוהר הכולל:
- Parser מותאם אישית עם תמיכה נכונה ב-namespace וטיפול בשגיאות.
- מיפוי קטגוריות Google לעץ הקטגוריות של האתר שלך.
- לוגיקת הסרת כפילויות לפי GTIN, SKU או MPN.
- קיבוץ מוצרי וריאנט באמצעות item_group_id.
- תמיכה בפידים דחוסים (.gz), דחיסה אוטומטית.
- עיבוד ואימות תמונות.
- בדיקות על הפיד האמיתי שלך עם כיסוי של 100%.
- תיעוד מלא והעברת קוד.
- שעת הדרכה אחת לצוות שלך.
- 30 ימי תמיכה לאחר ההשקה.
הניסיון שלנו של 10+ שנים ולמעלה מ-50 אינטגרציות מוצלחות מבטיחים התקנה חלקה. פריסת אינטגרציה זו מקצרת את זמן עדכון הקטלוג ב-90% וחוסכת בדרך כלל 80,000 RUB בשנה בעבודת יד. תקופת ההחזר היא פחות מחודשיים.
צור קשר כדי לדון בפרויקט שלך ולקבל ניתוח פיד. מפתחים מוסמכים מבטיחים פתרון אמין וניתן להרחבה.







