fix(tradein/scraper-kit): wire browser proxy-pool into yandex/cian SERP + persist activation flags (#2160) (#2268)
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 17s
Deploy Trade-In / build-frontend (push) Successful in 43s
Deploy Trade-In / build-browser (push) Successful in 44s
Deploy Trade-In / test (push) Successful in 1m58s
Deploy Trade-In / build-backend (push) Has been cancelled
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 17s
Deploy Trade-In / build-frontend (push) Successful in 43s
Deploy Trade-In / build-browser (push) Successful in 44s
Deploy Trade-In / test (push) Successful in 1m58s
Deploy Trade-In / build-backend (push) Has been cancelled
This commit is contained in:
parent
21608eced1
commit
e1c5f2184c
6 changed files with 192 additions and 7 deletions
164
tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py
Normal file
164
tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
"""Тесты проброса browser-пула в SERP-скраперы yandex/cian (#2160).
|
||||||
|
|
||||||
|
Зеркало P4 (#2164) для avito: там пул раздавался только в avito-путях, а yandex/cian
|
||||||
|
SERP создавали BrowserFetcher сами в __aenter__ без пула → весь трафик шёл через
|
||||||
|
env-прокси браузера. Здесь проверяем, что YandexRealtyScraper / CianScraper прокидывают
|
||||||
|
proxy_provider + use_pool=config.use_proxy_pool_browser в BrowserFetcher.
|
||||||
|
|
||||||
|
Инвариант ship-dark:
|
||||||
|
- proxy_provider=None ИЛИ флаг off → BrowserFetcher(..., proxy_provider=None, use_pool=False),
|
||||||
|
т.е. env-прокси браузера, поведение байт-в-байт как до фикса.
|
||||||
|
- proxy_provider задан + флаг on → BrowserFetcher получает провайдер + use_pool=True.
|
||||||
|
|
||||||
|
BrowserFetcher монки-патчится на recording-заглушку в модуле каждого скрапера, так что
|
||||||
|
реального HTTP к tradein-browser не происходит.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import types
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from scraper_kit.providers.cian import serp as cian_serp
|
||||||
|
from scraper_kit.providers.yandex import serp as yandex_serp
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeProxyProvider:
|
||||||
|
"""ProxyProvider-заглушка: методы no-op (в этих тестах пул не вызывается —
|
||||||
|
BrowserFetcher замокан, acquire/release не срабатывают)."""
|
||||||
|
|
||||||
|
def acquire(self, provider: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def release(self, lease: object) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def mark_health(self, lease: object, ok: bool) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _make_recorder() -> tuple[type, list[dict[str, Any]]]:
|
||||||
|
"""Recording-заглушка BrowserFetcher: пишет init-kwargs в общий список."""
|
||||||
|
calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
class _RecFetcher:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
source: str = "avito",
|
||||||
|
fetch_timeout_s: float = 120.0,
|
||||||
|
*,
|
||||||
|
endpoint: str,
|
||||||
|
proxy_provider: object | None = None,
|
||||||
|
use_pool: bool = False,
|
||||||
|
) -> None:
|
||||||
|
calls.append(
|
||||||
|
{
|
||||||
|
"source": source,
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"proxy_provider": proxy_provider,
|
||||||
|
"use_pool": use_pool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __aenter__(self) -> _RecFetcher:
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: object) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return _RecFetcher, calls
|
||||||
|
|
||||||
|
|
||||||
|
def _config(*, use_pool: bool) -> types.SimpleNamespace:
|
||||||
|
return types.SimpleNamespace(
|
||||||
|
browser_http_endpoint="http://browser:3000",
|
||||||
|
use_proxy_pool_browser=use_pool,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Yandex SERP ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_yandex_serp_wires_pool_when_flag_on(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""proxy_provider + флаг on → BrowserFetcher получает провайдер + use_pool=True."""
|
||||||
|
rec_fetcher, calls = _make_recorder()
|
||||||
|
monkeypatch.setattr(yandex_serp, "BrowserFetcher", rec_fetcher)
|
||||||
|
provider = _FakeProxyProvider()
|
||||||
|
|
||||||
|
scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=True), proxy_provider=provider)
|
||||||
|
async with scraper:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0]["source"] == "yandex"
|
||||||
|
assert calls[0]["proxy_provider"] is provider
|
||||||
|
assert calls[0]["use_pool"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_yandex_serp_no_pool_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Без provider/флага → BrowserFetcher(proxy_provider=None, use_pool=False) — ship-dark."""
|
||||||
|
rec_fetcher, calls = _make_recorder()
|
||||||
|
monkeypatch.setattr(yandex_serp, "BrowserFetcher", rec_fetcher)
|
||||||
|
|
||||||
|
scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=False))
|
||||||
|
async with scraper:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0]["proxy_provider"] is None
|
||||||
|
assert calls[0]["use_pool"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_yandex_serp_provider_without_flag_stays_dark(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""provider задан, но флаг off → use_pool=False (BrowserFetcher пул не трогает)."""
|
||||||
|
rec_fetcher, calls = _make_recorder()
|
||||||
|
monkeypatch.setattr(yandex_serp, "BrowserFetcher", rec_fetcher)
|
||||||
|
provider = _FakeProxyProvider()
|
||||||
|
|
||||||
|
scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=False), proxy_provider=provider)
|
||||||
|
async with scraper:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert calls[0]["proxy_provider"] is provider
|
||||||
|
assert calls[0]["use_pool"] is False
|
||||||
|
|
||||||
|
|
||||||
|
# ── Cian SERP ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cian_serp_wires_pool_when_flag_on(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""proxy_provider + флаг on → BrowserFetcher получает провайдер + use_pool=True."""
|
||||||
|
rec_fetcher, calls = _make_recorder()
|
||||||
|
monkeypatch.setattr(cian_serp, "BrowserFetcher", rec_fetcher)
|
||||||
|
provider = _FakeProxyProvider()
|
||||||
|
|
||||||
|
scraper = cian_serp.CianScraper(_config(use_pool=True), proxy_provider=provider)
|
||||||
|
async with scraper:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0]["source"] == "cian"
|
||||||
|
assert calls[0]["proxy_provider"] is provider
|
||||||
|
assert calls[0]["use_pool"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cian_serp_no_pool_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Без provider/флага → BrowserFetcher(proxy_provider=None, use_pool=False) — ship-dark."""
|
||||||
|
rec_fetcher, calls = _make_recorder()
|
||||||
|
monkeypatch.setattr(cian_serp, "BrowserFetcher", rec_fetcher)
|
||||||
|
|
||||||
|
scraper = cian_serp.CianScraper(_config(use_pool=False))
|
||||||
|
async with scraper:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0]["proxy_provider"] is None
|
||||||
|
assert calls[0]["use_pool"] is False
|
||||||
|
|
@ -177,6 +177,11 @@ services:
|
||||||
GENDESIGN_FDW_PASSWORD: "${GENDESIGN_FDW_PASSWORD:-}"
|
GENDESIGN_FDW_PASSWORD: "${GENDESIGN_FDW_PASSWORD:-}"
|
||||||
COOKIE_ENCRYPTION_KEY: "${COOKIE_ENCRYPTION_KEY:-}"
|
COOKIE_ENCRYPTION_KEY: "${COOKIE_ENCRYPTION_KEY:-}"
|
||||||
SCHEDULER_ENABLE: "true"
|
SCHEDULER_ENABLE: "true"
|
||||||
|
# Активация scraper-kit + proxy-pool (#2126/#2160): kit-scheduler вместо
|
||||||
|
# legacy sweep-путей; curl/browser-фетчи берут прокси из пула scrape_proxies.
|
||||||
|
USE_KIT_SCHEDULER: "true"
|
||||||
|
USE_PROXY_POOL_CURL: "true"
|
||||||
|
USE_PROXY_POOL_BROWSER: "true"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
||||||
|
|
@ -1632,6 +1632,7 @@ async def run_yandex_city_sweep(
|
||||||
matcher: HouseMatcher,
|
matcher: HouseMatcher,
|
||||||
enrichment: EnrichmentJobs,
|
enrichment: EnrichmentJobs,
|
||||||
shutdown_requested: Callable[[], bool] = lambda: False,
|
shutdown_requested: Callable[[], bool] = lambda: False,
|
||||||
|
proxy_provider: ProxyProvider | None = None,
|
||||||
anchors: list[tuple[float, float, str]] | None = None,
|
anchors: list[tuple[float, float, str]] | None = None,
|
||||||
radius_m: int = 25000,
|
radius_m: int = 25000,
|
||||||
pages_per_anchor: int = 2,
|
pages_per_anchor: int = 2,
|
||||||
|
|
@ -1791,7 +1792,7 @@ async def run_yandex_city_sweep(
|
||||||
counters.lots_fetched,
|
counters.lots_fetched,
|
||||||
)
|
)
|
||||||
|
|
||||||
async with YandexRealtyScraper(config) as scraper:
|
async with YandexRealtyScraper(config, proxy_provider=proxy_provider) as scraper:
|
||||||
await scraper.fetch_around_multi_room(
|
await scraper.fetch_around_multi_room(
|
||||||
_a_lat,
|
_a_lat,
|
||||||
_a_lon,
|
_a_lon,
|
||||||
|
|
@ -2216,7 +2217,7 @@ async def run_cian_city_sweep(
|
||||||
nonlocal anchor_lots, consecutive_failures, cian_rotations_done
|
nonlocal anchor_lots, consecutive_failures, cian_rotations_done
|
||||||
|
|
||||||
# ── Phase 1+2: SERP + save ─────────────────────────────────
|
# ── Phase 1+2: SERP + save ─────────────────────────────────
|
||||||
async with CianScraper(config) as scraper:
|
async with CianScraper(config, proxy_provider=proxy_provider) as scraper:
|
||||||
anchor_lots = await scraper.fetch_around_multi_room(
|
anchor_lots = await scraper.fetch_around_multi_room(
|
||||||
_a_lat,
|
_a_lat,
|
||||||
_a_lon,
|
_a_lon,
|
||||||
|
|
@ -2667,7 +2668,7 @@ async def run_cian_full_load(
|
||||||
try:
|
try:
|
||||||
_hb_task = asyncio.create_task(_background_heartbeat())
|
_hb_task = asyncio.create_task(_background_heartbeat())
|
||||||
|
|
||||||
async with CianScraper(config) as scraper:
|
async with CianScraper(config, proxy_provider=proxy_provider) as scraper:
|
||||||
scraper.request_delay_sec = request_delay_sec
|
scraper.request_delay_sec = request_delay_sec
|
||||||
|
|
||||||
# #1949: per-fetch hard-cancel для browser-fetch'ей внутри bucket-gather.
|
# #1949: per-fetch hard-cancel для browser-fetch'ей внутри bucket-gather.
|
||||||
|
|
@ -2865,6 +2866,7 @@ async def run_yandex_full_load(
|
||||||
matcher: HouseMatcher,
|
matcher: HouseMatcher,
|
||||||
enrichment: EnrichmentJobs,
|
enrichment: EnrichmentJobs,
|
||||||
shutdown_requested: Callable[[], bool] = lambda: False,
|
shutdown_requested: Callable[[], bool] = lambda: False,
|
||||||
|
proxy_provider: ProxyProvider | None = None,
|
||||||
price_cap_per_bucket: int = 500,
|
price_cap_per_bucket: int = 500,
|
||||||
request_delay_sec: float = 2.0,
|
request_delay_sec: float = 2.0,
|
||||||
concurrency: int = 4,
|
concurrency: int = 4,
|
||||||
|
|
@ -2965,7 +2967,7 @@ async def run_yandex_full_load(
|
||||||
runs.update_heartbeat(db, run_id, counters.to_dict())
|
runs.update_heartbeat(db, run_id, counters.to_dict())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with YandexRealtyScraper(config) as scraper:
|
async with YandexRealtyScraper(config, proxy_provider=proxy_provider) as scraper:
|
||||||
scraper.request_delay_sec = request_delay_sec
|
scraper.request_delay_sec = request_delay_sec
|
||||||
|
|
||||||
await scraper.fetch_all_secondary(
|
await scraper.fetch_all_secondary(
|
||||||
|
|
|
||||||
|
|
@ -520,6 +520,7 @@ async def _job_yandex_city_sweep(
|
||||||
matcher=ctx.matcher,
|
matcher=ctx.matcher,
|
||||||
enrichment=ctx.enrichment,
|
enrichment=ctx.enrichment,
|
||||||
shutdown_requested=ctx.shutdown_requested,
|
shutdown_requested=ctx.shutdown_requested,
|
||||||
|
proxy_provider=ctx.proxy_provider,
|
||||||
pages_per_anchor=int(params.get("pages_per_anchor", 2)),
|
pages_per_anchor=int(params.get("pages_per_anchor", 2)),
|
||||||
request_delay_sec=float(params.get("request_delay_sec", 9.0)),
|
request_delay_sec=float(params.get("request_delay_sec", 9.0)),
|
||||||
radius_m=int(params.get("radius_m", 1500)),
|
radius_m=int(params.get("radius_m", 1500)),
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ from scraper_kit.repair_state_normalizer import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from scraper_kit.contracts import ScraperConfig
|
from scraper_kit.contracts import ProxyProvider, ScraperConfig
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -118,6 +118,7 @@ class CianScraper(BaseScraper):
|
||||||
config: ScraperConfig,
|
config: ScraperConfig,
|
||||||
*,
|
*,
|
||||||
delay_provider: Callable[[str], float] | None = None,
|
delay_provider: Callable[[str], float] | None = None,
|
||||||
|
proxy_provider: ProxyProvider | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# Strangler-инжекция (#2133): конфиг и провайдер задержки приходят снаружи
|
# Strangler-инжекция (#2133): конфиг и провайдер задержки приходят снаружи
|
||||||
|
|
@ -127,6 +128,9 @@ class CianScraper(BaseScraper):
|
||||||
self._config = config
|
self._config = config
|
||||||
if delay_provider is not None:
|
if delay_provider is not None:
|
||||||
self.request_delay_sec = delay_provider(self.name)
|
self.request_delay_sec = delay_provider(self.name)
|
||||||
|
# #2160: пул прокси для camoufox-браузера (за флагом use_proxy_pool_browser).
|
||||||
|
# None → BrowserFetcher без пула = env-прокси браузера (ship-dark, как сейчас).
|
||||||
|
self._proxy_provider = proxy_provider
|
||||||
self._browser: BrowserFetcher | None = None
|
self._browser: BrowserFetcher | None = None
|
||||||
|
|
||||||
async def __aenter__(self) -> CianScraper:
|
async def __aenter__(self) -> CianScraper:
|
||||||
|
|
@ -136,7 +140,10 @@ class CianScraper(BaseScraper):
|
||||||
# отдельный браузер+мобильный прокси для cian. Прокси ротирует IP сам, поэтому
|
# отдельный браузер+мобильный прокси для cian. Прокси ротирует IP сам, поэтому
|
||||||
# code-side warm-up cookies и changeip-ротация больше не нужны.
|
# code-side warm-up cookies и changeip-ротация больше не нужны.
|
||||||
self._browser = BrowserFetcher(
|
self._browser = BrowserFetcher(
|
||||||
source="cian", endpoint=self._config.browser_http_endpoint
|
source="cian",
|
||||||
|
endpoint=self._config.browser_http_endpoint,
|
||||||
|
proxy_provider=self._proxy_provider,
|
||||||
|
use_pool=self._config.use_proxy_pool_browser,
|
||||||
)
|
)
|
||||||
await self._browser.__aenter__()
|
await self._browser.__aenter__()
|
||||||
return self
|
return self
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ from scraper_kit.price_brackets import get_price_seed_brackets
|
||||||
from scraper_kit.pricing import BisectionConfig, ProbeFailPolicy, ProbeResult, walk_price_range
|
from scraper_kit.pricing import BisectionConfig, ProbeFailPolicy, ProbeResult, walk_price_range
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from scraper_kit.contracts import ScraperConfig
|
from scraper_kit.contracts import ProxyProvider, ScraperConfig
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -461,6 +461,7 @@ class YandexRealtyScraper(BaseScraper):
|
||||||
city: str = DEFAULT_CITY,
|
city: str = DEFAULT_CITY,
|
||||||
*,
|
*,
|
||||||
delay_provider: Callable[[str], float] | None = None,
|
delay_provider: Callable[[str], float] | None = None,
|
||||||
|
proxy_provider: ProxyProvider | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.city = city
|
self.city = city
|
||||||
|
|
@ -471,6 +472,9 @@ class YandexRealtyScraper(BaseScraper):
|
||||||
self._config = config
|
self._config = config
|
||||||
if delay_provider is not None:
|
if delay_provider is not None:
|
||||||
self.request_delay_sec = delay_provider(self.name)
|
self.request_delay_sec = delay_provider(self.name)
|
||||||
|
# #2160: пул прокси для camoufox-браузера (за флагом use_proxy_pool_browser).
|
||||||
|
# None → BrowserFetcher без пула = env-прокси браузера (ship-dark, как сейчас).
|
||||||
|
self._proxy_provider = proxy_provider
|
||||||
self._browser: BrowserFetcher | None = None
|
self._browser: BrowserFetcher | None = None
|
||||||
# _cffi_session retained only for _rotate_ip (changeip call).
|
# _cffi_session retained only for _rotate_ip (changeip call).
|
||||||
self._cffi_session: _CurlCffiSession | None = None
|
self._cffi_session: _CurlCffiSession | None = None
|
||||||
|
|
@ -490,6 +494,8 @@ class YandexRealtyScraper(BaseScraper):
|
||||||
source="yandex",
|
source="yandex",
|
||||||
fetch_timeout_s=_YANDEX_BROWSER_FETCH_TIMEOUT_S,
|
fetch_timeout_s=_YANDEX_BROWSER_FETCH_TIMEOUT_S,
|
||||||
endpoint=self._config.browser_http_endpoint,
|
endpoint=self._config.browser_http_endpoint,
|
||||||
|
proxy_provider=self._proxy_provider,
|
||||||
|
use_pool=self._config.use_proxy_pool_browser,
|
||||||
)
|
)
|
||||||
await self._browser.__aenter__()
|
await self._browser.__aenter__()
|
||||||
return self
|
return self
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue