mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 15:44:47 +00:00
wb: API v9 → v18 (WB сменил endpoint и структуру) + brand+category fallback query
DISCOVERED in real test:
- WB API v9 (/exactmatch/ru/common/v9/search) теперь возвращает только метаданные
(name, query, shardKey, filters, search_result={}) — products пусто
- WB API v18 (/exactmatch/ru/common/v18/search) — рабочий
Структура: {metadata, products, total} — products НА ВЕРХНЕМ уровне (не data.products)
- Подтверждено: query='Haier холодильник' → 100 products via v18
CHANGES:
1. _SEARCH_URL → v18 endpoint
2. Парсинг products: сначала data.products (legacy fallback), потом products top-level
3. _build_item: цены теперь читаются из sizes[].price.{product, total, basic}
(v18 формат), с fallback на priceU/salePriceU (v9 legacy)
4. _generate_query_variants: добавлен brand+category fallback
('Bosch холодильник' если не нашли по модели)
TEST: Haier холодильник → 100 results (first: 'Холодильник двухкамерный C2F619CFU1')
This commit is contained in:
parent
555c5568ff
commit
e9b0db6772
@ -16,13 +16,14 @@ from .. import proxy_pool
|
||||
|
||||
log = logging.getLogger("zov.parser.wb")
|
||||
|
||||
_SEARCH_URL = "https://search.wb.ru/exactmatch/ru/common/v9/search"
|
||||
# WB обновил API: v9 → v18 (2026). Новый формат: products на верхнем уровне.
|
||||
_SEARCH_URL = "https://search.wb.ru/exactmatch/ru/common/v18/search"
|
||||
_DEFAULT_PARAMS = {
|
||||
"TestGroup": "no_test",
|
||||
"TestID": "no_test",
|
||||
"appType": "1",
|
||||
"curr": "rub",
|
||||
"dest": "-1257786", # Москва, можно поменять
|
||||
"dest": "-1257786", # Москва (можно подменить, нам это не критично)
|
||||
"resultset": "catalog",
|
||||
"sort": "popular",
|
||||
"spp": "30",
|
||||
@ -54,26 +55,40 @@ def search_wb(query: str, limit: int = 3, timeout: float = 12.0,
|
||||
|
||||
|
||||
def _generate_query_variants(query: str) -> list[str]:
|
||||
"""Из 'Bosch Serie 4 KGN39NW00R холодильник' делаем варианты:
|
||||
1. Bosch Serie 4 KGN39NW00R холодильник
|
||||
2. Bosch KGN39NW00R
|
||||
3. KGN39NW00R
|
||||
4. Bosch holodilnik
|
||||
"""Из 'Bosch Serie 4 KGN39NW00R холодильник' делаем варианты от specific к general:
|
||||
1. Bosch Serie 4 KGN39NW00R холодильник (исходный)
|
||||
2. Bosch KGN39NW00R (brand + model)
|
||||
3. KGN39NW00R (только индекс)
|
||||
4. Bosch холодильник (brand + category — последний шанс)
|
||||
"""
|
||||
import re
|
||||
variants = [query]
|
||||
parts = query.split()
|
||||
# Находим модель-индекс (с цифрами и буквами)
|
||||
# Находим модель-индекс (буквы + цифры, длина ≥4)
|
||||
model_idx = None
|
||||
for p in parts:
|
||||
if re.search(r"\d", p) and re.search(r"[a-zA-Z]", p) and len(p) >= 4:
|
||||
model_idx = p
|
||||
break
|
||||
brand = parts[0] if parts else ""
|
||||
|
||||
# Категории-ключевые слова на русском
|
||||
cat_words = {
|
||||
"холодильник", "холодильника", "варочная", "духовой", "духовка", "плита",
|
||||
"посудомоечная", "вытяжка", "микроволновая", "свч", "кофемашина", "стиральная",
|
||||
"морозильник", "морозильная",
|
||||
}
|
||||
cat_in_query = next((w for w in parts if w.lower() in cat_words), None)
|
||||
|
||||
if brand and model_idx:
|
||||
variants.append(f"{brand} {model_idx}")
|
||||
variants.append(model_idx)
|
||||
return list(dict.fromkeys(variants)) # дедуп с сохранением порядка
|
||||
|
||||
# Brand + category — широкий fallback
|
||||
if brand and cat_in_query and f"{brand} {cat_in_query}" not in variants:
|
||||
variants.append(f"{brand} {cat_in_query}")
|
||||
|
||||
return list(dict.fromkeys(variants))
|
||||
|
||||
|
||||
def _search_wb_one(query: str, limit: int, timeout: float, max_retries: int) -> list[dict[str, Any]]:
|
||||
@ -109,7 +124,8 @@ def _search_wb_one(query: str, limit: int, timeout: float, max_retries: int) ->
|
||||
log.warning("WB JSON parse failed: %s", e)
|
||||
return []
|
||||
|
||||
products = (data.get("data") or {}).get("products") or []
|
||||
# WB v18: products на верхнем уровне; v9 (legacy fallback): data.products
|
||||
products = data.get("products") or (data.get("data") or {}).get("products") or []
|
||||
if not products:
|
||||
log.info("WB no products for query=%r", query)
|
||||
return []
|
||||
@ -121,25 +137,36 @@ def _search_wb_one(query: str, limit: int, timeout: float, max_retries: int) ->
|
||||
|
||||
|
||||
def _build_item(p: dict[str, Any]) -> dict[str, Any]:
|
||||
sale_u = p.get("salePriceU") or 0
|
||||
price_u = p.get("priceU") or 0
|
||||
# WB цена в копейках (или /100). Старое поле было в копейках, иногда в условных единицах.
|
||||
# Делим на 100 — стандартный паттерн.
|
||||
price_min = (sale_u // 100) if sale_u else (price_u // 100 if price_u else None)
|
||||
price_max = (price_u // 100) if price_u and price_u != sale_u else None
|
||||
"""Парсит product из WB API.
|
||||
v9: salePriceU / priceU (в копейках, делим на 100)
|
||||
v18: sizes[0].price.{basic, product, total} (в копейках)
|
||||
"""
|
||||
price_min = None
|
||||
price_max = None
|
||||
|
||||
# Если у товара есть варианты sizes — берём минимальную цену оттуда
|
||||
# v18: цены в sizes[].price.{product, total, basic}
|
||||
sizes = p.get("sizes") or []
|
||||
if sizes:
|
||||
size_prices = []
|
||||
for s in sizes:
|
||||
sp = (s.get("price") or {}).get("product") or 0
|
||||
if sp:
|
||||
size_prices.append(sp // 100)
|
||||
if size_prices:
|
||||
price_min = min(size_prices)
|
||||
if len(size_prices) > 1:
|
||||
price_max = max(size_prices)
|
||||
size_prices = []
|
||||
for s in sizes:
|
||||
pr = s.get("price") or {}
|
||||
# Приоритет: product (с учётом скидки) > total > basic
|
||||
for fld in ("product", "total", "basic"):
|
||||
v = pr.get(fld) or 0
|
||||
if v:
|
||||
size_prices.append(v // 100)
|
||||
break
|
||||
if size_prices:
|
||||
price_min = min(size_prices)
|
||||
if len(size_prices) > 1 and max(size_prices) != price_min:
|
||||
price_max = max(size_prices)
|
||||
|
||||
# v9 fallback — только если ничего не нашли в sizes
|
||||
if price_min is None:
|
||||
sale_u = p.get("salePriceU") or 0
|
||||
price_u = p.get("priceU") or 0
|
||||
price_min = (sale_u // 100) if sale_u else (price_u // 100 if price_u else None)
|
||||
if price_u and price_u != sale_u:
|
||||
price_max = price_u // 100
|
||||
|
||||
pid = p.get("id")
|
||||
image_url = _build_image_url(pid) if pid else None
|
||||
|
||||
Loading…
Reference in New Issue
Block a user