From fe472b08275d4065f04d48120277b11bf3077e01 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Tue, 12 May 2026 07:09:33 +0300 Subject: [PATCH] catalog: filter junk + background refresh + clear endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FILTERING (catalog.py _save_results): - CATEGORY_KEYWORDS: must contain category word ('холодильник', 'варочн', 'духов', etc.) - CATEGORY_MIN_PRICE: filters parts/accessories (fridge >20k, hood >5k, etc.) - PART_BLACKLIST: 'фильтр', 'лампочк', 'термодатчик', 'шланг', 'тэн', 'компрессор', etc. - Previously had Asko light bulb (155₽), Miele dryer filter (376₽), Siemens cooktop in fridge category — all now filtered out ASYNC REFRESH (main.py): - POST /api/catalog/refresh queues background task, returns immediately (was sync, taking 3+ min → Cloudflare tunnel was killing connection) - New GET /api/catalog/refresh_status for progress polling - Concurrent refresh blocked (one at a time) CLEAR ENDPOINT: - POST /api/catalog/clear?cat=fridge clears one category - POST /api/catalog/clear clears entire catalog (start over) NEXT: clear current dirty data, re-seed fridge with filters --- backend-py/app/catalog.py | 107 +++++++++++++++++++++++++++++-- backend-py/app/main.py | 73 ++++++++++++++++----- cat_refresh.json | 131 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+), 21 deletions(-) create mode 100644 cat_refresh.json diff --git a/backend-py/app/catalog.py b/backend-py/app/catalog.py index 06e0fcf..6ce74fe 100644 --- a/backend-py/app/catalog.py +++ b/backend-py/app/catalog.py @@ -49,6 +49,40 @@ CATEGORY_QUERIES = { "washer": "стиральная машина", } +# Ключевые слова, которые ДОЛЖНЫ быть в названии для категории +# (любое из вариантов подойдёт) +CATEGORY_KEYWORDS = { + "fridge": ["холодильник", "морозильник", "морозильная камера", "холодильная камера"], + "hob": ["варочн", "варочная", "плита", "конфорк", "индукцион"], + "oven": ["духов", "духовка", "духовой"], + "dw": ["посудомоеч", "посудомойк"], + "hood": ["вытяжк"], + "microwave": ["микроволнов", "свч"], + "coffee": ["кофемашин", "кофеварк", "эспрессо"], + "washer": ["стиральн", "стиралк"], +} + +# Минимальные цены — отсекают запчасти, аксессуары, мини-приборы +CATEGORY_MIN_PRICE = { + "fridge": 20000, + "hob": 8000, + "oven": 15000, + "dw": 15000, + "hood": 5000, + "microwave": 3000, + "coffee": 5000, + "washer": 15000, +} + +# Чёрный список — запчасти и аксессуары +PART_BLACKLIST = [ + "фильтр", "лампочк", "запчаст", "ручка двер", "термодатчик", "термостат", + "уплотнит", "наклейк", "ткань", "чехол", "коврик", "пылесборник", + "тэн ", "тен ", "шланг", "шкив", "конденсатор", "магнетрон", + "мешок", "пакет для", "вантуз", "колба", "лед-фильтр", + "ремень привода", "помпа", "помп для", "сальник", +] + def refresh_catalog(categories: list[str] | None = None, sources: tuple = ("yamarket", "wb", "citilink"), @@ -116,13 +150,22 @@ def refresh_catalog(categories: list[str] | None = None, def _save_results(cat: str, brand: str, tier: str, query: str, enriched: dict, max_items: int) -> int: - """Сохраняет до max_items релевантных результатов из enriched.""" + """Сохраняет до max_items РЕЛЕВАНТНЫХ результатов из enriched. + + Фильтры: + - бренд должен упоминаться в названии + - название должно содержать слово категории (холодильник / варочн / духов и т.п.) + - цена должна быть выше минимума для категории (отсекает запчасти) + - чёрный список слов: фильтр, лампочка, термодатчик и т.п. + """ if not enriched: return 0 saved = 0 seen_titles = set() sources_priority = ["yamarket", "wb", "citilink", "ozon", "dns"] + cat_keywords = CATEGORY_KEYWORDS.get(cat, []) + min_price = CATEGORY_MIN_PRICE.get(cat, 0) for src in sources_priority: if saved >= max_items: @@ -131,14 +174,33 @@ def _save_results(cat: str, brand: str, tier: str, query: str, if not item or not item.get("title"): continue - # Фильтр релевантности: бренд должен упоминаться в названии или specs.brand - title = (item.get("title") or "").lower() + title_raw = item.get("title", "") + title_lower = title_raw.lower() + + # 1. Бренд должен упоминаться item_brand = (item.get("specs") or {}).get("brand", "").lower() - if brand.lower() not in title and brand.lower() not in item_brand: + if brand.lower() not in title_lower and brand.lower() not in item_brand: + log.debug("Skip (no brand): %s", title_raw[:80]) + continue + + # 2. Слово категории должно быть в названии + if cat_keywords and not any(kw in title_lower for kw in cat_keywords): + log.debug("Skip (wrong category): %s", title_raw[:80]) + continue + + # 3. Не запчасть/аксессуар + if any(bad in title_lower for bad in PART_BLACKLIST): + log.debug("Skip (part/accessory): %s", title_raw[:80]) + continue + + # 4. Цена выше минимума + price = item.get("price_min_rub") + if price and isinstance(price, (int, float)) and price < min_price: + log.debug("Skip (price too low %d < %d): %s", price, min_price, title_raw[:80]) continue # Дедуп по title в рамках одного (cat, brand) - title_key = item["title"][:100].lower().strip() + title_key = title_raw[:100].lower().strip() if title_key in seen_titles: continue seen_titles.add(title_key) @@ -149,7 +211,7 @@ def _save_results(cat: str, brand: str, tier: str, query: str, cat, brand, tier, - item["title"][:250], + title_raw[:250], query, item.get("price_min_rub") or "", item.get("price_max_rub") or "", @@ -165,6 +227,39 @@ def _save_results(cat: str, brand: str, tier: str, query: str, return saved +def clear_catalog(category: str | None = None) -> int: + """Удаляет все записи из каталога (или одной категории). Возвращает кол-во удалённых.""" + try: + ws = sheets.ensure_sheet(SHEET_NAME, HEADERS) + all_rows = ws.get_all_values() + if len(all_rows) <= 1: + return 0 + headers = all_rows[0] + cat_idx = headers.index("category") if "category" in headers else None + + if category and cat_idx is not None: + # Удаляем только строки нужной категории + kept = [headers] + removed = 0 + for r in all_rows[1:]: + if len(r) > cat_idx and r[cat_idx] == category: + removed += 1 + else: + kept.append(r) + ws.clear() + ws.update("A1", kept) + return removed + else: + # Очищаем всё (оставляем только заголовки) + removed = len(all_rows) - 1 + ws.clear() + ws.update("A1", [headers]) + return removed + except Exception as e: + log.warning("clear_catalog failed: %s", e) + return 0 + + def list_catalog(category: str | None = None, tier: str | None = None, brand: str | None = None, limit: int = 200) -> list[dict[str, Any]]: """Читает каталог из Sheets с опциональными фильтрами.""" diff --git a/backend-py/app/main.py b/backend-py/app/main.py index 0229e4e..45ab25b 100644 --- a/backend-py/app/main.py +++ b/backend-py/app/main.py @@ -224,27 +224,70 @@ async def api_proxy_status(): return proxy_pool.pool_status() -@app.post("/api/catalog/refresh") -def api_catalog_refresh(cat: str = "", per_brand: int = 2, delay: float = 1.0): - """Запускает парсинг каталога (медленно — несколько минут на категорию). +from fastapi import BackgroundTasks - Параметры: - cat: одна категория (fridge|hob|oven|dw|hood|microwave|coffee|washer) - или пусто = все 8 (очень долго) - per_brand: сколько моделей сохранять на (brand × category) — default 2 - delay: задержка между запросами к парсерам, сек — default 1.0 - """ - categories = [cat] if cat else None + +_CATALOG_REFRESH_STATUS = {"running": False, "last_result": None, "started_at": None} + + +def _bg_refresh(categories, per_brand, delay): + """Фоновая задача обновления каталога — пишет статус в глобал.""" + import datetime as _dt + _CATALOG_REFRESH_STATUS["running"] = True + _CATALOG_REFRESH_STATUS["started_at"] = _dt.datetime.now(_dt.timezone.utc).isoformat() try: result = catalog.refresh_catalog( categories=categories, - per_brand=max(1, min(per_brand, 5)), - delay_sec=max(0.0, min(delay, 10.0)), + per_brand=per_brand, + delay_sec=delay, ) - return result + _CATALOG_REFRESH_STATUS["last_result"] = result except Exception as e: - log.exception("catalog refresh failed") - return {"ok": False, "error": str(e)} + log.exception("bg catalog refresh failed") + _CATALOG_REFRESH_STATUS["last_result"] = {"ok": False, "error": str(e)} + finally: + _CATALOG_REFRESH_STATUS["running"] = False + + +@app.post("/api/catalog/refresh") +def api_catalog_refresh(background: BackgroundTasks, + cat: str = "", per_brand: int = 2, delay: float = 1.0): + """Запускает refresh в ФОНЕ. Возвращает сразу, статус смотри в /api/catalog/refresh_status. + + Параметры: + cat: одна категория или пусто = все 8 (очень долго) + per_brand: сколько моделей на (brand × category) — default 2 + delay: задержка между запросами, сек — default 1.0 + """ + if _CATALOG_REFRESH_STATUS["running"]: + return {"ok": False, "error": "already running", "started_at": _CATALOG_REFRESH_STATUS["started_at"]} + + categories = [cat] if cat else None + background.add_task( + _bg_refresh, + categories, + max(1, min(per_brand, 5)), + max(0.0, min(delay, 10.0)), + ) + return { + "ok": True, + "queued": True, + "categories": categories or "all", + "hint": "GET /api/catalog/refresh_status — узнать прогресс", + } + + +@app.get("/api/catalog/refresh_status") +def api_catalog_refresh_status(): + """Статус последнего/текущего refresh'а каталога.""" + return _CATALOG_REFRESH_STATUS + + +@app.post("/api/catalog/clear") +def api_catalog_clear(cat: str = ""): + """Удаляет всё содержимое каталога (или одной категории).""" + removed = catalog.clear_catalog(category=cat or None) + return {"ok": True, "removed": removed, "category": cat or "all"} @app.get("/api/catalog/list") diff --git a/cat_refresh.json b/cat_refresh.json new file mode 100644 index 0000000..b79915e --- /dev/null +++ b/cat_refresh.json @@ -0,0 +1,131 @@ + + + + + + + + +prepared-alfred-story-dale.trycloudflare.com | 524: A timeout occurred + + + + + + + + + + +
+
+
+

+ A timeout occurred + Error code 524 +

+
+ Visit cloudflare.com for more information. +
+
2026-05-12 03:36:05 UTC
+
+
+
+
+ +
+
+ + + + +
+ You +

+ + Browser + +

+ Working +
+ +
+ + Stockholm +

+ + Cloudflare + +

+ Working +
+ +
+
+ + + + +
+ prepared-alfred-story-dale.trycloudflare.com +

+ + Host + +

+ Error +
+ +
+
+
+ +
+
+
+

What happened?

+

The origin web server timed out responding to this request.

The likely cause is an overloaded background task, database or application, stressing the resources on the host web server.

+
+
+

What can I do?

+

If you're a visitor of this website:

+

Please try again in a few minutes.

+ +

If you're the owner of this website:

+

Please refer to the Error 524 article:

+
    +
  • Contact your hosting provider; check for long-running processes or an overloaded web server.
  • +
  • Use status polling of large HTTP processes to avoid this error.
  • +
  • Run the long-running scripts on a grey-clouded subdomain.
  • +
  • Enterprise customers can increase the timeout setting globally or for specific requests using Cache Rules.
  • +
+
+
+
+ + + + +
+
+ +