Suppose your catalog has 50,000 products, and 80% of them have empty descriptions and characteristics. Manual filling would take a content manager six months and significant investment. Importing from price aggregators solves this in days, providing substantial annual savings. We develop parsers and importers turnkey: from a single YML file to a multi-aggregator system with priorities. During our work, we've automated imports for 50+ online stores, processing over 2 million products.
The most common problem is format incompatibility. Yandex.Market delivers YML, Price.ru — XML, OZON — JSON. Without normalization, data becomes a mess. This is where the adapter pattern comes to the rescue. It isolates the logic of each source, allowing you to change the format or add a new one without modifying existing code.
We use a single interface for all sources, enabling us to add a new aggregator in 2 days. As a result, you get a unified catalog with correct prices, characteristics, and images. Conversion grows by 15–20% due to data completeness. Catalog update time is reduced by 5 times.
Data sources from aggregators
| Aggregator | Data format | Retrieval method |
|---|---|---|
| Yandex.Market | YML (price export) | Export from personal account |
| Price.ru | XML / CSV | FTP or HTTP |
| E-Katalog | XML with characteristics | API (paid) or export |
| OZON | JSON via Seller API | REST API |
| Wildberries | JSON via Supplier API | REST API |
| Pricelist.ru | CSV | HTTP |
Each approach is different, but the goal is the same: normalize data and fit it into a single catalog schema.
How to normalize data from different formats?
Adapter layer
interface AggregatorAdapterInterface
{
public function fetchProducts(array $options = []): iterable;
public function getSupportedFields(): array;
public function getSourceId(): string;
}
Registration in the service container:
$this->app->tag([
YandexMarketAdapter::class,
EKatalogAdapter::class,
OzonSellerAdapter::class,
WildberriesAdapter::class,
], 'aggregator.adapters');
Adapters hide differences in APIs and formats. A new aggregator is added with a single interface implementation.
E-Katalog: characteristics and comparisons
E-Katalog is the richest source of technical specifications. On average, it provides 40% more specs per product than Yandex.Market's YML export. Their XML contains standardized characteristics with units of measurement.
According to E-Katalog API documentation, their XML contains standardized characteristics with units of measurement.
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 returns data in JSON, but with a limit of 100 products per request. We use pagination and batch loading.
Why is the adapter pattern the best choice?
Compare with a monolithic parser: every format change breaks the whole system. Adapters isolate changes — a new aggregator is added in 1–2 days without touching existing ones. We use this approach in 50+ projects for import automation. The adapter pattern processes 10,000 products 3 times faster than a monolithic script and reduces the integration time of a source by 5 times.
| Parameter | Monolithic parser | Adapter pattern |
|---|---|---|
| Time to add a source | 2–3 weeks | 2 days |
| Risk of breakage on change | High | Zero |
| Code maintainability | Complex | Simple |
Want to implement this approach? Get a free consultation.
Using market prices for analytics
To store data, a market_price_data table is created with fields: product_id, source, price_min, price_max, price_avg, offers_count, collected_at. Based on this data, you can automatically set the price as "market minimum - 5%" or "2% above average" — dynamic pricing based on real data. This solution increases conversion by up to 15% in our projects.
How to add a new aggregator: step-by-step guide
- Create an adapter class implementing
AggregatorAdapterInterface. - Register the adapter in the service container with the tag
aggregator.adapters. - Implement field mapping from the source to the unified schema.
- Test on a sample of 200 products.
- Run the full load.
Enriching existing products
The main use case: the catalog has a product with an SKU but without characteristics and description. The aggregator knows this product by GTIN or brand+model name. We enrich only empty fields without overwriting manual edits.
class ProductEnrichmentService
{
public function enrich(Product $product): bool
{
// Search by GTIN across aggregators
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
{
// Enrich only empty fields — do not overwrite existing
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();
}
}
How does deduplication work?
Example of source priority configuration: the sourcePriority array defines which source is considered primary. Higher number means higher priority. In case of conflict, the higher-priority source wins.
private array $sourcePriority = [
'manufacturer_direct' => 100,
'ekatalog' => 80,
'yandex_market' => 70,
'ozon' => 60,
'price_ru' => 50,
];
Deduplication is performed by external ID (GTIN, SKU). If two sources provide the same product, data is taken from the higher-priority source. This eliminates duplicates and conflicts.
Implementation timeline
- One adapter (YML from Yandex.Market), empty field enrichment — 2 days
- Multi-aggregator structure + priorities + market prices — +2 days
- OZON/WB API, dynamic pricing based on market — +2–3 days
What's included in the work
- Development and configuration of adapters for each source
- Data normalization and deduplication
- Enrichment of empty fields (descriptions, characteristics, images)
- Preparation of documentation on data structure and update process
- Transfer of access to the system (personal account, FTP, API keys)
- Training content managers to work with import
- Technical support for one month after launch
The final timeline depends on the number of adapters and complexity of normalization. Contact us for a free assessment of your project. Order product import and get a catalog with complete data in 2 days.







