All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 / browser-tests (pull_request) Successful in 1m7s
CI Trade-In / backend-tests (pull_request) Successful in 4m57s
Сайдкар вообще не читал код ответа page.goto: страница классифицировалась только по маркерам, снятым с Авито. Домклик отдаёт статическую `403 | Домклик` на 26 624 байта, где нет ни одного такого маркера (замер прода 28.08.2026) — она уезжала наверх как валидный HTML, парсер не находил состояние, и прогон получал блок неизвестной природы. За 14 дней все 14 прогонов домклика легли с ban_kind='unknown'; у Яндекса счётчика blocked не было вовсе, поэтому ветка перевода прогона в 'banned' была недостижима по построению — ноль банов. - browser/server.py: статус целевой навигации сохраняется per-provider и доезжает в тело /fetch аддитивным ключом "status" (ключ "html" не тронут); 403/429 с маркерами челленджа больше не ждут PoW — ждать нечего, статическая страница сама себя не перезагрузит. Наверх идёт BanPageDetectedError, а не заглушка: вернув её контентом, воскресили бы #3045. - scraper_kit/browser_fetcher.py: BrowserFetcher.last_response_status + ban_kind_from_status (403/429 → platform, 5xx → infra, прочее → None). Поток управления не менялся: fetch() по-прежнему отдаёт str. - domclick: DomClickBlockedError несёт .status — один тип исключения на маркер-детект и на сбой фетча разводится без размножения типов; прогон передаёт перепись диагнозов в mark_backfill_finished. - yandex: появился счётчик blocked, оживляющий ветку бана. Серии блоков и промахов парсера считаются РАЗДЕЛЬНО: иначе четыре промаха плюс один 403 пятым давали 'banned' с переписью {platform: 1}. - cian: ban_kinds наполняется только диагностируемым статусом. HTTP 200 с пустым разбором — дрейф разметки на нашей стороне, а не отказ площадки; записав его блоком, мы бы штамповали фиктивные баны у здорового источника (13 done против 1 banned за 14 дней). Инвариант: непустой ban_kinds ⟺ виден ответ 403/429/5xx. Значения остаются в пределах CHECK scrape_runs.ban_kind. Известный пробел: шов providers/domclick/detail.py `blocked.status = status` тестами не покрыт — существующие домкликовые тесты подают исключение готовым моком и боевой fetch_detail не исполняют.
64 lines
3.8 KiB
Python
64 lines
3.8 KiB
Python
"""DomClick-specific exceptions для anti-bot detection."""
|
||
|
||
from scraper_kit.proxy_errors import ProxyBanError
|
||
|
||
# ── Канонический список anti-bot маркеров (QRATOR/DataDome) ──────────────────
|
||
# Единый источник для Layer A (providers/domclick/serp.py) и Layer B
|
||
# (providers/domclick/detail.py) — были два расходящихся списка (#2636), теперь
|
||
# оба слоя импортируют этот. Сравнение case-insensitive: caller лоуеркейзит HTML
|
||
# перед `marker in html_lower`.
|
||
DOMCLICK_BLOCK_MARKERS: tuple[str, ...] = (
|
||
"qrator",
|
||
"bot_mitigation",
|
||
"система защиты",
|
||
"403 | домклик",
|
||
"captcha",
|
||
"access denied",
|
||
"datadome",
|
||
)
|
||
|
||
|
||
class DomClickBlockedError(ProxyBanError):
|
||
"""DomClick BFF вернул QRATOR block-страницу (HTTP 200 + block HTML).
|
||
|
||
QRATOR (qrator.net) — WAF/DDoS-защита domclick.ru. Блокирует datacenter-IP
|
||
и возвращает HTML с маркерами: см. `DOMCLICK_BLOCK_MARKERS` в этом модуле
|
||
("qrator", "bot_mitigation", "система защиты", "403 | домклик", "captcha",
|
||
"access denied", "datadome").
|
||
|
||
Обходится через shared mobile proxy (BrowserFetcher(source="domclick") →
|
||
generic provider → мобильный egress).
|
||
|
||
Наследует `ProxyBanError` (#2600 п.1, а не голый `Exception`) — ProxyBanError
|
||
САМ является `Exception`-подклассом (см. proxy_errors.py), так что `except
|
||
DomClickBlockedError:`/`isinstance(exc, Exception)` по всему коду не меняется;
|
||
это ДОПОЛНИТЕЛЬНО делает `isinstance(exc, ProxyBanError)` истинным для generic
|
||
curl-слоя (см. avito_exceptions.AvitoBlockedError для того же паттерна). Прямая
|
||
двойная база `(Exception, ProxyBanError)` даёт MRO-конфликт (ProxyBanError уже
|
||
сам наследует Exception) — единственная база решает это чище.
|
||
|
||
`status` (#3196) — HTTP-код ответа площадки, если он известен
|
||
(`BrowserFetcher.last_response_status`). Одно и то же исключение поднимается и
|
||
на распознанном маркере, и на сбое браузерного фетча — по счётчикам они были
|
||
неразличимы, и прогон получал ban_kind='unknown'. Статус разводит их без
|
||
размножения типов: 403/429 — отказ площадки, 5xx — инфраструктура, None —
|
||
правда «не знаем» (сайдкар статус не отдал либо фетч не состоялся).
|
||
"""
|
||
|
||
def __init__(self, *args: object, status: int | None = None) -> None:
|
||
super().__init__(*args)
|
||
self.status = status
|
||
|
||
|
||
class DomClickParseError(Exception):
|
||
"""DomClick detail-карточка получена (HTTP 200, не block), но SSR-стейт
|
||
не извлекается / не парсится.
|
||
|
||
Возникает в Layer B (domclick_detail.py) когда:
|
||
- `window.__SSR_STATE__` не найден в HTML (но это и не challenge-страница);
|
||
- object-литерал не сбалансирован по скобкам;
|
||
- json.loads падает после санитайза bare `undefined` → `null`.
|
||
|
||
В отличие от DomClickBlockedError (anti-bot), это сигнал дрейфа схемы карточки
|
||
или повреждённого ответа — оркестратор считает его parse-failure, а не блоком.
|
||
"""
|