All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m12s
Найдено сверкой двух независимых источников: подсчёт упоминаний по всем
python-файлам (включая тесты) дал имена, встречающиеся ровно один раз — в
собственном объявлении; каждое затем подтверждено serena find_referencing_symbols
(LSP, видит и косвенные ссылки) и проверено grep'ом по yml/sql/ts/md на случай
ссылки строкой.
Удалено:
* _qr_code_data_url (trade_in_pdf) — QR как SVG data URL, не вызывался
ниоткуда; вместе с ним ушёл осиротевший import segno. `io` ОСТАВЛЕН —
io.BytesIO используется дальше по файлу (строка 774);
* _local_name (gar_flats_loader) — снятие namespace с имени тега;
* AvitoParseError — класс никто не поднимает и не ловит;
* IMVCityMismatchError — то же.
Что НЕ тронуто, хотя фильтр их показал:
* ~200 обработчиков FastAPI и celery-задач — их поднимает декоратор, по имени
их действительно никто не зовёт;
* refresh_ddu_price_indicator — точка ручного обслуживания, задокументирована
в комментарии к матвью (data/sql/152_mv_ddu_price_indicator.sql: «Refresh:
... (не в beat)»);
* schemas/parcel.py::MarketPrice — половина контракта, фронт использует
(frontend/src/types/site-finder.ts: market_price?: MarketPrice);
* JobSetting — SQLAlchemy-модель, живёт через metadata Base.
segno остался в backend/pyproject.toml и больше нигде не используется — снятие
зависимости требует пересборки лока, поэтому отдельным PR.
ruff clean, 4974 passed / 37 skipped.
73 lines
4.5 KiB
Python
73 lines
4.5 KiB
Python
"""Avito-specific exceptions для anti-bot detection."""
|
||
|
||
from scraper_kit.proxy_errors import ProxyBanError
|
||
|
||
|
||
class AvitoError(Exception):
|
||
"""Base для всех Avito scraper exceptions."""
|
||
|
||
|
||
class AvitoBlockedError(AvitoError, ProxyBanError):
|
||
"""HTTP 403 от Avito — IP-level block detected.
|
||
|
||
Дополнительно наследует `ProxyBanError` (#2600 п.1) — генерик curl-путь
|
||
(`providers/_proxy.py::curl_proxy_url`) распознаёт это как «площадка забанила
|
||
прокси» через `isinstance(exc, ProxyBanError)`, не импортируя avito_exceptions
|
||
напрямую. `except AvitoBlockedError:` во всём остальном коде не меняется —
|
||
mixin ничего не убирает, только добавляет.
|
||
"""
|
||
|
||
|
||
class AvitoRateLimitedError(AvitoError):
|
||
"""HTTP 429 от Avito — rate limit triggered."""
|
||
|
||
|
||
class AvitoSidecarUnavailableError(AvitoRateLimitedError):
|
||
"""НАШ браузерный сайдкар не отдал страницу — площадка ни при чём (#2686).
|
||
|
||
Поднимается только там, где отказ ПОРОЖДЁН нашей инфраструктурой: sidecar
|
||
вернул 503 «browser unavailable», упал внутри (Page.goto Timeout, браузер не
|
||
поднялся) либо соединение до него оборвалось (ReadTimeout/ConnectError). Это
|
||
отличается от AvitoBlockedError (площадка показала firewall) и от честного
|
||
HTTP 429 самой площадки, хотя все три раньше давали ОДИН статус 'banned' —
|
||
92 прогона из 114 (81%) за 45 суток были нашим сайдкаром, и по этому статусу
|
||
приняли решение замедлить сбор вдвое (миграция 206).
|
||
|
||
Наследует AvitoRateLimitedError НАМЕРЕННО: всё поведение прогона обязано
|
||
остаться прежним (`except (AvitoBlockedError, AvitoRateLimitedError)` в
|
||
pipeline/full-load/backfill ловят его без изменений → mark_banned →
|
||
done_buckets-чекпоинт сохраняется). Разводятся не пути, а ДИАГНОЗ:
|
||
pipeline пишет его в scrape_runs.ban_kind ('infra' vs 'platform').
|
||
"""
|
||
|
||
|
||
class AvitoListingGoneError(AvitoError):
|
||
"""Объявление удалено / вернуло 404 (страница «Ошибка 404» вместо item-view).
|
||
|
||
Отдельный сигнал от AvitoBlockedError/AvitoRateLimitedError: 404 — НЕ анти-бот
|
||
deflect, а мёртвый листинг (его незачем ротировать/ретраить). Наследует общий
|
||
AvitoError, но НЕ AvitoBlockedError — чтобы backfill-ловушка
|
||
`except (AvitoBlockedError, AvitoRateLimitedError)` его НЕ перехватывала и не крутила
|
||
consecutive-block breaker на мёртвых координатных дырах (run 458: 5 dead → abort).
|
||
"""
|
||
|
||
|
||
class AvitoContentBlockedError(AvitoBlockedError):
|
||
"""HTTP 200 от Avito, но 0 карточек на первой странице — content-block / captcha.
|
||
|
||
Наследует AvitoBlockedError, чтобы существующие except-блоки на AvitoBlockedError
|
||
продолжали ловить этот сигнал (mark_banned, pipeline abort и т.д.).
|
||
"""
|
||
|
||
|
||
class AvitoWarmupCookiesMissingError(AvitoBlockedError):
|
||
"""Прогрев вернул HTTP 200 без firewall-маркеров, но антибот-cookies
|
||
(`__zzatw-*`/`cfidsw-*`) НЕ появились в сессии (#3034).
|
||
|
||
Тихий soft-block, не пойманный `_is_firewall_page`/`_is_detail_soft_block`: страница
|
||
отдалась, но сессия не помечена площадкой как прогретая — детейл-батч на ней сжигал
|
||
бы прокси на обречённых запросах (403 на каждой карточке). Наследует AvitoBlockedError,
|
||
чтобы существующие `except (AvitoBlockedError, AvitoRateLimitedError)` (ban_kind,
|
||
proxy-ротация, consecutive-block breaker в pipeline/backfill) отработали как на обычном
|
||
блоке площадки, без отдельного except-блока на call-сайтах.
|
||
"""
|