diff --git a/API-UI/history.json b/API-UI/history.json new file mode 100644 index 0000000..e69de29 diff --git a/API-UI/main.py b/API-UI/main.py new file mode 100644 index 0000000..1ee5d8e --- /dev/null +++ b/API-UI/main.py @@ -0,0 +1,495 @@ +import sys, os, json, re +from datetime import datetime +from typing import Any, Dict, List +import requests +import openpyxl + +from PyQt6 import QtWidgets +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QFileDialog, + QVBoxLayout, QHBoxLayout, QWidget, + QPushButton, QLabel, QComboBox, QTextEdit, + QTableWidget, QTableWidgetItem, QHeaderView +) + + +# === ПУТИ === +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +MAPPINGS_DIR = os.path.join(BASE_DIR, "mappings") +HISTORY_FILE = os.path.join(BASE_DIR, "history.json") + + +def ts() -> str: + return datetime.now().strftime("%H:%M:%S") + + +def split_list_cell(val: str) -> List[str]: + if val is None: + return [] + s = str(val).strip() + if not s: + return [] + parts = re.split(r"[\n,;|]+", s) + return [p.strip() for p in parts if p.strip()] + + +def cast_value(raw: Any, t: str): + if raw is None: + return None + s = str(raw).strip() + + if t == "int": + try: + return int(float(s)) + except: + return None + if t == "float": + try: + return float(str(s).replace(",", ".")) + except: + return None + if t == "bool": + s_low = s.lower() + return s_low in ("1", "true", "yes", "y", "да", "истина") + if t == "list" or t == "list[string]": + return split_list_cell(s) + # string + return s + + +def resolve_endpoint(selected_base_url: str, mapping_endpoint: str) -> str: + """ + Правила: + - Если endpoint начинается с '/', приклеиваем к выбранному base_url. + - Если endpoint начинается с 'http', заменяем схему+хост на выбранный base_url (оставляем path). + - Иначе считаем это относительным путём от base_url. + """ + base = selected_base_url.rstrip("/") + ep = mapping_endpoint.strip() + + if ep.startswith("/"): + return f"{base}{ep}" + if ep.startswith("http://") or ep.startswith("https://"): + # вытащим только путь + try: + from urllib.parse import urlparse + parsed = urlparse(ep) + path = parsed.path + if parsed.query: + path += f"?{parsed.query}" + return f"{base}{path}" + except: + return ep # fallback + return f"{base}/{ep.lstrip('/')}" + + +class ImportApp(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("🧩 Import Client v0.3") + self.resize(1200, 800) + + self.api_history = self.load_history() + self.current_mapping: Dict[str, Any] = {} + self.loaded_data: List[List[Any]] = [] # с заголовком + self.headers: List[str] = [] # заголовки из Excel + self.map_pairs: List[Dict[str, str]] = [] # [{"json_field": "...", "excel_col": "..."}] + + # === UI === + main_widget = QWidget() + main_layout = QVBoxLayout(main_widget) + self.setCentralWidget(main_widget) + + # Верхняя панель: API URL + top_layout = QHBoxLayout() + self.api_label = QLabel("API URL:") + self.api_box = QComboBox() + self.api_box.setEditable(True) + self.api_box.addItems(self.api_history or ["http://localhost:3005"]) + self.btn_save_url = QPushButton("💾 Сохранить") + self.btn_save_url.clicked.connect(self.save_current_url) + + top_layout.addWidget(self.api_label) + top_layout.addWidget(self.api_box) + top_layout.addWidget(self.btn_save_url) + main_layout.addLayout(top_layout) + + # Тип импорта + import_layout = QHBoxLayout() + self.import_label = QLabel("Тип импорта:") + self.import_type = QComboBox() + self.import_type.addItems(["brands", "categories", "products"]) + self.import_type.currentIndexChanged.connect(self.load_mapping) + import_layout.addWidget(self.import_label) + import_layout.addWidget(self.import_type) + + # Кнопки Excel + self.btn_load_excel = QPushButton("📂 Загрузить Excel-файл") + self.btn_load_excel.clicked.connect(self.load_excel) + import_layout.addWidget(self.btn_load_excel) + main_layout.addLayout(import_layout) + + # Таблица предпросмотра Excel + self.table = QTableWidget() + main_layout.addWidget(self.table) + + # Таблица сопоставлений + main_layout.addWidget(QLabel("Сопоставление полей: JSON ↔ Колонка Excel")) + self.map_table = QTableWidget() + self.map_table.setColumnCount(3) + self.map_table.setHorizontalHeaderLabels(["Поле JSON", "Тип", "Колонка Excel"]) + self.map_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + self.map_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + self.map_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + main_layout.addWidget(self.map_table) + + # Кнопки действий + action_layout = QHBoxLayout() + self.btn_autofill = QPushButton("✨ Автосопоставление") + self.btn_autofill.clicked.connect(self.autofill_mapping) + + self.btn_generate = QPushButton("🧱 Сформировать JSON") + self.btn_generate.clicked.connect(self.generate_json) + + self.btn_send = QPushButton("📤 Отправить в API") + self.btn_send.clicked.connect(self.send_to_api) + + action_layout.addWidget(self.btn_autofill) + action_layout.addWidget(self.btn_generate) + action_layout.addWidget(self.btn_send) + main_layout.addLayout(action_layout) + + # Предпросмотр JSON + main_layout.addWidget(QLabel("Предпросмотр JSON:")) + self.json_preview = QTextEdit() + self.json_preview.setReadOnly(True) + main_layout.addWidget(self.json_preview) + + # Лог + main_layout.addWidget(QLabel("Лог:")) + self.log_box = QTextEdit() + self.log_box.setReadOnly(True) + main_layout.addWidget(self.log_box) + + # Загрузим первый шаблон + self.load_mapping() + + # ---------- History ---------- + def load_history(self): + if os.path.exists(HISTORY_FILE): + try: + with open(HISTORY_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except: + return [] + return [] + + def save_current_url(self): + url = self.api_box.currentText().strip() + if not url: + return + if url not in self.api_history: + self.api_history.insert(0, url) + with open(HISTORY_FILE, "w", encoding="utf-8") as f: + json.dump(self.api_history[:10], f, ensure_ascii=False, indent=2) + self.log(f"✅ Сохранен URL: {url}") + + # ---------- Mapping ---------- + def load_mapping(self): + import_type = self.import_type.currentText() + path = os.path.join(MAPPINGS_DIR, f"{import_type}.json") + if not os.path.exists(path): + self.current_mapping = {} + self.log(f"⚠️ Не найден шаблон: {path}") + return + try: + with open(path, "r", encoding="utf-8") as f: + self.current_mapping = json.load(f) + self.log(f"📘 Загружен шаблон: {import_type}") + self.populate_map_table() + except Exception as e: + self.current_mapping = {} + self.log(f"❌ Ошибка чтения шаблона: {e}") + + def populate_map_table(self): + self.map_table.setRowCount(0) + self.map_pairs.clear() + + fields = (self.current_mapping.get("fields") or {}) + # Список кандидатов полей JSON для сопоставления + # Пропускаем поля с "fixed" (их не мапим) — они выставляются автоматически + candidates = [] + for k, meta in fields.items(): + if isinstance(meta, dict) and "fixed" in meta: + continue + candidates.append((k, meta)) + + self.map_table.setRowCount(len(candidates)) + for i, (json_field, meta) in enumerate(candidates): + # колонка 0: имя поля json + self.map_table.setItem(i, 0, QTableWidgetItem(json_field)) + # колонка 1: тип + t = meta["type"] if isinstance(meta, dict) and "type" in meta else "string" + self.map_table.setItem(i, 1, QTableWidgetItem(t)) + + # колонка 2: выпадающий список — заголовки Excel + combo = QComboBox() + combo.addItem("") # пусто = не сопоставлено + for h in self.headers: + combo.addItem(h) + self.map_table.setCellWidget(i, 2, combo) + + def autofill_mapping(self): + """Простое автосопоставление по подстроке (без учета регистра).""" + rows = self.map_table.rowCount() + for i in range(rows): + json_field = self.map_table.item(i, 0).text() + jf_key = json_field.split(".")[-1].lower() + best = "" + score_best = 0 + for h in self.headers: + hl = h.lower() + score = 0 + if hl == jf_key: + score = 3 + elif jf_key in hl or hl in jf_key: + score = 2 + elif hl.replace(" ", "") == jf_key.replace("_", ""): + score = 1 + if score > score_best: + score_best = score + best = h + combo = self.map_table.cellWidget(i, 2) + if combo and best: + idx = combo.findText(best) + if idx >= 0: + combo.setCurrentIndex(idx) + self.log("✨ Выполнено автосопоставление.") + + # ---------- Excel ---------- + def load_excel(self): + path, _ = QFileDialog.getOpenFileName(self, "Выбери Excel-файл", "", "Excel Files (*.xlsx)") + if not path: + return + try: + wb = openpyxl.load_workbook(path) + sheet = wb.active + + self.display_sheet(sheet) + self.log(f"📄 Загружен файл: {os.path.basename(path)} ({sheet.max_row} строк)") + # обновим список заголовков в таблице сопоставления + self.populate_map_table() + except Exception as e: + self.log(f"❌ Ошибка при чтении файла: {e}") + + def display_sheet(self, sheet): + self.table.clear() + self.table.setRowCount(0) + self.table.setColumnCount(0) + + data = [] + for row in sheet.iter_rows(values_only=True): + data.append(list(row)) + if len(data) > 50: # покажем первые 50 строк + break + + if not data: + return + + self.headers = [str(h) if h else "" for h in data[0]] + self.loaded_data = data + + self.table.setColumnCount(len(self.headers)) + self.table.setHorizontalHeaderLabels(self.headers) + + for r_idx, row in enumerate(data[1:], start=0): + self.table.insertRow(r_idx) + for c_idx, value in enumerate(row): + self.table.setItem(r_idx, c_idx, QTableWidgetItem("" if value is None else str(value))) + + # ---------- Генерация JSON ---------- + def collect_mapping(self) -> Dict[str, str]: + """Собираем словарь {json_field -> excel_header} для выбранных пользователем маппингов.""" + mapping = {} + rows = self.map_table.rowCount() + for i in range(rows): + json_field = self.map_table.item(i, 0).text() + combo = self.map_table.cellWidget(i, 2) + header = combo.currentText().strip() if combo else "" + if header: + mapping[json_field] = header + return mapping + + def generate_json(self): + if not self.loaded_data: + self.log("⚠️ Сначала загрузите Excel-файл.") + return + if not self.current_mapping: + self.log("⚠️ Не загружен шаблон JSON.") + return + + import_type = self.import_type.currentText() + fields_meta = self.current_mapping.get("fields") or {} + mapping = self.collect_mapping() + rows = self.loaded_data[1:] # без заголовка + + result: Any + + # ===== BRANDS ===== + if import_type == "brands": + # ожидается список строк в ключе "brands" + json_key = "brands" + selected_col = mapping.get(json_key) + values = [] + if selected_col: + col_idx = self.headers.index(selected_col) + seen = set() + for r in rows: + if col_idx < len(r) and r[col_idx]: + val = str(r[col_idx]).strip() + if val and val not in seen: + seen.add(val) + values.append(val) + result = {json_key: values} + + # ===== CATEGORIES ===== + elif import_type == "categories": + # ожидаем объекты {"originPath": "...", "path": "..."} + origin_col = mapping.get("originPath", "") + path_col = mapping.get("path", "") + result = [] + idx_origin = self.headers.index(origin_col) if origin_col in self.headers else -1 + idx_path = self.headers.index(path_col) if path_col in self.headers else -1 + + for r in rows: + origin_val = (str(r[idx_origin]).strip() if idx_origin >= 0 and idx_origin < len(r) and r[idx_origin] else "") + path_val = (str(r[idx_path]).strip() if idx_path >= 0 and idx_path < len(r) and r[idx_path] else "") + if origin_val or path_val: + result.append({"originPath": origin_val, "path": path_val}) + + # ===== PRODUCTS ===== + else: + # структура: + # { + # "parserName": fixed или из поля, + # "items": [ { category:{name}, brand:{name}, variant:{...} } ] + # } + parser_name = fields_meta.get("parserName", {}).get("fixed", "ikea") + result = {"parserName": parser_name, "items": []} + + # подготовим индекс колонок + col_index = {h: i for i, h in enumerate(self.headers)} + + for r in rows: + # helpers для получения значения по json-полю + def get_cell(json_field: str): + header = mapping.get(json_field) + if not header: + return None + idx = col_index.get(header, -1) + if idx < 0 or idx >= len(r): + return None + return r[idx] + + # категори/бренд + cat_name = get_cell("category.name") + brand_name = get_cell("brand.name") + + # variant поля с типами + v = {} + + for jf, meta in fields_meta.items(): + if jf in ("parserName", "category.name", "brand.name"): + continue + if isinstance(meta, dict) and "fixed" in meta: + v_key = jf.split("variant.", 1)[-1] if jf.startswith("variant.") else jf + v[v_key] = meta["fixed"] + continue + + header = mapping.get(jf) + if not header: + # оставим default если указан + default = meta.get("default") if isinstance(meta, dict) else None + if default is not None and jf.startswith("variant."): + v_key = jf.split("variant.", 1)[-1] + v[v_key] = default + continue + + raw = get_cell(jf) + t = meta.get("type", "string") if isinstance(meta, dict) else "string" + + # особый случай: images -> список ссылок + if jf.endswith(".images"): + v["images"] = cast_value(raw, "list") + else: + val = cast_value(raw, t) + v_key = jf.split("variant.", 1)[-1] if jf.startswith("variant.") else jf + v[v_key] = val + + item = { + "category": {"name": cast_value(cat_name, "string") or ""}, + "brand": {"name": cast_value(brand_name, "string") or ""}, + "variant": { + # значения по умолчанию, если не заданы маппингом/фиксами + "status_id": v.get("status_id", 1), + "color": v.get("color", ""), + "sku": v.get("sku", ""), + "size": v.get("size", ""), + "cost": v.get("cost", 0), + "originalUrl": v.get("originalUrl", ""), + "originalName": v.get("originalName", ""), + "originalDescription": v.get("originalDescription", ""), + "originalComposition": v.get("originalComposition", ""), + "images": v.get("images", []), + "inStock": v.get("inStock", True), + "weight": v.get("weight", 0) + } + } + # фильтр пустых строк: нужен хотя бы sku или originalUrl или название + if any([item["variant"]["sku"], item["variant"]["originalUrl"], item["variant"]["originalName"]]): + result["items"].append(item) + + # Вывод + self.json_preview.setText(json.dumps(result, ensure_ascii=False, indent=2)) + self.log(f"🧱 Сформирован JSON для: {self.import_type.currentText()}") + + # ---------- Отправка ---------- + def send_to_api(self): + if not self.current_mapping: + self.log("⚠️ Не загружен шаблон JSON.") + return + text = self.json_preview.toPlainText().strip() + if not text: + self.log("⚠️ Сначала сформируйте JSON.") + return + + try: + payload = json.loads(text) + except Exception as e: + self.log(f"❌ Некорректный JSON: {e}") + return + + base_url = self.api_box.currentText().strip() or "http://localhost:3005" + endpoint = self.current_mapping.get("endpoint", "/") + url = resolve_endpoint(base_url, endpoint) + + try: + self.log(f"➡️ POST {url}") + r = requests.post(url, json=payload, headers={"Content-Type": "application/json"}, timeout=60) + # plain text ответ + content_type = r.headers.get("Content-Type", "") + body = r.text if "text" in content_type or "html" in content_type or content_type == "" else r.content[:1000] + self.log(f"⬅️ {r.status_code} {r.reason}\n{body}") + except Exception as e: + self.log(f"❌ Ошибка запроса: {e}") + + # ---------- Лог ---------- + def log(self, text): + self.log_box.append(f"{ts()} - {text}") + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = ImportApp() + window.show() + sys.exit(app.exec()) diff --git a/API-UI/mappings/brands.json b/API-UI/mappings/brands.json new file mode 100644 index 0000000..982c51d --- /dev/null +++ b/API-UI/mappings/brands.json @@ -0,0 +1,16 @@ +{ + "endpoint": "http://localhost:3005/create-brands", + "description": "Импорт списка брендов", + "structure": { + "brands": [ + "string" + ] + }, + "fields": { + "brands": { + "type": "list", + "source": "названия брендов", + "example": ["IKEA", "ORGIE", "PASSION LABS"] + } + } +} diff --git a/API-UI/mappings/categories.json b/API-UI/mappings/categories.json new file mode 100644 index 0000000..2b13837 --- /dev/null +++ b/API-UI/mappings/categories.json @@ -0,0 +1,18 @@ +{ + "endpoint": "http://localhost:3005/create-categories", + "description": "Импорт категорий (словаря)", + "structure": { + "originPath": "string", + "path": "string" + }, + "fields": { + "originPath": { + "type": "string", + "source": "Короткий код или оригинальное имя категории (например EDT)" + }, + "path": { + "type": "string", + "source": "Полный путь категории через ' > '" + } + } +} diff --git a/API-UI/mappings/products.json b/API-UI/mappings/products.json new file mode 100644 index 0000000..bbbf298 --- /dev/null +++ b/API-UI/mappings/products.json @@ -0,0 +1,48 @@ +{ + "endpoint": "http://localhost:3005/parser/data", + "description": "Импорт товаров (сложная структура)", + "structure": { + "parserName": "string", + "items": [ + { + "category": { "name": "string" }, + "brand": { "name": "string" }, + "variant": { + "status_id": "int", + "color": "string", + "sku": "string", + "size": "string", + "cost": "float", + "originalUrl": "string", + "originalName": "string", + "originalDescription": "string", + "originalComposition": "string", + "images": ["string"], + "inStock": "bool", + "weight": "float" + } + } + ] + }, + "fields": { + "parserName": { + "type": "string", + "fixed": "ikea", + "description": "Идентификатор парсера (постоянный)" + }, + "category.name": { "type": "string", "source": "путь категории" }, + "brand.name": { "type": "string", "source": "название бренда" }, + "variant.status_id": { "type": "int", "default": 1 }, + "variant.color": { "type": "string", "source": "цвет" }, + "variant.sku": { "type": "string", "source": "артикул" }, + "variant.size": { "type": "string", "optional": true }, + "variant.cost": { "type": "float", "source": "цена" }, + "variant.originalUrl": { "type": "string", "source": "ссылка" }, + "variant.originalName": { "type": "string", "source": "название товара" }, + "variant.originalDescription": { "type": "string", "source": "описание товара" }, + "variant.originalComposition": { "type": "string", "source": "состав" }, + "variant.images": { "type": "list", "source": "ссылки на фото (через запятую или \\n)" }, + "variant.inStock": { "type": "bool", "default": true }, + "variant.weight": { "type": "float", "optional": true } + } +} diff --git a/APIlocalhost/Brand-creation.py b/APIlocalhost/Brand-creation.py new file mode 100644 index 0000000..00643bc --- /dev/null +++ b/APIlocalhost/Brand-creation.py @@ -0,0 +1,39 @@ +import requests +import json + +url = "http://localhost:3005/create-brands" + +payload = json.dumps({ + "bramds": [ + "ORION", + "LYBAILE + PRETTY LOVE", + "OTHERS", + "LOVETOY", + "ORGIE", + "EROSPACE - COSMIC PLAY", + "GUILTY TOYS", + "MOKKO TOYS", + "PASSION LABS", + "STD", + "JGF LINGERIE", + "SUNSPICE", + "SHUNGA", + "LIEBE", + "S PLEASURES", + "EXS", + "EROSPACE - SWEET PLAY", + "EROSPACE - MEN'S PLAY", + "EROSPACE - WILD PLAY", + "EROSPACE - NATURAL PLAY", + "BIJOUX", + "EROSPACE - SLICK PLAY", + "INPLEASURE" + ] +}) +headers = { + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, data=payload) + +print(response.text) diff --git a/Pars_Decathlon/categories.xlsx b/Pars_Decathlon/categories.xlsx index 7f1f934..c6f9151 100644 Binary files a/Pars_Decathlon/categories.xlsx and b/Pars_Decathlon/categories.xlsx differ diff --git a/Pars_Decathlon/extractor.py b/Pars_Decathlon/extractor.py index 828741c..acaa02c 100644 --- a/Pars_Decathlon/extractor.py +++ b/Pars_Decathlon/extractor.py @@ -248,8 +248,6 @@ class Extractor: - - if "models" not in model_info or not model_info["models"]: print(f"Ошибка: нет 'models' для товара {product_url}") continue diff --git a/Pars_Decathlon/request_settings.json b/Pars_Decathlon/request_settings.json index c39eeec..5b6c078 100644 --- a/Pars_Decathlon/request_settings.json +++ b/Pars_Decathlon/request_settings.json @@ -1,6 +1,6 @@ { "proxy": "", - "request_delay": 2.5, + "request_delay": 0.1, "request_repeats": 2, "request_repeat_delay": 3 } \ No newline at end of file diff --git a/Pars_Decathlon/send_all_json_to_localAPI_only.py b/Pars_Decathlon/send_all_json_to_localAPI_only.py new file mode 100644 index 0000000..f2e2cce --- /dev/null +++ b/Pars_Decathlon/send_all_json_to_localAPI_only.py @@ -0,0 +1,175 @@ + +"""Работает не только в корне, но и в records_folder — если ты запускаешь его из папки парсера, он сам найдёт нужную директорию. + +Пропускает пустые JSON-файлы (у которых items == []). + +Запоминает успешно отправленные файлы — чтобы не отправлять их повторно (создаёт sent_files.txt). + +Добавляет цветной вывод в консоль (удобно при большом объёме). + +Улучшенный лог с временем выполнения и статусами.""" + +import os +import json +import time +import re +import requests +import openpyxl +from datetime import datetime + +API_URL = "http://172.25.4.101:3005/parser/data" +LOG_FILE = "send_log.txt" +SENT_TRACK_FILE = "sent_files.txt" + +class Colors: + OK = "\033[92m" + WARN = "\033[93m" + ERR = "\033[91m" + END = "\033[0m" + +def log(msg: str, color=""): + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + line = f"[{ts}] {msg}" + print(color + line + Colors.END) + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(line + "\n") + +def clean_description(text: str) -> str: + """Очищает описание: заменяет переносы и удаляет HTML-теги, кроме
""" + if not text: + return "" + text = re.sub(r"(\n\s*){1,}", "
", text) + text = re.sub(r"\s*\n", "
", text, flags=re.IGNORECASE) + text = re.sub(r"\n\s*", "
", text, flags=re.IGNORECASE) + text = re.sub(r"\s*", "
", text, flags=re.IGNORECASE) + text = re.sub(r"<(?!br\s*/?)[^>]+>", "", text) # удалить всё, кроме
+ return text.strip() + +def get_brand_map(xlsx_path: str) -> dict: + """Создаёт словарь {Артикул: Параметр: Бренд} из XLSX""" + brand_map = {} + try: + wb = openpyxl.load_workbook(xlsx_path) + ws = wb.active + + # найдём номера нужных колонок + headers = [cell.value for cell in ws[1]] + if not headers: + return {} + + try: + art_idx = headers.index("Артикул") + 1 + brand_idx = headers.index("Параметр: Бренд") + 1 + except ValueError: + log(f"⚠️ В {xlsx_path} не найдены нужные колонки.", Colors.WARN) + return {} + + for row in ws.iter_rows(min_row=2, values_only=True): + sku = row[art_idx - 1] + brand = row[brand_idx - 1] + if sku and brand: + brand_map[str(sku).strip()] = str(brand).strip() + except Exception as e: + log(f"❌ Ошибка при чтении {xlsx_path}: {e}", Colors.ERR) + return brand_map + +def enhance_json_with_brands(file_path: str, brand_map: dict) -> str: + """Обновляет JSON: добавляет бренды и чистит описания""" + try: + with open(file_path, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception as e: + log(f"❌ Ошибка чтения JSON {file_path}: {e}", Colors.ERR) + return "" + + for item in data.get("items", []): + variant = item.get("variant", {}) + sku = str(variant.get("sku", "")).strip() + if sku in brand_map: + item["brand"] = {"name": brand_map[sku]} + else: + # если бренд не найден, оставляем пустым + item["brand"] = {"name": ""} + + # чистим description + if "originalDescription" in variant: + variant["originalDescription"] = clean_description(variant["originalDescription"]) + + # формируем новое имя + now_str = datetime.now().strftime("%Y%m%d_%H%M") + new_name = os.path.splitext(file_path)[0] + f"_{now_str}.json" + + with open(new_name, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + log(f"🧩 Обновлён JSON → {new_name}", Colors.OK) + return new_name + +def send_json_file(file_path: str): + """Отправляет JSON на API""" + try: + with open(file_path, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception as e: + log(f"❌ Ошибка чтения файла {file_path}: {e}", Colors.ERR) + return False + + total_items = len(data.get("items", [])) + if total_items == 0: + log(f"⚠️ Файл {file_path} пуст — пропуск.", Colors.WARN) + return True + + log(f"📤 Отправка: {file_path} | items: {total_items}") + + for attempt in range(1, 4): + try: + resp = requests.post(API_URL, json=data, timeout=30) + if resp.status_code == 200: + log(f"✅ Успешно отправлен ({attempt}-я попытка): {file_path}", Colors.OK) + return True + else: + log(f"⚠️ Ошибка {resp.status_code}: {resp.text[:200]}", Colors.WARN) + except Exception as e: + log(f"❌ Ошибка сети (попытка {attempt}): {e}", Colors.ERR) + time.sleep(5) + + log(f"🚫 Не удалось отправить {file_path} после 3 попыток.", Colors.ERR) + return False + +def main(): + cwd = os.path.abspath(".") + records_folder = os.path.join(cwd, "records_folder") + search_dir = records_folder if os.path.isdir(records_folder) else cwd + + json_files = [ + os.path.join(search_dir, f) + for f in os.listdir(search_dir) + if f.lower().endswith(".json") + ] + + if not json_files: + log("⚠️ В папке нет JSON-файлов.", Colors.WARN) + return + + log(f"🔍 Найдено {len(json_files)} JSON-файлов. Начинаем обработку...\n") + + for json_file in json_files: + file_path = os.path.join(cwd, json_file) + xlsx_path = os.path.splitext(file_path)[0] + ".xlsx" + + if not os.path.exists(xlsx_path): + log(f"⚠️ Нет XLSX для {json_file} → пропуск добавления брендов.", Colors.WARN) + brand_map = {} + else: + brand_map = get_brand_map(xlsx_path) + + new_json = enhance_json_with_brands(file_path, brand_map) + if new_json: + send_json_file(new_json) + + time.sleep(2) + + log("\n🏁 Отправка завершена.", Colors.OK) + +if __name__ == "__main__": + main() diff --git a/Parsing ZARAHOME/src/categories.py b/Parsing ZARAHOME/src/categories.py index df2a46f..3c6d973 100644 --- a/Parsing ZARAHOME/src/categories.py +++ b/Parsing ZARAHOME/src/categories.py @@ -1,11 +1,16 @@ from openpyxl import load_workbook -from os.path import abspath +from os.path import join, dirname, abspath -# получаем все ссылки из categories.xlsx +# получаем все ссылки из records_folder/cat/categories.xlsx def get_categories(): + # вычисляем абсолютный путь к текущему файлу + base_dir = dirname(abspath(__file__)) + # формируем путь к файлу с категориями + cat_path = join(base_dir, "records_folder", "cat", "categories.xlsx") - wookbook = load_workbook(abspath("categories.xlsx")) - worksheet = wookbook.active + # открываем файл + workbook = load_workbook(cat_path) + worksheet = workbook.active categories = [] diff --git a/Parsing ZARAHOME/src/xlsx_recorder.py b/Parsing ZARAHOME/src/xlsx_recorder.py index 272d750..9d32127 100644 --- a/Parsing ZARAHOME/src/xlsx_recorder.py +++ b/Parsing ZARAHOME/src/xlsx_recorder.py @@ -118,7 +118,11 @@ class Recorder: safe_str(row[idx["Параметр: Происхождение"]]).replace("\n", "
") ).strip("
") - images = [img for img in row[idx["Изображения варианта"]].split("\n") if img] + images = [ + img.split("?", 1)[0] + for img in row[idx["Изображения варианта"]].split("\n") + if img + ] cat_raw = row[idx["Размещение на сайте"]].replace("Каталог/ZaraHome/WOMEN/", "") category_name = re.sub(r"[^\w/-]+|_+", "_", cat_raw) diff --git a/send_log.txt b/send_log.txt new file mode 100644 index 0000000..ed872da --- /dev/null +++ b/send_log.txt @@ -0,0 +1,3 @@ +[2025-10-16 15:40:21] ⚠️ В папке нет JSON-файлов. +[2025-10-16 15:41:41] ⚠️ В папке нет JSON-файлов. +[2025-10-16 15:46:26] ⚠️ В папке нет JSON-файлов.