# xlsx_recorder.py · 2025-07-24
from openpyxl import Workbook
from os.path import isdir, abspath, join
from os import mkdir
import re, json, math, logging, requests, os, sys, pathlib
from pathlib import Path
log = logging.getLogger("recorder")
# ─────────────────────── интерактивные флаги ──────────────────────
def ask(flag: str, default: str = "1") -> bool:
"""Спрашивает 0/1; пустой ввод → default."""
try:
val = input(f"{flag} (1=yes, 0=no) [{default}]: ").strip() or default
except EOFError:
val = default
return val == "1"
SEND_JSON = ask("SEND_JSON") # отправка POST
SAVE_JSON = ask("SAVE_JSON") # сохранять копию JSON
# ─────────────────────── прочие настройки ─────────────────────────
POST_URL = "http://localhost:3005/parser/data"
MIN_PRICE = 40
MAX_PRICE = 1200
EXCLUSION_FILE = Path(__file__).with_name("exclusion_materials.txt") # в том же каталоге
# Только такие статусы допускаем в JSON/POST
ALLOWED_VIS = {"SHOW", "RUNNING_OUT"}
INVALID_FILE_CHARS = r'[<>:"/\\|*?]' # для имён файлов Windows
def sanitize_filename(name: str, repl: str = "_") -> str:
clean = re.sub(INVALID_FILE_CHARS, repl, name)
return clean.split("?", 1)[0].strip()
# ────────────────────────── Recorder ──────────────────────────────
class Recorder:
def __init__(self, records_folder="records_folder"):
rf_abs = abspath(records_folder)
if not isdir(rf_abs):
mkdir(rf_abs)
self.record_folder = rf_abs
self.forbidden = self._load_forbidden()
# ---------- загрузка словаря исключений ----------
def _load_forbidden(self):
path = pathlib.Path(EXCLUSION_FILE)
if not path.is_file():
log.warning("Exclusion file not found: %s (filter disabled)", path)
return set()
txt = path.read_text(encoding="utf-8")
# ищем элементы в кавычках или делим по запятой
tokens = re.findall(r'"([^"]+)"', txt) or [t.strip() for t in txt.split(",")]
cleaned = {t.lower() for t in tokens if t}
log.info("Loaded %s exclusion tokens", len(cleaned))
return cleaned
# ---------------- запись таблицы + JSON ----------------
def record(self, csv_name, table):
csv_name = sanitize_filename(csv_name)
# 1) сохраняем XLSX
wb = Workbook()
ws = wb.active
for row in table:
ws.append(row)
xlsx_path = join(self.record_folder, f"{csv_name}.xlsx")
wb.save(xlsx_path)
log.info("XLSX saved → %s", xlsx_path)
# индексы столбцов
headers = table[0]
idx = {h: i for i, h in enumerate(headers)}
items = []
for row in table[1:]:
# ----------- фильтр цены -----------
price_raw = row[idx["Цена закупки"]]
price_int = math.ceil(float(price_raw))
if not (MIN_PRICE <= price_int <= MAX_PRICE):
# вне ценового коридора — остаётся только в XLSX
continue
# ----------- фильтр статуса наличия --------
vis_val = row[idx["Наличие на сайте"]]
#Закомментить это если нужно выключить (начало)-------------------------------#
if vis_val not in ALLOWED_VIS: #
# любые статусы кроме SHOW/RUNNING_OUT — только в XLSX #
log.debug("Skip by availability: %s (%s)", row[idx["Артикул"]], vis_val) #
continue #
#Закомментить это если нужно выключить (конец)--------------------------------#
# ----------- фильтр по составу -----------
comp_txt = row[idx["Параметр: Состав"]].replace("\n", "
")
comp_low = comp_txt.lower()
if any(tok in comp_low for tok in self.forbidden):
log.debug("Skip by exclusion token: %s", row[idx["Артикул"]])
continue
# ----------- формируем variant -----------
article = row[idx["Артикул"]]
partnumber = row[idx["PartNumber"]]
clr_name = row[idx["Свойство: Цвет"]].capitalize()
size_full = row[idx["Свойство: Размер"]].replace("\n", "
")
vis = vis_val
weight_g = float(row[idx["Свойство: Вес(г)"]]) if row[idx["Свойство: Вес(г)"]] else 0.0
weight_kg = math.ceil(weight_g / 1000) if weight_g else 0
url_full = row[idx["Краткое описание"]]
name_orig = row[idx["Название товара или услуги"]].capitalize()
desc_orig = (
row[idx["Полное описание"]].replace("\n", "
") + "
" +
row[idx["Параметр: Уход"]].replace("\n", "
") + "
" +
row[idx["Параметр: Происхождение"]].replace("\n", "
")
).strip("
")
images = [img for img in row[idx["Изображения варианта"]].split("\n") if img]
cat_raw = row[idx["Размещение на сайте"]].replace("Каталог/ZaraHome/", "")
category_name = re.sub(r"[^\w/-]+|_+", "_", cat_raw)
variant = {
"status_id": 1,
"color": clr_name,
"sku": f"{article}-{partnumber}",
"size": size_full,
"cost": price_int,
"originalUrl": url_full,
"originalName": name_orig,
"originalDescription": desc_orig,
"originalComposition": comp_txt,
"images": images,
"inStock": vis in ALLOWED_VIS, # ← здесь привязали к ALLOWED_VIS
"weight": weight_kg
}
items.append({
"category": {"name": category_name},
"variant": variant,
"brand": {"name": "zara-home"}
})
payload = {"items": items, "parserName": "zara-home"}
# 3) сохраняем JSON
if SAVE_JSON:
json_path = join(self.record_folder, f"{csv_name}.json")
with open(json_path, "w", encoding="utf-8") as fh:
json.dump(payload, fh, ensure_ascii=False, indent=2)
log.info("JSON saved → %s", json_path)
# 4) отправляем POST
if SEND_JSON:
try:
resp = requests.post(POST_URL, json=payload, timeout=20)
resp.raise_for_status()
log.info("POST %s OK (%s items)", csv_name, len(items))
except Exception as err:
log.warning("POST %s FAILED: %s", csv_name, err)