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.







