From 44633b0df4d7ccbcd9997a9ef3a56e7775f7f5fe Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 30 Aug 2026 09:40:21 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein/domclick):=20=D1=81=D0=B2=D0=B8?= =?UTF-8?q?=D0=BF=20=D1=85=D0=BE=D0=B4=D0=B8=D0=BB=20=D0=BD=D0=B0=20QRATOR?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20=D0=BA=D1=83=D0=BA=20=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=20=D0=BD=D0=B0=20PoW=20=D0=BA=D0=B0=D0=B6=D0=B4?= =?UTF-8?q?=D1=8B=D0=BC=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=BC?= =?UTF-8?q?=20(#3264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serp.py не передавал куки сессии вообще — слова cookie в файле не было. Свип приходил к QRATOR с чистым браузером и был обязан решать proof-of-work с нуля на каждый запрос. Прод, прогон 5330 (30.08 03:49-03:55): 9 запросов к bff-search-web.domclick.ru, все 9 зависли на челлендже (перезагрузка 1/2, 2/2, отказ), status=failed, 0 лотов. Прокси при этом ротировался (узел 9 → 10) — узел тут ни при чём. Добор с тем же сайдкаром и тем же пулом в ту же ночь взял 37 карточек из 37 без единого блока. Разница ровно в куках. Чинить это стало возможно только сейчас: свип ходит не на ekaterinburg.domclick.ru, а на отдельный хост bff-search-web.domclick.ru, и до правки _cookie_domain (PR #3262) куки легли бы на .bff-search-web.domclick.ru, не совпав с сессией площадки. Теперь оба хоста схлопываются в общий .domclick.ru. Снимок приходит параметром снаружи, а не читается внутри kit: kit не импортирует app.* (strangler-инвариант #2133). Поэтому джоба domclick_city_sweep переопределена продуктовым Handler'ом — build_registry это прямо допускает («последнее слово за продуктом»), а БД читает только app-сторона. Отсутствие сессии не авария: load_session вернул None → свип идёт как раньше, без инъекции, факт логируется один раз. Цена решения — override повторяет вызов kit-джобы целиком и может тихо с ней разойтись. Добавлен тест, который зовёт оба джоба одинаково и сравнивает наборы kwargs, допуская расхождение ровно в cookies. Проверен мутацией: с искусственно добавленным в kit-версию аргументом краснеет, без него зелёный. Тесты: 245 passed, 1 skipped (domclick + parity). --- .../backend/app/services/product_handlers.py | 59 +++- .../tests/test_3264_domclick_sweep_cookies.py | 297 ++++++++++++++++++ .../src/scraper_kit/orchestration/pipeline.py | 12 +- .../scraper_kit/providers/domclick/serp.py | 15 +- 4 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 tradein-mvp/backend/tests/test_3264_domclick_sweep_cookies.py diff --git a/tradein-mvp/backend/app/services/product_handlers.py b/tradein-mvp/backend/app/services/product_handlers.py index f3285747..b7a74b60 100644 --- a/tradein-mvp/backend/app/services/product_handlers.py +++ b/tradein-mvp/backend/app/services/product_handlers.py @@ -3,8 +3,12 @@ `build_product_handlers(ctx)` собирает реестр НЕ-sweep source'ов, тело которых осталось в `app` (rosreestr_dkp / sber_index / deactivate_stale_* / *_backfill / cadastral_geo_match / house_* / proxy_healthcheck / …). Kit-native sweep-оркестраторы -(avito/yandex/cian/domclick city/full-load) регистрируются самим `build_registry` -через `_default_kit_handlers` — здесь их НЕТ. +(avito/yandex/cian/domclick full-load) регистрируются самим `build_registry` через +`_default_kit_handlers` — здесь их НЕТ. Исключение: `domclick_city_sweep` (#3264) — +kit-native тело осталось прежним (`run_domclick_city_sweep`), но здесь переопределён +Handler, который ДО вызова читает БД за куки сессии (kit не имеет права на app.*/БД, +см. докстринг `_job_domclick_city_sweep` ниже); `build_registry` явно допускает такое +переопределение продуктовым ключом. Дизайн-инвариант: НЕ дублируем логику. Каждый `job` переиспользует существующую боевую джоб-функцию (`app.services.scheduler.import_rosreestr_dkp`, `app.tasks.*`, @@ -32,6 +36,9 @@ from scraper_kit.orchestration.scheduler import ( from scraper_kit.orchestration.scheduler import ( _defer_next_run_at as kit_defer_next_run_at, ) +from scraper_kit.orchestration.scheduler import ( + _pick_resume as kit_pick_resume, +) if TYPE_CHECKING: from scraper_kit.orchestration.scheduler import SchedulerContext @@ -419,6 +426,52 @@ async def _job_domclick_detail_backfill( await run_domclick_detail_backfill(db, run_id=run_id, params=params) +# ── domclick_city_sweep — override kit-native с инъекцией куки сессии (#3264) ─ +# Kit-native `_job_domclick_city_sweep` (scraper_kit.orchestration.scheduler) ходит на +# bff-search-web.domclick.ru БЕЗ кук: kit не имеет права импортировать app.* / читать БД +# (strangler-инвариант #2133). Каждый запрос свипа поэтому был обязан решать QRATOR PoW +# с нуля — прод 30.08 (прогон 5330): 9/9 запросов зависли на challenge, 0 листингов. +# `domclick_detail_backfill` (добор карточек) уже решает эту задачу инъекцией того же +# снимка (domclick_session.load_session) — здесь тот же приём для свипа. +# +# Регистрируем ЗДЕСЬ (app-side) под тем же ключом "domclick_city_sweep", а не правим +# kit: build_registry явно допускает переопределение kit-native продуктовым Handler'ом +# (см. докстринг build_registry — "последнее слово за продуктом"), а строгий запрет на +# app.* внутри kit при этом не нарушается — БД читает только этот модуль. +async def _job_domclick_city_sweep( + db: Session, run_id: int, params: dict[str, Any], ctx: SchedulerContext +) -> None: + from scraper_kit.orchestration.pipeline import run_domclick_city_sweep + + from app.services import domclick_session as domclick_session_svc + + # None — сессии в БД нет либо протухла (load_session сам фильтрует + # expires_at_estimate > NOW()). Это НЕ авария: свип продолжает работать без + # инъекции, ровно как до #3264, только логируем факт один раз для видимости. + cookies = domclick_session_svc.load_session(db) + if cookies is None: + logger.info( + "domclick_city_sweep run_id=%d: куки Sber ID сессии отсутствуют/протухли — " + "свип идёт без инъекции (см. #3264)", + run_id, + ) + + await run_domclick_city_sweep( + db, + run_id=run_id, + config=ctx.config, + matcher=ctx.matcher, + shutdown_requested=ctx.shutdown_requested, + proxy_provider=ctx.proxy_provider, + city_id=int(params.get("city_id", 4)), + rooms=params.get("rooms"), + pages=int(params.get("pages_per_anchor", 5)), + request_delay_sec=float(params.get("request_delay_sec", 6.0)), + resume_run_id=kit_pick_resume(db, run_id), + cookies=cookies, + ) + + # ── house_coords_from_listings — sync set-based UPDATE в executor (#2771) ───── async def _job_house_coords_from_listings( db: Session, run_id: int, params: dict[str, Any], ctx: SchedulerContext @@ -709,6 +762,8 @@ def build_product_handlers(ctx: SchedulerContext) -> dict[str, Handler]: "domclick_detail_backfill": Handler( _job_domclick_detail_backfill, "domclick_detail_backfill" ), + # Override kit-native (#3264) — инъекция куки сессии, см. докстринг job'а выше. + "domclick_city_sweep": Handler(_job_domclick_city_sweep, "domclick_city_sweep"), "cadastral_geo_match": Handler(_job_cadastral_geo_match, "cadastral_geo_match"), "osm_poi_ekb_refresh": Handler(_job_osm_poi_ekb_refresh, "osm_poi_ekb_refresh"), "house_imv_backfill": Handler(_job_house_imv_backfill, "house_imv_backfill"), diff --git a/tradein-mvp/backend/tests/test_3264_domclick_sweep_cookies.py b/tradein-mvp/backend/tests/test_3264_domclick_sweep_cookies.py new file mode 100644 index 00000000..2930175b --- /dev/null +++ b/tradein-mvp/backend/tests/test_3264_domclick_sweep_cookies.py @@ -0,0 +1,297 @@ +"""#3264: свип DomClick (`domclick_city_sweep`) ходил на bff-search-web.domclick.ru БЕЗ +кук сессии и был обязан решать QRATOR PoW с нуля на каждый запрос — прод 30.08 (прогон +5330): 9/9 запросов зависли на challenge, 0 листингов. `domclick_detail_backfill` (добор +карточек) уже решал эту задачу инъекцией снимка `domclick_session.load_session` — этот +файл проверяет, что тот же снимок теперь доезжает и до свипа: + + 1. DomClickScraper(cookies=...) кладёт снимок в self._cookies и пробрасывает его в + ОБА fetcher.fetch() (count-probe и paginate-leaf). + 2. run_domclick_city_sweep(cookies=...) пробрасывает их в DomClickScraper(...). + 3. product_handlers._job_domclick_city_sweep (app-side override kit-native, см. его + докстринг) грузит куки через domclick_session.load_session(db) и не падает, когда + сессии в БД нет/протухла (load_session вернул None) — свип в этом случае работает + как раньше, без инъекции. + +Не проверяется (вне периметра): реальный обход QRATOR, browser_fetcher.fetch() +внутренности (уже покрыты test_domclick_detail.py), ротация прокси/корзин (#2854/#2670). +""" + +from __future__ import annotations + +import os +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, patch + +os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") + +import pytest +from scraper_kit.providers.domclick.serp import DomClickScraper + +PFX = "scraper_kit.orchestration.pipeline" + + +def _config() -> SimpleNamespace: + return SimpleNamespace( + scraper_fetch_mode="curl_cffi", + browser_http_endpoint="http://browser.test/fetch", + scraper_proxy_url=None, + avito_proxy_max_rotations=0, + avito_serp_ok_not_banned=True, + avito_proxy_rotate_settle_s=0.0, + proxy_rotate_attempts=1, + proxy_rotate_attempt_timeout_s=1.0, + cian_proxy_max_rotations=0, + yandex_proxy_max_rotations=0, + scraper_skip_seen_today=False, + cian_full_load_per_fetch_timeout_s=0.0, + ) + + +# ── 1. DomClickScraper: cookies → self._cookies → оба fetcher.fetch() ──────────────── + + +def test_domclick_scraper_stores_cookies() -> None: + snapshot = {"sbjs": "x", "dvc": "y"} + scraper = DomClickScraper(_config(), cookies=snapshot) + assert scraper._cookies == snapshot + + +def test_domclick_scraper_defaults_cookies_to_none() -> None: + scraper = DomClickScraper(_config()) + assert scraper._cookies is None + + +@pytest.mark.asyncio +async def test_count_passes_cookies_to_fetch() -> None: + snapshot = {"sbjs": "x"} + scraper = DomClickScraper(_config(), cookies=snapshot) + fetcher = MagicMock() + fetcher.fetch = AsyncMock(return_value='{"result": {"snippetsCount": 3}}') + n = await scraper._count(fetcher, "st", None, None) + assert n == 3 + assert fetcher.fetch.await_args.kwargs["cookies"] == snapshot + + +@pytest.mark.asyncio +async def test_count_passes_none_cookies_without_error() -> None: + """Cookies отсутствуют/протухли — не авария, fetch() зовётся с cookies=None.""" + scraper = DomClickScraper(_config(), cookies=None) + fetcher = MagicMock() + fetcher.fetch = AsyncMock(return_value='{"result": {"snippetsCount": 0}}') + await scraper._count(fetcher, "st", None, None) + assert fetcher.fetch.await_args.kwargs["cookies"] is None + + +@pytest.mark.asyncio +async def test_paginate_passes_cookies_to_fetch() -> None: + snapshot = {"sbjs": "x"} + scraper = DomClickScraper(_config(), cookies=snapshot) + fetcher = MagicMock() + fetcher.fetch = AsyncMock(return_value='{"result": {"items": []}}') + out: list[object] = [] + await scraper._paginate(fetcher, "st", None, None, set(), out, pages=1) + assert fetcher.fetch.await_args.kwargs["cookies"] == snapshot + + +# ── 2. run_domclick_city_sweep: cookies → DomClickScraper(...) ─────────────────────── + +def _stub_runs() -> MagicMock: + """runs-двойник: is_cancelled=False (иначе sweep выходит ДО создания scraper'а).""" + m = MagicMock() + m.is_cancelled = MagicMock(return_value=False) + return m + + + +@pytest.mark.asyncio +async def test_run_domclick_city_sweep_passes_cookies_to_scraper() -> None: + from scraper_kit.orchestration.pipeline import run_domclick_city_sweep + from scraper_kit.providers.domclick.serp import ROOM_BUCKETS + + snapshot = {"sbjs": "x"} + scraper = MagicMock() + scraper.__aenter__ = AsyncMock(return_value=scraper) + scraper.__aexit__ = AsyncMock(return_value=None) + scraper.fetch_city = AsyncMock(return_value=[]) + scraper.blocked = False + scraper.geo_filtered = 0 + scraper.fetch_errors = 0 + scraper.buckets_completed = len(ROOM_BUCKETS) + scraper.buckets_total = len(ROOM_BUCKETS) + ctor = MagicMock(return_value=scraper) + + db = MagicMock() + with ( + patch(f"{PFX}.DomClickScraper", ctor), + patch(f"{PFX}.save_listings", MagicMock(return_value=(0, 0))), + patch(f"{PFX}.runs", _stub_runs()), + ): + await run_domclick_city_sweep( + db, + config=_config(), + matcher=MagicMock(), + run_id=1, + city_id=4, + pages=1, + request_delay_sec=0.0, + cookies=snapshot, + ) + + assert ctor.call_args.kwargs["cookies"] == snapshot + + +@pytest.mark.asyncio +async def test_run_domclick_city_sweep_none_cookies_does_not_break() -> None: + """cookies=None (дефолт) — прежнее поведение, свип не падает.""" + from scraper_kit.orchestration.pipeline import run_domclick_city_sweep + from scraper_kit.providers.domclick.serp import ROOM_BUCKETS + + scraper = MagicMock() + scraper.__aenter__ = AsyncMock(return_value=scraper) + scraper.__aexit__ = AsyncMock(return_value=None) + scraper.fetch_city = AsyncMock(return_value=[]) + scraper.blocked = False + scraper.geo_filtered = 0 + scraper.fetch_errors = 0 + scraper.buckets_completed = len(ROOM_BUCKETS) + scraper.buckets_total = len(ROOM_BUCKETS) + ctor = MagicMock(return_value=scraper) + + db = MagicMock() + with ( + patch(f"{PFX}.DomClickScraper", ctor), + patch(f"{PFX}.save_listings", MagicMock(return_value=(0, 0))), + patch(f"{PFX}.runs", _stub_runs()), + ): + counters = await run_domclick_city_sweep( + db, + config=_config(), + matcher=MagicMock(), + run_id=1, + city_id=4, + pages=1, + request_delay_sec=0.0, + ) + + assert ctor.call_args.kwargs["cookies"] is None + assert counters.errors_count == 0 + + +# ── 3. product_handlers._job_domclick_city_sweep: БД → cookies → sweep ─────────────── + + +@pytest.mark.asyncio +async def test_job_domclick_city_sweep_loads_and_passes_cookies() -> None: + from app.services.product_handlers import _job_domclick_city_sweep + + snapshot = {"sbjs": "x"} + db = MagicMock() + ctx = MagicMock(proxy_provider=None, shutdown_requested=lambda: False) + + with ( + patch( + "app.services.domclick_session.load_session", + MagicMock(return_value=snapshot), + ), + patch( + "scraper_kit.orchestration.pipeline.run_domclick_city_sweep", + AsyncMock(), + ) as run_mock, + patch("app.services.product_handlers.kit_pick_resume", MagicMock(return_value=None)), + ): + await _job_domclick_city_sweep(db, 1, {}, ctx) + + assert run_mock.await_args.kwargs["cookies"] == snapshot + + +@pytest.mark.asyncio +async def test_job_domclick_city_sweep_none_session_is_not_fatal() -> None: + """load_session вернул None (нет/протухла) — свип идёт БЕЗ инъекции, не падает.""" + from app.services.product_handlers import _job_domclick_city_sweep + + db = MagicMock() + ctx = MagicMock(proxy_provider=None, shutdown_requested=lambda: False) + + with ( + patch("app.services.domclick_session.load_session", MagicMock(return_value=None)), + patch( + "scraper_kit.orchestration.pipeline.run_domclick_city_sweep", + AsyncMock(), + ) as run_mock, + patch("app.services.product_handlers.kit_pick_resume", MagicMock(return_value=None)), + ): + await _job_domclick_city_sweep(db, 1, {}, ctx) + + assert run_mock.await_args.kwargs["cookies"] is None + + +def test_domclick_city_sweep_registered_as_product_override() -> None: + """build_product_handlers() переопределяет kit-native domclick_city_sweep (#3264).""" + from app.services.product_handlers import _job_domclick_city_sweep, build_product_handlers + + handlers = build_product_handlers(ctx=None) # type: ignore[arg-type] + assert handlers["domclick_city_sweep"].job is _job_domclick_city_sweep + + +# ── 4. Дрейф override'а от kit-native (#3264) ──────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_product_override_matches_kit_native_call_except_cookies() -> None: + """Продуктовый override зовёт sweep ТЕМИ ЖЕ аргументами, что kit-native, плюс cookies. + + Override (app.services.product_handlers) не оборачивает kit-джобу, а повторяет её + вызов целиком — иначе пришлось бы тащить чтение БД внутрь kit, что запрещено + strangler-инвариантом #2133. Цена такого решения — дрейф: параметр, добавленный в + kit-native `_job_domclick_city_sweep`, в override не появится, и свип по расписанию + молча поедет со старым набором. Этот тест — единственное, что такой дрейф ловит: + оба джоба зовутся с одинаковыми db/run_id/params/ctx, и наборы kwargs сравниваются. + + Расхождение допускается ровно одно — `cookies`, ради которого override и заведён. + """ + from scraper_kit.orchestration.scheduler import ( + _job_domclick_city_sweep as kit_job, + ) + + from app.services.product_handlers import _job_domclick_city_sweep as product_job + + params = {"city_id": 4, "pages_per_anchor": 7, "request_delay_sec": 3.0} + + # Патчить sweep надо В ПРОСТРАНСTВЕ ИМЁН ВЫЗЫВАЮЩЕГО: kit-джоба связала имя + # `run_domclick_city_sweep` на импорте модуля, продуктовая импортирует его внутри + # тела. Патч по одному только `pipeline` kit-джобу не перехватит — она позовёт + # настоящий свип и уйдёт в сеть. + # db и ctx общие на оба захвата: иначе сравнение спотыкается не о дрейф, а о то, + # что ctx.config — разные MagicMock-объекты в двух вызовах. + db = MagicMock() + ctx = MagicMock(proxy_provider=None, shutdown_requested=lambda: False) + + async def _capture(job, patch_sweep_in: str, patch_resume_in: str) -> dict: + with ( + patch("app.services.domclick_session.load_session", MagicMock(return_value=None)), + patch(patch_sweep_in, AsyncMock()) as run_mock, + patch(patch_resume_in, MagicMock(return_value=None)), + ): + await job(db, 1, params, ctx) + return run_mock.await_args.kwargs + + kit_kwargs = await _capture( + kit_job, + "scraper_kit.orchestration.scheduler.run_domclick_city_sweep", + "scraper_kit.orchestration.scheduler._pick_resume", + ) + product_kwargs = await _capture( + product_job, + f"{PFX}.run_domclick_city_sweep", + "app.services.product_handlers.kit_pick_resume", + ) + + assert set(product_kwargs) - set(kit_kwargs) == {"cookies"}, ( + "override приобрёл аргументы сверх kit-native помимо cookies" + ) + assert not set(kit_kwargs) - set(product_kwargs), ( + "kit-native получил аргумент, которого нет в override — свип по расписанию " + "поедет без него (дрейф #3264)" + ) + for key, kit_value in kit_kwargs.items(): + assert product_kwargs[key] == kit_value, f"override разошёлся с kit-native по {key!r}" diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py index 785c06dc..71599c87 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py @@ -4289,6 +4289,7 @@ async def run_domclick_city_sweep( request_delay_sec: float | None = None, region_code: int = DEFAULT_REGION_CODE, resume_run_id: int | None = None, + cookies: dict[str, str] | None = None, ) -> DomClickCitySweepCounters: """DomClick citywide sweep через BFF JSON API. @@ -4310,6 +4311,13 @@ async def run_domclick_city_sweep( прогон не доделал работу). Ноль лотов с fetch-ошибками, но без блока → mark_failed. Иначе mark_done. + cookies (#3264): снимок Sber ID сессии (dict cookie_name→value), инжектируется + снаружи strangler-путём (#2133) — kit сам не читает app.services.domclick_session / + БД. Без него sweep ходит на bff-search-web.domclick.ru БЕЗ кук и обязан решать + QRATOR PoW с нуля на каждый запрос (прод 30.08, прогон 5330 — 9/9 запросов + зависли на challenge). None (дефолт, сессии в БД нет/протухла) — прежнее + поведение, без инъекции. + Возвращает DomClickCitySweepCounters. """ _resolved_delay = request_delay_sec if request_delay_sec is not None else 6.0 @@ -4376,7 +4384,9 @@ async def run_domclick_city_sweep( async def _domclick_phase() -> None: """Единственная citywide-фаза: fetch_city + save.""" nonlocal lots - async with DomClickScraper(config, proxy_provider=proxy_provider) as _scraper: + async with DomClickScraper( + config, proxy_provider=proxy_provider, cookies=cookies + ) as _scraper: _scraper_ref.append(_scraper) if request_delay_sec is not None: _scraper.request_delay_sec = _resolved_delay diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py index 00cfe912..6df94ed9 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py @@ -253,6 +253,7 @@ class DomClickScraper(BaseScraper): *, delay_provider: Callable[[str], float] | None = None, proxy_provider: ProxyProvider | None = None, + cookies: dict[str, str] | None = None, ) -> None: super().__init__() # Strangler-инжекция (#2133): config даёт browser_http_endpoint для @@ -263,6 +264,16 @@ class DomClickScraper(BaseScraper): # ЕДИНСТВЕННЫМ sweep-источником без пула (#2160 P4 wiring gap) и потому ходил # через статический SCRAPER_PROXY_URL сайдкара. self._proxy_provider = proxy_provider + # #3264: снимок cookies авторизованной test-аккаунт сессии (Sber ID), тот же + # приём, что уже работает в detail.py fetch_detail(). Свип раньше ходил на + # bff-search-web.domclick.ru БЕЗ кук и был обязан решать QRATOR PoW с нуля на + # каждый запрос — прод 30.08 (прогон 5330) показал 9/9 запросов зависших на + # PoW-челлендже. Strangler-инжекция (#2133): kit не читает app.* / БД сам, + # снимок приходит параметром снаружи (см. run_domclick_city_sweep → + # scheduler._job_domclick_city_sweep, который грузит его через + # app.services.domclick_session.load_session). None (дефолт) — сессии в БД + # нет/протухла, свип работает как раньше, без инъекции. + self._cookies = cookies if delay_provider is not None: self.request_delay_sec = delay_provider(self.name) self.parse_failures: int = 0 @@ -471,7 +482,7 @@ class DomClickScraper(BaseScraper): """ url = _build_count_url(rooms, price_gte, price_lte) logger.debug("domklik: count url=%s", url) - html = await fetcher.fetch(url) + html = await fetcher.fetch(url, cookies=self._cookies) # #3264: снимок Sber ID сессии, обходит QRATOR PoW try: data = _extract_json(html) except DomClickBlockedError: @@ -560,7 +571,7 @@ class DomClickScraper(BaseScraper): break url = _build_offers_url(rooms, price_gte, price_lte, offset) logger.debug("domklik: offers url=%s", url) - html = await fetcher.fetch(url) + html = await fetcher.fetch(url, cookies=self._cookies) # #3264: снимок Sber ID сессии, обходит QRATOR PoW try: data = _extract_json(html) except DomClickBlockedError: