From f6f494c17b74cf879ec24d97ea217536bdd90580 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 28 Aug 2026 20:49:39 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein/browser):=20=D1=81=D0=B0=D0=B9?= =?UTF-8?q?=D0=B4=D0=BA=D0=B0=D1=80=20=D0=B1=D0=B5=D0=B7=D1=83=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=D0=BD=D0=BE=20=D1=80=D0=B5=D0=B7=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BA=D0=B8,=20=D0=B0=20ca?= =?UTF-8?q?moufox=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D0=BF=D1=80=D0=B5=D0=B6?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB,=20=D1=87=D1=82=D0=BE=20=D0=B7=D0=B0=20?= =?UTF-8?q?=D1=8D=D1=82=D0=BE=20=D0=BB=D0=BE=D0=B2=D1=8F=D1=82=20WAF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _launch_browser ставил block_images: True для всех провайдеров, и в комментарии стояла только выгода — «рендер быстрее». Цена в комментарии не стояла, хотя camoufox пишет о ней в каждый запуск: LeakWarning: Blocking image requests has been reported to cause detection issues on major WAFs. За три часа прода — 44 таких предупреждения. Авито стоит за QRATOR, доля заглушек в detail-сборе ~32%. Теперь block_images разрешается через _resolve_block_images по образцу соседней _resolve_min_interval: per-provider env -> global env -> код-дефолт. Для avito дефолт — не блокировать. Для cian/yandex/generic дефолт остаётся прежним: их долю блоков никто не мерил, и менять им поведение заодно значило бы поменять три скрапера вслепую. Это не доказанная причина блоков, а единственная названная и документированная — своим же инструментом, в своём же конфиге. Проверять её надо замером на выборке от 40 карточек: блоки идут пачками (#3184), на малых выборках разница неразличима. Итоговая карта пишется в лог на старте, симметрично page-intervals — иначе значение видно только в коде. Refs #3185 --- tradein-mvp/browser/server.py | 65 ++++++++++++++++++++++++++++-- tradein-mvp/browser/test_server.py | 41 +++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/tradein-mvp/browser/server.py b/tradein-mvp/browser/server.py index 3d6f47a9..b921d04f 100644 --- a/tradein-mvp/browser/server.py +++ b/tradein-mvp/browser/server.py @@ -60,6 +60,19 @@ Per-provider модель (#1793): ⚠ BROWSER_BLOCK_RESOURCES (булев выключатель до #1812) НЕ читается — см. _RETIRED_ENV; в проде он всё ещё выставлен во всех трёх контейнерах и ни на что не влияет. + BROWSER_BLOCK_IMAGES — булев глобальный дефолт camoufox block_images (#3185). + Camoufox сам предупреждает (LeakWarning), что блокировка + картинок повышает риск детекта на крупных WAF — картинки + часть нормального fingerprint браузера, их отсутствие + выделяет сессию. Авито стоит за QRATOR: с block_images=True + в проде ~32% detail-сборов возвращали заглушку. Дефолт для + avito поэтому False (не блокировать); для cian/yandex/generic + остаётся True — их WAF-чувствительность не замеряна, менять + без замера не будем. + BROWSER_BLOCK_IMAGES_{PROVIDER} — per-provider override, тот же формат, что у + BROWSER_MIN_PAGE_INTERVAL_S_{PROVIDER}: PROVIDER в + upper-case (AVITO, CIAN, YANDEX, GENERIC), перебивает и + global BROWSER_BLOCK_IMAGES, и код-дефолт per-provider. BROWSER_MIN_PAGE_INTERVAL_S — минимальный интервал (сек) между последовательными page.goto ОДНОГО провайдера (default: 2.0). Даёт под- коннектам предыдущей страницы дренироваться, прежде чем @@ -235,6 +248,46 @@ _MIN_PAGE_INTERVAL_BY_PROVIDER: dict[str, float] = { p: _resolve_min_interval(p) for p in ("avito", "cian", "yandex", "generic") } +# Код-дефолт block_images ПО ПРОВАЙДЕРУ (#3185): avito не измерен пассивно — детект на +# QRATOR стоит дороже, чем экономия трафика/времени рендера. cian/yandex/generic не +# замерены, поведение не трогаем (True, как раньше). +_BLOCK_IMAGES_DEFAULT_BY_PROVIDER: dict[str, bool] = {"avito": False} +_BLOCK_IMAGES_DEFAULT_FALLBACK: bool = True + + +def _resolve_block_images( + provider: str, + environ: Mapping[str, str] | None = None, +) -> bool: + """Возвращает эффективный флаг block_images для провайдера (#3185). + + Приоритет (выше — сильнее): per-provider env ``BROWSER_BLOCK_IMAGES_{PROVIDER}`` + → глобальный env ``BROWSER_BLOCK_IMAGES`` → код-дефолт per-provider + (``_BLOCK_IMAGES_DEFAULT_BY_PROVIDER``, avito=False, остальные=True). Не кидает + исключений — невалидный/отсутствующий env тихо проваливается на следующий уровень. + + Args: + provider: имя провайдера ("avito", "cian", "yandex", "generic"). + environ: env-словарь; None → ``os.environ`` (тестируемость без патча модуля). + + Returns: + True — блокировать image-запросы в camoufox, False — не блокировать. + """ + env = environ if environ is not None else os.environ + provider_default = _BLOCK_IMAGES_DEFAULT_BY_PROVIDER.get( + provider, _BLOCK_IMAGES_DEFAULT_FALLBACK + ) + global_value = _parse_bool(env.get("BROWSER_BLOCK_IMAGES"), provider_default) + key = f"BROWSER_BLOCK_IMAGES_{provider.upper()}" + return _parse_bool(env.get(key), global_value) + + +# Per-provider эффективные флаги block_images, вычисляются на module-load из os.environ. +# Читается в _launch_browser; логируется на старте (_on_startup). +_BLOCK_IMAGES_BY_PROVIDER: dict[str, bool] = { + p: _resolve_block_images(p) for p in ("avito", "cian", "yandex", "generic") +} + def _provider_for_url(url: str) -> str: """Определяет поставщика по host URL. @@ -374,9 +427,14 @@ async def _launch_browser(provider: str, proxy_override: str | None = None) -> N "locale": "ru-RU", "geoip": True, "humanize": True, - # Скорость: не грузим картинки — нам нужен HTML/JSON, рендер быстрее - # (трафик безлимитный, выигрыш именно по времени загрузки). - "block_images": True, + # block_images НЕ бесплатен (#3185): camoufox сам предупреждает LeakWarning'ом, + # что блокировка image-запросов повышает риск детекта на крупных WAF — картинки + # часть штатного fingerprint браузера, их отсутствие выделяет сессию. Авито за + # QRATOR: пока блокировали всем провайдерам одинаково, ~32% detail-сборов + # получали заглушку. Теперь значение per-provider (BROWSER_BLOCK_IMAGES / + # BROWSER_BLOCK_IMAGES_{PROVIDER}, см. docstring модуля) — avito по дефолту НЕ + # блокирует, cian/yandex/generic блокируют как раньше (не измерены, не трогаем). + "block_images": _BLOCK_IMAGES_BY_PROVIDER.get(provider, _BLOCK_IMAGES_DEFAULT_FALLBACK), # Анти-leak: WebRTC может слить реальный IP контейнера мимо прокси → деанон. "block_webrtc": True, # Форсим таймзону +3 МСК. geoip берёт tz одного IP на старте, а БэкКоннект @@ -565,6 +623,7 @@ async def _on_startup(app: web.Application) -> None: list(PROVIDERS), ) logger.info("tradein-browser: page-intervals %s", _MIN_PAGE_INTERVAL_BY_PROVIDER) + logger.info("tradein-browser: block-images %s", _BLOCK_IMAGES_BY_PROVIDER) _warn_retired_env() diff --git a/tradein-mvp/browser/test_server.py b/tradein-mvp/browser/test_server.py index 2f1438eb..98c94d22 100644 --- a/tradein-mvp/browser/test_server.py +++ b/tradein-mvp/browser/test_server.py @@ -490,6 +490,47 @@ def test_pace_provider_independent_per_provider(monkeypatch: pytest.MonkeyPatch) assert slept == [], "разные провайдеры не пейсят друг друга" +# ── #3185: _resolve_block_images (per-provider block_images) ─────────────────────── + + +def test_resolve_block_images_default_avito_unblocked() -> None: + """Код-дефолт: avito НЕ блокирует картинки (QRATOR-детект, ~32% заглушек).""" + assert server._resolve_block_images("avito", environ={}) is False + + +def test_resolve_block_images_default_others_blocked() -> None: + """Код-дефолт: cian/yandex/generic блокируют как раньше (не измерены).""" + assert server._resolve_block_images("cian", environ={}) is True + assert server._resolve_block_images("yandex", environ={}) is True + assert server._resolve_block_images("generic", environ={}) is True + + +def test_resolve_block_images_global_env_overrides_default_both_ways() -> None: + """Глобальный BROWSER_BLOCK_IMAGES перебивает код-дефолт для ЛЮБОГО провайдера.""" + assert server._resolve_block_images("avito", environ={"BROWSER_BLOCK_IMAGES": "true"}) is True + assert server._resolve_block_images("cian", environ={"BROWSER_BLOCK_IMAGES": "false"}) is False + + +def test_resolve_block_images_per_provider_env_overrides_global() -> None: + """Per-provider BROWSER_BLOCK_IMAGES_{PROVIDER} сильнее и global env, и дефолта.""" + env = {"BROWSER_BLOCK_IMAGES": "false", "BROWSER_BLOCK_IMAGES_CIAN": "true"} + assert server._resolve_block_images("cian", environ=env) is True + # avito не упомянут per-provider'ом → берёт global (false). + assert server._resolve_block_images("avito", environ=env) is False + + +def test_resolve_block_images_per_provider_env_overrides_avito_default() -> None: + """BROWSER_BLOCK_IMAGES_AVITO=true может включить блокировку и для avito.""" + env = {"BROWSER_BLOCK_IMAGES_AVITO": "true"} + assert server._resolve_block_images("avito", environ=env) is True + + +def test_resolve_block_images_unrecognized_value_is_falsy() -> None: + """Нераспознанная строка per-provider env — как везде в _parse_bool: не True.""" + env = {"BROWSER_BLOCK_IMAGES_CIAN": "not-a-bool"} + assert server._resolve_block_images("cian", environ=env) is False + + # ── #2674: отставные переменные окружения ──────────────────────────────────── # BROWSER_BLOCK_RESOURCES стоит =true во всех трёх прод-контейнерах, а код перестал # её читать в #1812 (булев выключатель → BROWSER_BLOCK_RESOURCE_TYPES). Блокировка при