diff --git a/tradein-mvp/scripts/local-avito-msk/collect.py b/tradein-mvp/scripts/local-avito-msk/collect.py index 7a2ff186..db6ddb6a 100644 --- a/tradein-mvp/scripts/local-avito-msk/collect.py +++ b/tradein-mvp/scripts/local-avito-msk/collect.py @@ -60,6 +60,11 @@ _BATCH_ID_RE = re.compile(r"^[A-Za-z0-9._-]+$") _POW_MARKERS = ("startpow", "доступ ограничен: проверка безопасности") +# Заполняется при входе в Loader.__aenter__ (playwright импортируется лениво, +# чтобы --help работал без установленного пакета). +PlaywrightTimeoutError: type[BaseException] = TimeoutError + + class Blocked(Exception): """Первый признак блока. Ретраев нет — только немедленный стоп.""" @@ -212,6 +217,10 @@ class Loader: async def __aenter__(self) -> "Loader": from playwright.async_api import async_playwright + from playwright.async_api import TimeoutError as _PwTimeout + + global PlaywrightTimeoutError + PlaywrightTimeoutError = _PwTimeout endpoint = os.environ.get("AVITO_CDP", "http://localhost:9222") self._pw = await async_playwright().start() @@ -262,7 +271,7 @@ class Loader: if not self.budget_left(): raise BudgetExhausted() await self._pause() - resp = await self._page.goto(url, wait_until="domcontentloaded", timeout=90_000) + resp = await self._goto(url) self.loads += 1 self._last_load = time.monotonic() status = resp.status if resp is not None else None @@ -274,6 +283,33 @@ class Loader: _guard(html, status) return html, status + async def _goto(self, url: str, attempts: int = 3): + """goto с ограниченным ретраем на таймаут навигации. + + Авито изредка держит соединение до упора и goto падает по timeout. Это + НЕ признак отказа: в наблюдавшемся случае вкладка показывала нормальную + выдачу, а маркеров фаервола/PoW не было. Но и молча ретраить бесконечно + нельзя — тихий отказ выглядит ровно так же. Поэтому: перед каждым + повтором пробуем прочитать то, что в документе, и прогнать через _guard, + чтобы настоящий блок остановил прогон с правильной причиной; исчерпали + попытки — жёсткий стоп с причиной nav_timeout. + """ + for i in range(attempts): + try: + return await self._page.goto(url, wait_until="domcontentloaded", + timeout=90_000) + except PlaywrightTimeoutError: + try: + partial = await self._page.content() + except Exception: # noqa: BLE001 — документа может не быть вовсе + partial = "" + if partial: + _guard(partial, None) # настоящий блок остановит прогон здесь + if i == attempts - 1: + raise Blocked("nav_timeout") from None + print(f" таймаут навигации, попытка {i + 2}/{attempts}", flush=True) + await asyncio.sleep(10.0) + async def _read_content(self, attempts: int = 4) -> str: """page.content() с узким ретраем на гонку клиентской перенавигации.