feat(tradein/domclick-local): --enum-cache — enumerate один раз, resume detail поверх
enumerate (~6422 id через SERP, ~1.5-2ч) теперь пишет полный список id в файл-кэш (--enum-cache PATH) с sentinel завершённости; следующий запуск грузит кэш и полностью пропускает enumerate → сразу detail. detail-resume по done (JSONL) композится поверх. Кэш пишется только после реально завершённого свода (прерванный enumerate не кэшируется). Тест: run1 записал 5 id+sentinel, run2 загрузил и пропустил SERP.
This commit is contained in:
parent
0f7f5ea0ab
commit
5f994cc300
1 changed files with 81 additions and 3 deletions
|
|
@ -755,6 +755,65 @@ def append_record(path: Path, rec: dict[str, Any]) -> None:
|
||||||
fh.flush()
|
fh.flush()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Enumerate cache (полный список id, чтобы НЕ гонять SERP заново) ────────────
|
||||||
|
|
||||||
|
|
||||||
|
def write_enum_cache(path: str, offers: list[dict[str, Any]]) -> None:
|
||||||
|
"""Перезаписать кэш-файл полным списком offer-id + sentinel завершённости.
|
||||||
|
|
||||||
|
Каждый offer пишется как {"id":..., "source_url":...} (только 2 поля),
|
||||||
|
финальной строкой — sentinel {"_enum_complete": true, "count": N}. Sentinel
|
||||||
|
означает «enumerate завершился полностью» — без него load_enum_cache считает
|
||||||
|
кэш неполным и заставляет re-enumerate.
|
||||||
|
"""
|
||||||
|
cache_path = Path(path)
|
||||||
|
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with cache_path.open("w", encoding="utf-8") as fh:
|
||||||
|
for serp in offers:
|
||||||
|
row = {"id": serp.get("id"), "source_url": serp.get("source_url")}
|
||||||
|
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
|
||||||
|
fh.write(json.dumps({"_enum_complete": True, "count": len(offers)}) + "\n")
|
||||||
|
fh.flush()
|
||||||
|
logger.info("enum-cache: wrote %d ids → %s", len(offers), path)
|
||||||
|
|
||||||
|
|
||||||
|
def load_enum_cache(path: str) -> list[dict[str, Any]] | None:
|
||||||
|
"""Загрузить полный список offer-id из кэша. None если файла нет/кэш неполный.
|
||||||
|
|
||||||
|
Кэш валиден ТОЛЬКО если последняя валидная строка — sentinel с
|
||||||
|
``_enum_complete == True`` (enumerate завершился). Битые строки пропускаются;
|
||||||
|
нет sentinel / пустой / повреждённый → None (заставляет re-enumerate).
|
||||||
|
"""
|
||||||
|
cache_path = Path(path)
|
||||||
|
if not cache_path.exists():
|
||||||
|
return None
|
||||||
|
records: list[dict[str, Any]] = []
|
||||||
|
with cache_path.open("r", encoding="utf-8") as fh:
|
||||||
|
for line in fh:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rec = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if isinstance(rec, dict):
|
||||||
|
records.append(rec)
|
||||||
|
if not records:
|
||||||
|
return None
|
||||||
|
last = records[-1]
|
||||||
|
if not (isinstance(last, dict) and last.get("_enum_complete") is True):
|
||||||
|
return None # кэш неполный (enumerate не завершился) → re-enumerate
|
||||||
|
offers: list[dict[str, Any]] = []
|
||||||
|
for rec in records[:-1]:
|
||||||
|
sid = rec.get("id")
|
||||||
|
if sid is None:
|
||||||
|
continue
|
||||||
|
offers.append({"id": str(sid), "source_url": rec.get("source_url")})
|
||||||
|
logger.info("enum-cache: loaded %d ids (complete)", len(offers))
|
||||||
|
return offers
|
||||||
|
|
||||||
|
|
||||||
# ── SERP transport (playwright page navigation, чистый браузерный транспорт) ──
|
# ── SERP transport (playwright page navigation, чистый браузерный транспорт) ──
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1220,9 +1279,22 @@ async def run(args: argparse.Namespace) -> None:
|
||||||
logger.warning("warmup navigation failed (%s) — continuing", exc)
|
logger.warning("warmup navigation failed (%s) — continuing", exc)
|
||||||
|
|
||||||
# 1. Enumerate через фронтовый SERP (тот же чистый браузерный транспорт).
|
# 1. Enumerate через фронтовый SERP (тот же чистый браузерный транспорт).
|
||||||
cards = await enumerate_cards(
|
# enum-cache: полный список id живёт в файле → следующий запуск ПРОПУСКАЕТ
|
||||||
page, done, args.limit_pages, target_new, enum_render, enum_jitter
|
# enumerate (~1.5-2ч) и сразу идёт в detail. detail-resume по `done`
|
||||||
)
|
# композится поверх: кэш даёт ВСЕ id, `done` (из JSONL) скипает готовые.
|
||||||
|
cards: list[dict[str, Any]] | None = None
|
||||||
|
if args.enum_cache:
|
||||||
|
cards = load_enum_cache(args.enum_cache)
|
||||||
|
if cards is None:
|
||||||
|
cards = await enumerate_cards(
|
||||||
|
page, done, args.limit_pages, target_new, enum_render, enum_jitter
|
||||||
|
)
|
||||||
|
# Sentinel пишем ТОЛЬКО после реально завершённого enumerate (offers
|
||||||
|
# полный) — прерванный/упавший свод кэш не перезаписывает.
|
||||||
|
if args.enum_cache:
|
||||||
|
write_enum_cache(args.enum_cache, cards)
|
||||||
|
else:
|
||||||
|
logger.info("enum-cache HIT: %d ids — enumerate ПРОПУЩЕН", len(cards))
|
||||||
summary.enumerated = len(cards)
|
summary.enumerated = len(cards)
|
||||||
|
|
||||||
if args.enumerate_only:
|
if args.enumerate_only:
|
||||||
|
|
@ -1312,6 +1384,12 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
||||||
)
|
)
|
||||||
p.add_argument("--limit-pages", type=int, default=100, help="макс. страниц на room-бакет")
|
p.add_argument("--limit-pages", type=int, default=100, help="макс. страниц на room-бакет")
|
||||||
p.add_argument("--enumerate-only", action="store_true", help="только перечислить, без detail")
|
p.add_argument("--enumerate-only", action="store_true", help="только перечислить, без detail")
|
||||||
|
p.add_argument(
|
||||||
|
"--enum-cache",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="файл-кэш полного списка id (enumerate один раз → resume detail поверх)",
|
||||||
|
)
|
||||||
p.add_argument(
|
p.add_argument(
|
||||||
"--with-sold-similar",
|
"--with-sold-similar",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue