Заказчик пришёл с конкретной болью. Магазин работал на OpenCart, и импорт от поставщиков не делал свою работу: товары задваивались, картинки заливались повторно, позиции попадали не в те категории, атрибуты подставлялись чужие. После каждой выгрузки каталог приходилось разбирать руками и править карточки по одной. Это отнимало время, которое должно было уходить на продажи, и копило раздражение.
До меня сайт вели несколько исполнителей, включая профильную студию по OpenCart, и наладить импорт не получилось ни у кого. Когда я разобрал код, стало понятно почему: он собирался разными руками и до конца не доводился, часть кусков выглядела сгенерированной и не делала того, что заявлено, попадались небезопасные места.
Сначала я пробовал чинить на месте. Но модуль импорта был зашифрован ionCube: разобрать его логику и поправить поведение было нельзя, а вокруг лежал легаси от разных разработчиков, который тоже пришлось бы переписывать. Латать это выходило дольше и дороже, чем перенести магазин туда, где импорт пишется с нуля и открыто. Так проект переехал на WooCommerce, а я написал под него два модуля — импорт поставщиков и конвертер прайсов.
Перенос с OpenCart на WooCommerce
- перенесены товары с вариациями, категории, атрибуты, бренды, клиенты и заказы;
- дерево категорий пересобрано под витрину: фильтры по цене, производителю, размеру и цвету;
- старые адреса закрыты постраничными редиректами, чтобы не потерять позиции в поиске;
- витрина и карточка товара рассчитаны на каталог в тысячи позиций.
Модуль импорта поставщиков
Ядро проекта и ответ на исходную боль. Каждый поставщик заводится отдельным источником со своими правилами разбора, а совпадения ищутся по артикулу, так что повторный прогон обновляет карточку, а не создаёт вторую.
- источники CSV с любым разделителем и кодировкой, у каждого свой тип разбора;
- сопоставление колонок файла с полями WooCommerce прямо в интерфейсе, с предпросмотром значений;
- маппинг категорий и брендов, исключение ненужных брендов, маппинг атрибутов — размер, цвет, состав;
- картинки сверяются по имени файла: уже загруженные не скачиваются заново и не плодят копии в медиатеке;
- наценка в процентах и режимы «добавить», «обновить», «добавить и обновить»;
- автозапуск по слотам времени: у каждого источника свой слот, чтобы прогоны не накладывались друг на друга;
- порционная обработка — файл в тысячи строк читается батчами и не роняет сайт по памяти и таймауту;
- товары, пропавшие из выгрузки или оставшиеся без цены, скрываются автоматически;
- защита полей галочками: если описание или главное фото доработаны вручную, импорт их не трогает;
- журнал прогонов с результатами и ошибками.
Конвертер прайсов в XLS
Для брендов Gorsenia и ModeNova готового фида нет вообще: прайс приходит на почту в XLS, а фотографии лежат отдельной папкой на Яндекс.Диске. Под них написан отдельный конвертер.
- разбирает XLS и приводит его к тому же CSV, который понимает импорт;
- строит индекс папки на Диске через API, по именам файлов, без скачивания гигабайтов;
- сопоставляет фотографии с товаром по артикулу и названию цвета, включая написание латиницей и кириллицей;
- описания прогоняет через нейросеть, чтобы вытащить состав, цвет и характеристики из сплошного текста;
- два режима: полная пересборка прайса и обновление цен с добавлением новинок.
Что было сложным
- поставщики не структурируют данные: категории приходят строкой с разделителями, размеры и цвета лежат в одном поле, часть артикулов дублируется;
- у каждого бренда своя логика вариаций, а собрать их нужно в один товар WooCommerce;
- прайсы меняются без предупреждения, поэтому разбор сделан устойчивым к новым и пропавшим колонкам;
- объёмы: один прогон — это тысячи строк, и всё это должно обновляться в фоне, пока магазин работает.
Результат
Ручной разбор каталога закончился. Один прогон обрабатывает 4217 строк — 448 товаров и 3769 вариаций, — и такие прогоны идут по расписанию дважды в сутки для каждого бренда. Дублей нет, товары встают в свои категории, атрибуты подставляются те, что заданы в маппинге. Цены, остатки и новинки приезжают на витрину без участия владельца магазина. Работа заняла месяц.



