Общее устройство
• main.py
o Инициализирует окружение (лог-уровень, режимы отправки JSON и т.п.), подхватывает категории из categories.xlsx (через categories.py), запускает экстрактор и рекордер.
o По договорённости, переменные окружения:
• LOG_LEVEL — уровень логов (DEBUG|INFO|WARNING|ERROR).
• SEND_JSON / SAVE_JSON — 0/1 (или False/True) для отправки POST и/или сохранения локальной копии JSON.
o Обрабатывает исключения верхнего уровня (не падаем молча).
• categories.py
o Читает categories.xlsx, отдаёт список URL категорий (пустые/невалидные строки игнорируются).
• requester.py
o Унифицированная обёртка вокруг HTTP:
• parse(url, return_type=None) — делает GET, ретраи, заголовки, куки; при return_type="json" — json.loads().
• Логи HTTP-запросов (в DEBUG видно метод/код/URL).
• Контролируемые паузы между повторами при бэд-кодах.
o Возвращает None при безнадёжной ошибке (экстрактор учитывает это).
• xlsx_recorder.py
o Создаёт папку records_folder (если её нет).
o Пишет XLSX с «шапкой» колонок и данными.
o Имя файла санитизируется (убираются/заменяются проблемные символы типа ? : * " < > | \ /), чтобы windows/mac не ругались.
• extractor.py (ядро)
o Класс Extractor выбирает метод в зависимости от parse_settings.json["method"].
o Для ZaraHome — метод zarahome_extract_method. Остальные магазины пока «заглушены» (сообщают, что отключены), но исходные заготовки/сигнатуры сохранены, чтобы parse_settings не падал.

Поток данных (Zara Home)
1. Старт по категории
o Загружаем HTML категории; парсим #serverApp-state (большой JSON от сайта).
o Определяем category_id. Ищем продуктовый ключ /category/{id}/product... в state.
2. Получение списка товаров в категории
o Если в state сразу есть массив products — берём «сводки» оттуда.
o Если есть только списки ID (productIds, sortedProductIds...) — тянем товары пачками через API:
o https://www.zarahome.com/itxrest/3/catalog/store/85009924/80290000/productsArray?languageId=-1&productIds=ID1,ID2,...&appId=1
o Размер пачки ~20–50 ID (выбираем безопасно, чтобы не словить таймауты/блокировки).
o Итог: «список сводок» товаров по категории (summaries).
3. Проход по товарам и разворачивание вариаций
o Для каждой сводки:
• Если в ней уже есть detail.colors — это единичный товар, обрабатываем как «vprod».
• Если это «bundled» — пробегаем по bundleProductSummaries.
• Если вообще нет цветов — считаем «моно-вариант».
o На каждом «vprod» берём:
• name, detail.displayReference (article), price (в копейках), weight, и т.д.
• Состав/уход/происхождение:
• compositionDetail.parts → через нашу функцию extract_components_zarahome() собираем читаемую строку.
• care[] → список описаний, склеенных переносами.
• trace (страна изготовления) — берём из поля country у конкретного size и формируем строку Страна изготовления {COUNTRY} (если есть).
• Каталожный путь: из sectionNameEN, familyName, subFamilyName (что пришло в конкретном vprod). Склеивается как Каталог/ZaraHome/<Section>/<Family>/<SubFamily> (пустые звенья выкидываются).
4. Цвета и размеры
o Проходим detail.colors:
• clr_name (название цвета),
• sizes[] — по каждому размеру берём:
• size.name/size.description → склеенный size_full ("{description} ({name})" или просто name),
• visibilityValue → поле «Наличие на сайте» (SHOW → в наличии, всё остальное → не в наличии),
• price (переводим из копеек в основную валюту),
• weight,
• sku и partnumber: берём напрямую из полей размера size["sku"], size["partnumber"].
o Картинки цвета (важно, обновлено):
• Собираем все deliveryUrl из extraInfo каждого медиа, относящегося к текущему цвету (фильтр по colorCode).
• clr_imgs_s = все такие URL (удаляем дубли, порядок сохраняем).
• clr_image (превью цвета) — выбираем единственный кадр из тех же медиа по правилу:
• смотрим только медиа с clazz == 3 (ракурс R),
• среди них приоритет кадрам, у которых deliveryUrl содержит "-r1/", если таких нет — берём первый с "-r/",
• если вообще нет clazz == 3 — фолбэк: превью из clr["image"]["url"] + "_3_1_0.jpg".
5. Фильтрация дублей (DEL_SAME)
o Опциональная логика (включается флагом DEL_SAME = "YES"):
• Сначала группируем позиции по ключу: (article, name, clr_name, size_full, price).
• Если в группе одна позиция — оставляем.
• Если несколько:
1. Если все partnumber одинаковые — оставляем любую одну (остальные выкидываем).
2. Если partnumber различаются:
• Сначала смотрим visibilityValue:
• если в группе есть SHOW и нет противоречий по «короткому артикулу» (см. ниже) — оставляем только SHOW.
• если vis у всех одинаковый → идём к проверке «короткого артикула».
• «Короткий артикул» vs PartNumber: сравниваем article[:4] и partnumber[1:5].
• если совпадают — оставляем только те, у кого совпали 4 символа;
• если не совпадают — удаляем такой элемент как «лишний вариант».
• В итоге из каждой группы остаётся максимум 1 строка.
o Вся фильтрация выполняется в памяти, уже после сбора всех строк по категории, до записи в XLSX/JSON. Это резко ускорило работу (запись — один раз в конце).
6. Запись в XLSX
o Формируем шапку и строки в точной последовательности, как ожидает ваш шаблон.
o Имя файла — хвост URL категории, очищенный от запрещённых символов.
7. Формирование JSON и отправка POST
o Для каждой строки сначала проверяем «ценовой фильтр»:
• MIN_PRICE <= cost <= MAX_PRICE (включительно) — только такие попадают в JSON/POST.
o Формируем объект:
o {
o   "items": [
o     {
o       "category": { "name": "<очищенный путь без 'Каталог/ZaraHome/'>" },
o       "variant": {
o         "status_id": 1,
o         "color": "<ClrName, Первая Заглавная>",
o         "sku": "<article>-<partnumber>",
o         "size": "<size_full с <br/>>",
o         "cost": <price округлён вверх до целого>,
o         "originalUrl": "<url_full>",
o         "originalName": "<Name, Первая заглавная, остальное как есть>",
o         "originalDescription": "<longDescription + care + 'Страна изготовления ...' склеены через <br/>>",
o         "originalComposition": "<comp_txt с <br/>>",
o         "images": ["<список clr_imgs_s>"],
o         "inStock": <true если vis == 'SHOW'>,
o         "weight": <вес кг, ceil(граммы/1000)>
o       },
o       "brand": { "name": "zara-home" }
o     }
o   ],
o   "parserName": "zara-home"
o }
o Нормализация category.name: из cat_path убираем префикс Каталог/ZaraHome/, затем заменяем все спецсимволы, кроме / и -, на _ (регулярка по сути [^\\w\\/-]+ → _).
o Все \n в size_full и comp_txt заменяем на <br/>.
o originalDescription — это longDescription, затем care (каждую строку), затем наша строка Страна изготовления ... — склеиваем через <br/>, все \n → <br/>.
o Отправка: POST на http://localhost:3005/parser/data с JSON-телом (если SEND_JSON=1).
o Сохранение локально: если SAVE_JSON=1, пишем JSON-файл рядом (имя файла включает timestamp и ключ категории; расширение .json).
o Если цена вне диапазона — позиция остаётся только в XLSX (в JSON не попадает).
8. Исключение по материалам (exclusion_materials.txt)
o Путь: в корне проекта (рядом с кодом), имя exclusion_materials.txt.
o Формат: строки вида "glass","kniv","knif"," steel ", запятая — разделитель, кавычки обязательны (пробелы внутри кавычек учитываются).
o Для каждого товара берём только поле comp_txt и приводим к одному регистру.
o Если вхождение любой строки из словаря встречается как подстрока (без требований к границам слова), товар исключается из JSON/POST (из XLSX не исключаем).
o Сравнение регистронезависимое.
o Если файл отсутствует — считаем список исключений пустым.

Обработка картинок (детально)
• Источник — данные варианта (конкретного colorCode) из detail.xmedia.
• Мы больше не конструируем пути из idMedia + "2.jpg" и не используем mediaLocations для ссылок.
• Берём «как есть» extraInfo.deliveryUrl (это уже абсолютный CDN-URL), причём:
o Для списка Изображения варианта (clr_imgs_s) — все deliveryUrl для текущего цвета, кроме clazz == 3 (ракурсы r/r1 отфильтрованы).
o Для clr_image — выбираем один кадр из clazz == 3 по приоритету -r1/ → -r/ (если нет — фолбэк к превью …_3_1_0.jpg из clr['image']['url']).
• Дубликаты удаляем, порядок — как в исходных массивах.
(Если когда-нибудь понадобится вернуть сортировку по xmediaLocations и/или учитывать только location == 0, это можно включить отдельно: у нас уже были патчи, но на текущей ревизии выбрана стратегия “все deliveryUrl текущего цвета, минус clazz==3 для списка и с отдельным выбором превью”).

Логирование
• Единый логер с форматированием времени/уровня/сообщения.
• LOG_LEVEL управляет подробностью.
• В DEBUG видно:
o Запуски категорий/количество продуктов,
o HTTP запросы (метод/код/URL),
o Ключевые точки преобразований (при необходимости можно включать точечные log.debug() в «узких местах» — выбор clr_image, выкидывание дублей, отбор по цене и т.п.).

Конфигурация/переключатели
• DEL_SAME = "YES"|"NO" — включение/выключение продвинутой фильтрации дублей (по (article, name, clr_name, size_full, price, partnumber, vis)).
• MIN_PRICE, MAX_PRICE — границы цены для попадания в JSON/POST (включительные).
• SEND_JSON / SAVE_JSON — 0/1, можно передать через env или (если так реализовано у вас в main.py) спросить у пользователя интерактивно при запуске, когда переменные не заданы.
• Путь к exclusion_materials.txt — корень проекта.

Производительность
• Сбор всей категории в память, затем фильтрации (дубли/исключения/ценовой фильтр), затем единичная запись XLSX/JSON → резко быстрее, чем писать строки по одной.
• API пачками по productIds (сбалансированный размер пачки).
• Минимум повторных запросов на одну карточку.

Отказоустойчивость/краевые случаи
• Если не нашли category_id в serverApp-state — категория пропускается (логируется ошибка).
• Если products пуст или productIds пуст — логируем: «no products in this category».
• Если по размеру отсутствует sku/partnumber — пишем пустые строки (а sku в JSON формируем аккуратно, чтобы не получить "-").
• Если deliveryUrl отсутствует — картинка цвета уходит в фолбэк (см. выше), список картинок может быть пуст.
• Если нет ни sectionNameEN/familyName/subFamilyName — cat_path укорачивается до того, что есть (или пусто), а category.name в JSON корректно нормализуется.
• Если comp_txt пустой, модуль исключений просто ничего не найдёт.
• Если хост localhost:3005 недоступен — логируем ошибку POST; XLSX и локальные JSON (при SAVE_JSON=1) при этом остаются.

Что «самое важное» мы поменяли в 2.0
1. Развели сбор картинок: список картинок варианта (clr_imgs_s) теперь из deliveryUrl, а не из конкатенации idMedia + "2.jpg".
2. Выбор превью цвета (clr_image) — строго из clazz == 3 с приоритетом -r1/ → -r/ (вместо «случайных» кропов).
3. Фильтрация дублей вынесена до записи (по группам, как описано выше) → существенный выигрыш по скорости и стабильности размера XLSX.
4. Ценовой фильтр и исключения материалов влияют только на JSON/POST; XLSX остаётся полной «сырым» логом категории.
5. Нормализация category.name для JSON (без «Каталог/ZaraHome/» и без мусорных символов).
6. Логи — вместо print, с уровнем подробности через LOG_LEVEL.

