Импорт 58 000 товаров в CS-Cart из CSV: каталог, характеристики, фильтры — программно
Магазин сантехники на CS-Cart, ~58 000 товаров. Штатный импорт не тянул логику характеристик, фильтров и вложенных категорий — собрал свой скрипт на API ядра: повторяемо и без дублей.
Задача
Интернет-магазин сантехники на CS-Cart, каталог ~58 000 товаров. Данные приходили одной CSV-выгрузкой: название, описание, цена, несколько фото на товар, плоское поле характеристик и трёхуровневая категория в текстовых колонках. Нужно было не просто залить строки, а собрать из них рабочий каталог — с вложенными категориями, характеристиками в карточке и в фильтрах, и так, чтобы повторная выгрузка обновляла товары, а не плодила дубли.
Это типичная ситуация для магазина, который переезжает с парсинга или другой платформы: штатный импортёр CS-Cart хорошо берёт простые поля, но на сложной логике — характеристики, которые должны стать фильтрами по уровням категорий, идемпотентный повторный импорт, тысячи изображений — либо спотыкается, либо требует ручной доводки на каждой итерации. Я решал это скриптом на API ядра, а не кликами в админке.
Решение
Импорт — это не разовая заливка, а повторяемый прогон: источник обновлялся, и скрипт должен был накатываться поверх без дублей. Поэтому я написал PHP-скрипт с бутстрапом init.php — он поднимает окружение CS-Cart и даёт доступ к функциям ядра, вместо записи в таблицы напрямую с риском рассинхрона кеша и связанных сущностей.
Категории. Каждая строка несёт путь из трёх уровней (category_level_1/2/3) в текстовом виде. Скрипт резолвит путь сверху вниз, создаёт недостающие узлы с правильным parent_id и переиспользует существующие. Каталог донора допускал несколько категорий на один товар — структуру привёл к этой же логике, чтобы товар находился во всех релевантных разделах.
Товары — upsert по артикулу. Перед записью товар ищется по product_code (SKU): есть — обновляется, нет — создаётся, через fn_update_product. Это и делает прогон идемпотентным — повторная выгрузка не плодит дубли, а догоняет изменения. Описания нормализую при импорте: переносы строк вместо литеральных \n, иначе текст карточки склеивается в сплошное полотно.
Характеристики и фильтры — главная сложность. Поле характеристик плоское: Название: Значение, часть помечена как фильтруемая. Для каждой характеристики скрипт проверяет, есть ли она уже (fn_get_product_features), при необходимости создаёт (fn_update_product_feature) и связывает значение с товаром (fn_update_product_features_values).
Отдельное решение — на каком уровне категорий вешать фильтры. Фильтруемые характеристики я привязал к категории 2-го уровня: первый уровень («Сантехника») оставил без фильтров — там они бессмысленны, а третий уровень использую только для точного определения подкатегории товара. CS-Cart сам скрывает фильтры без значений в конкретном разделе, поэтому покупатель в каждой подкатегории видит только релевантные фильтры, а не свалку из всех характеристик каталога. Привязка — через product_features_categories.
Изображения. Поле с картинками — список путей через ;. Скрипт разбирает его и привязывает изображения к товару (add_images_to_product), пропуская уже существующие при повторном прогоне. ~180 товаров пришли без фото — я зафиксировал их отдельным списком в отчёте, а не уронил импорт на отсутствующем файле.
Устойчивость прогона. Скрипт ведёт лог и сводный отчёт: сколько создано, сколько обновлено, что пропущено и почему. Пустые и битые поля валидируются, а не валят весь прогон. Это и позволило гонять импорт итеративно — например, повторно накатить характеристики после нового парсинга, не трогая остальное.

Стек
CS-Cart 4.18.3 · PHP 8.1 · MySQL 8.0 (MyISAM) · CSV · API ядра CS-Cart (fn_update_product, fn_update_product_feature, fn_update_product_features_values, add_images_to_product, product_features_categories)
Результат
- ~58 000 товаров в каталоге: название, нормализованные описания, цена, фото, характеристики.
- Характеристики работают и в карточке, и как фильтры — привязаны к категориям 2-го уровня, в каждой подкатегории показываются только релевантные.
- Вложенная структура категорий собрана из текстовых путей, товар доступен во всех своих разделах.
- ~180 товаров без изображений вынесены в отчёт, а не потеряны.
- Прогон идемпотентен: повторная выгрузка обновляет товары и характеристики без дублей — импорт можно гонять итеративно по мере обновления источника.

Подкатегория «Отдельностоящие акриловые ванны»: характеристики из CSV стали фасетными фильтрами — Бренд, Высота, Гарантия, Длина, Коллекция, Материал ванны, Назначение, Объём, Размер, — релевантными именно этой подкатегории.
Главное здесь не «залил N строк», а то, что каталог такого размера наполняется и переналивается повторяемым скриптом, а не вручную и не разовой загрузкой, которую страшно повторить.
Ограничение, честно. Скрипт заточен под конкретную схему этой выгрузки — набор колонок и формат поля характеристик. Под другой источник парсер и маппинг нужно перенастраивать: это инструмент проекта, а не универсальный импортёр «на все случаи». Зато внутри своего проекта он повторяемый и предсказуемый, что для каталога на 58k важнее универсальности.