Метод добавления товаров из xlsx с UI

+ ZH - категории теперь лежат в рекордсфолдер
This commit is contained in:
va1is 2025-10-27 11:47:32 +03:00
parent 5d077413c9
commit bd765f8349
13 changed files with 809 additions and 8 deletions

0
API-UI/history.json Normal file
View File

495
API-UI/main.py Normal file
View File

@ -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())

View File

@ -0,0 +1,16 @@
{
"endpoint": "http://localhost:3005/create-brands",
"description": "Импорт списка брендов",
"structure": {
"brands": [
"string"
]
},
"fields": {
"brands": {
"type": "list",
"source": "названия брендов",
"example": ["IKEA", "ORGIE", "PASSION LABS"]
}
}
}

View File

@ -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": "Полный путь категории через ' > '"
}
}
}

View File

@ -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 }
}
}

View File

@ -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)

Binary file not shown.

View File

@ -248,8 +248,6 @@ class Extractor:
if "models" not in model_info or not model_info["models"]:
print(f"Ошибка: нет 'models' для товара {product_url}")
continue

View File

@ -1,6 +1,6 @@
{
"proxy": "",
"request_delay": 2.5,
"request_delay": 0.1,
"request_repeats": 2,
"request_repeat_delay": 3
}

View File

@ -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-теги, кроме <br>"""
if not text:
return ""
text = re.sub(r"(\n\s*){1,}", "<br>", text)
text = re.sub(r"<br\s*/?>\s*\n", "<br>", text, flags=re.IGNORECASE)
text = re.sub(r"\n\s*<br\s*/?>", "<br>", text, flags=re.IGNORECASE)
text = re.sub(r"<br\s*/?>\s*<br\s*/?>", "<br>", text, flags=re.IGNORECASE)
text = re.sub(r"<(?!br\s*/?)[^>]+>", "", text) # удалить всё, кроме <br>
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()

View File

@ -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 = []

View File

@ -118,7 +118,11 @@ class Recorder:
safe_str(row[idx["Параметр: Происхождение"]]).replace("\n", "<br/>")
).strip("<br/>")
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)

3
send_log.txt Normal file
View File

@ -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-файлов.