gendesign/tradein-mvp/packages/scraper-kit/src/scraper_kit/domclick_exceptions.py
bot-backend bf3214b9e4
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
fix(tradein/scrapers): диагноз блока брался из текстовых маркеров чужой площадки, а не из HTTP-статуса (#3196)
Сайдкар вообще не читал код ответа 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 не исполняют.
2026-08-28 23:21:54 +03:00

64 lines
3.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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, а не блоком.
"""