Реализация импорта товаров из прайс-агрегаторов
Прайс-агрегаторы — Яндекс.Маркет, Price.ru, Pricelist.ru, E-Katalog и другие — аккумулируют данные о товарах от множества магазинов и производителей. Для интернет-магазина они могут быть источником обогащения каталога: описания, характеристики, изображения и ценовые ориентиры по рынку. Особенность работы с агрегаторами — у каждого свой формат выгрузки и своя модель доступа.
Источники данных агрегаторов
| Агрегатор | Формат данных | Способ получения |
|---|---|---|
| Яндекс.Маркет | YML (экспорт прайса) | Выгрузка из личного кабинета |
| Price.ru | XML / CSV | FTP или HTTP |
| E-Katalog | XML с характеристиками | API (платный) или выгрузка |
| OZON | JSON через API Seller | REST API |
| Wildberries | JSON через API Supplier | REST API |
| Pricelist.ru | CSV | HTTP |
Подход у каждого разный, но задача одна: нормализовать данные и уложить в единую схему каталога.
Адаптерный слой
interface AggregatorAdapterInterface
{
public function fetchProducts(array $options = []): iterable;
public function getSupportedFields(): array;
public function getSourceId(): string;
}
Регистрация в сервис-контейнере:
$this->app->tag([
YandexMarketAdapter::class,
EKatalogAdapter::class,
OzonSellerAdapter::class,
WildberriesAdapter::class,
], 'aggregator.adapters');
E-Katalog: характеристики и сравнения
E-Katalog — наиболее богатый источник технических характеристик. Их XML содержит стандартизованные спецификации с единицами измерения:
class EKatalogAdapter implements AggregatorAdapterInterface
{
public function fetchProducts(array $options = []): iterable
{
$response = $this->client->get('/api/v2/products', [
'query' => [
'category_id' => $options['category_id'] ?? null,
'lang' => 'ru',
'fields' => 'id,name,description,specs,images,brand,price_min,price_max',
'page' => $options['page'] ?? 1,
'per_page' => 200,
],
'headers' => ['Authorization' => 'Bearer ' . $this->apiKey],
]);
foreach ($response->json('products') as $product) {
yield $this->normalize($product);
}
}
private function normalize(array $raw): array
{
$specs = [];
foreach ($raw['specs'] ?? [] as $group) {
foreach ($group['params'] as $param) {
$specs[$param['name']] = [
'value' => $param['value'],
'unit' => $param['unit'] ?? null,
];
}
}
return [
'external_id' => 'ekatalog_' . $raw['id'],
'name' => $raw['name'],
'description' => $raw['description'],
'brand' => $raw['brand']['name'] ?? null,
'images' => array_column($raw['images'], 'url'),
'specs' => $specs,
'price_market_min' => $raw['price_min'],
'price_market_max' => $raw['price_max'],
];
}
}
OZON Seller API: актуальные данные по товарам
Если магазин торгует на OZON, можно тянуть оттуда контент:
class OzonSellerAdapter implements AggregatorAdapterInterface
{
public function fetchProducts(array $options = []): iterable
{
// OZON API v3: получение списка товаров продавца
$response = Http::withToken($this->apiKey)
->withHeaders(['Client-Id' => $this->clientId])
->post('https://api-seller.ozon.ru/v3/product/list', [
'filter' => ['visibility' => 'ALL'],
'limit' => 1000,
'last_id' => $options['last_id'] ?? '',
]);
$items = $response->json('result.items');
// Получаем детали батчами по 100
foreach (array_chunk(array_column($items, 'product_id'), 100) as $batch) {
$details = Http::withToken($this->apiKey)
->withHeaders(['Client-Id' => $this->clientId])
->post('https://api-seller.ozon.ru/v2/product/info/list', [
'product_id' => $batch,
])->json('result.items');
foreach ($details as $detail) {
yield $this->normalizeOzon($detail);
}
}
}
}
Использование рыночных цен для аналитики
Агрегаторы дают возможность отслеживать конкурентное ценообразование:
CREATE TABLE market_price_data (
product_id int REFERENCES products(id),
source varchar(50), -- 'ekatalog' | 'yandex_market' | 'price_ru'
price_min numeric(12,2),
price_max numeric(12,2),
price_avg numeric(12,2),
offers_count int,
collected_at timestamptz DEFAULT now()
);
На основе этих данных можно автоматически устанавливать цену как «рыночный минимум - 5%» или «выше среднего на 2%» — динамическое ценообразование на основе реальных данных.
Обогащение существующих товаров
Основной кейс: в каталоге есть товар с артикулом, но без характеристик и описания. Агрегатор знает этот товар по GTIN или названию бренда+модели:
class ProductEnrichmentService
{
public function enrich(Product $product): bool
{
// Поиск по GTIN у агрегаторов
foreach ($this->adapters as $adapter) {
$data = $adapter->findByGtin($product->gtin);
if (!$data) $data = $adapter->findByBrandModel($product->brand, $product->model);
if (!$data) continue;
$this->applyEnrichment($product, $data, $adapter->getSourceId());
return true;
}
return false;
}
private function applyEnrichment(Product $product, array $data, string $source): void
{
// Обогащаем только пустые поля — не перезаписываем существующее
if (!$product->description && !empty($data['description'])) {
$product->description = $data['description'];
$product->description_source = $source;
}
if (empty($product->specs) && !empty($data['specs'])) {
foreach ($data['specs'] as $name => $spec) {
ProductSpec::updateOrCreate(
['product_id' => $product->id, 'name' => $name],
['value' => $spec['value'], 'unit' => $spec['unit'], 'source' => $source]
);
}
}
$product->save();
}
}
Дедупликация при мультиагрегаторном обогащении
Когда несколько агрегаторов предоставляют данные для одного товара — нужна стратегия приоритетов:
private array $sourcePriority = [
'manufacturer_direct' => 100,
'ekatalog' => 80,
'yandex_market' => 70,
'ozon' => 60,
'price_ru' => 50,
];
Более приоритетный источник перезаписывает менее приоритетный. Приоритет хранится в product_specs.source_priority.
Сроки реализации
- Один адаптер (YML от Яндекс.Маркет), обогащение пустых полей — 2 дня
- Мультиагрегаторная структура + приоритеты + рыночные цены — +2 дня
- OZON/WB API, динамическое ценообразование на основе рынка — +2–3 дня







