Реализация импорта товаров из Google Merchant Feed
Google Merchant Feed (GMF) — XML-формат на основе Atom/RSS с полями из пространства имён g:, который производители и дистрибьюторы готовят для Google Shopping. Для интернет-магазина импорт из этого формата открывает доступ к хорошо структурированным данным с обязательными полями id, title, description, price и availability — всё, что нужно для каталога.
Структура Google Merchant Feed
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
<channel>
<title>Supplier Product Feed</title>
<link>https://supplier.com</link>
<item>
<g:id>SKU-12345</g:id>
<g:title>Беспроводные наушники Example Pro</g:title>
<g:description>Наушники с шумоподавлением, 30 ч работы</g:description>
<g:link>https://supplier.com/products/sku-12345</g:link>
<g:image_link>https://cdn.supplier.com/sku-12345-main.jpg</g:image_link>
<g:additional_image_link>https://cdn.supplier.com/sku-12345-2.jpg</g:additional_image_link>
<g:availability>in stock</g:availability>
<g:price>4990 RUB</g:price>
<g:sale_price>3990 RUB</g:sale_price>
<g:brand>Example</g:brand>
<g:gtin>0012345678901</g:gtin>
<g:mpn>EP-PRO-BLK</g:mpn>
<g:condition>new</g:condition>
<g:product_type>Электроника > Аудио > Наушники</g:product_type>
<g:google_product_category>142</g:google_product_category>
<g:color>Чёрный</g:color>
<g:size>One Size</g:size>
<g:material>Пластик</g:material>
<g:item_group_id>EP-PRO</g:item_group_id>
<g:shipping_weight>350 g</g:shipping_weight>
</item>
</channel>
</rss>
Парсер с поддержкой namespace
Ключевая особенность GMF — все поля товара в пространстве имён g:. SimpleXML требует явной работы с 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
{
// "4990 RUB" → [4990.0, 'RUB']
if (preg_match('/^([\d.,]+)\s+([A-Z]{3})$/', trim($raw), $m)) {
return [(float) str_replace(',', '.', $m[1]), $m[2]];
}
return [(float) $raw, 'RUB'];
}
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 Product Category
Google использует числовые идентификаторы из таксономии (142 = «Electronics > Audio > Headphones»). Таксономия публикуется Google как текстовый файл и содержит ~6 000 категорий:
class GoogleTaxonomyMapper
{
private array $taxonomy; // [id => 'path > to > category']
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
Поле g:item_group_id объединяет варианты одного товара (разные цвета/размеры):
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];
}
}
}
}
Вариативный товар создаётся как родительский продукт с дочерними вариантами по атрибутам color/size.
Хранение GTIN и поиск по штрихкоду
GTIN (EAN-13, UPC, ISBN) — глобальный идентификатор, который позволяет однозначно сопоставить товар из фида с уже существующим в каталоге:
$product = Product::where('gtin', $offer['gtin'])
->orWhere('sku', $offer['sku'])
->orWhere('mpn', $offer['mpn'])
->first();
Приоритет: GTIN > SKU > MPN.
Сжатые фиды (.gz)
Google рекомендует сжимать большие фиды. Автодетект:
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;
}
Сроки реализации
- Парсер с namespace, базовые поля, доступность, цена с валютой — 1–2 дня
- Вариативные товары через item_group_id, маппинг Google-категорий — +1–2 дня
- GTIN-дедупликация, сжатые фиды, скачивание изображений — +1 день







