Скрапер: newbuilding-провайдеры без config больше не уходят мимо пула прокси, IMV Авито строит сессию общей фабрикой #3562
3 changed files with 31 additions and 68 deletions
|
|
@ -36,6 +36,7 @@ import pytest
|
|||
# Mirror tests/test_scraper_kit_pricehistory_session_parity.py.
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||||
|
||||
from scraper_kit.providers._base import DEFAULT_IMPERSONATE, DOCUMENT_HEADERS
|
||||
from scraper_kit.providers.avito.imv import (
|
||||
IMVAddressNotFoundError as KitIMVAddressNotFoundError,
|
||||
)
|
||||
|
|
@ -102,7 +103,7 @@ def test_kit_evaluate_via_imv_without_config_drops_proxy(monkeypatch: pytest.Mon
|
|||
captured.update(kwargs)
|
||||
return mock_session
|
||||
|
||||
with patch("curl_cffi.requests.AsyncSession", _fake_session):
|
||||
with patch("scraper_kit.providers._base.AsyncSession", _fake_session):
|
||||
asyncio.run(_call_kit_own_session(config=None))
|
||||
|
||||
assert captured.get("proxies") is None
|
||||
|
|
@ -123,10 +124,15 @@ def test_kit_evaluate_via_imv_with_config_uses_proxy(monkeypatch: pytest.MonkeyP
|
|||
captured.update(kwargs)
|
||||
return mock_session
|
||||
|
||||
with patch("curl_cffi.requests.AsyncSession", _fake_session):
|
||||
with patch("scraper_kit.providers._base.AsyncSession", _fake_session):
|
||||
asyncio.run(_call_kit_own_session(config=RealScraperConfig()))
|
||||
|
||||
assert captured.get("proxies") == {
|
||||
"http": "http://test-proxy.local:8080",
|
||||
"https": "http://test-proxy.local:8080",
|
||||
}
|
||||
# #2386: сессия строится общей build_document_session — параметры те же, что были у
|
||||
# собственной (impersonate/timeout=25/document-заголовки).
|
||||
assert captured.get("impersonate") == DEFAULT_IMPERSONATE
|
||||
assert captured.get("timeout") == 25
|
||||
assert captured.get("headers") == DOCUMENT_HEADERS
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ def test_empty_pool_in_production_degrades_without_imv(
|
|||
# патч обязан бить по РЕАЛЬНОМУ атрибуту. С create=True переименование импорта
|
||||
# оставило бы тест зелёным против фантома.
|
||||
patch.object(estimator, "RealProxyProvider", _EmptyPoolProvider),
|
||||
patch("curl_cffi.requests.AsyncSession", _no_http),
|
||||
patch("scraper_kit.providers._base.AsyncSession", _no_http),
|
||||
):
|
||||
result = await _call(_db_cache_miss(), address="ЕКБ, ул. Тургенева, 4")
|
||||
|
||||
|
|
@ -213,7 +213,7 @@ def _run_with_pool(
|
|||
with (
|
||||
patch.object(estimator, "RealProxyProvider", lambda: provider),
|
||||
patch.object(estimator, "save_imv_evaluation", return_value=1),
|
||||
patch("curl_cffi.requests.AsyncSession", lambda *a, **kw: session),
|
||||
patch("scraper_kit.providers._base.AsyncSession", lambda *a, **kw: session),
|
||||
# Транспорт нам не интересен — проверяем жизненный цикл lease вокруг него.
|
||||
patch("scraper_kit.providers.avito.imv._warmup", new=AsyncMock()),
|
||||
patch(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from uuid import UUID
|
|||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from scraper_kit.providers._base import DEFAULT_IMPERSONATE
|
||||
from scraper_kit.providers._base import DOCUMENT_HEADERS, build_document_session
|
||||
from scraper_kit.providers._proxy import acurl_proxy_url
|
||||
from scraper_kit.providers.avito.shared import _unix_to_date
|
||||
|
||||
|
|
@ -54,19 +54,6 @@ IMV_ENDPOINT = "/web/1/realty-imv/get-data"
|
|||
# Тёплая страница для seed anti-bot cookies (srv_id/_avisc/u/...) перед XHR.
|
||||
WARMUP_URL = f"{AVITO_BASE}/evaluation/realty"
|
||||
|
||||
# Заголовки document-навигации (warm-up GET) — зеркалят production-набор
|
||||
# из avito.py (AvitoScraper.__aenter__). Нужны чтобы пройти TLS+header anti-bot.
|
||||
_DOC_HEADERS = {
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
|
||||
"Cache-Control": "max-age=0",
|
||||
"Sec-Fetch-Dest": "document",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "none",
|
||||
"Sec-Fetch-User": "?1",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
}
|
||||
|
||||
# Заголовки XHR/fetch для API-шагов (coords/position/get-data). Имитируют
|
||||
# fetch() со страницы /evaluation/realty: JSON Accept + same-origin Sec-Fetch.
|
||||
_COMMON_HEADERS = {
|
||||
|
|
@ -486,8 +473,7 @@ async def evaluate_via_imv(
|
|||
# камуфокс in-page fetch (same-origin avito.ru), а не curl_cffi. Прокси +
|
||||
# warmed-cookies + реальный fingerprint из sidecar обходят datacenter-403
|
||||
# (#562/#853). _own_session=True → finally дёрнет adapter.close() (no-op,
|
||||
# браузер принадлежит caller'у — backfill открывает один на батч). Этот путь
|
||||
# НЕ требует curl_cffi, поэтому импорт пакета остаётся только в else-ветке.
|
||||
# браузер принадлежит caller'у — backfill открывает один на батч).
|
||||
# AsyncExitStack держит прокси-lease пула (#2163) на всё время own-session: warm-up +
|
||||
# geocode + evaluate. Регистрируется ТОЛЬКО когда сами создаём curl_cffi-сессию;
|
||||
# для browser_fetcher / переданной cffi_session — no-op. Исключение из блока
|
||||
|
|
@ -499,53 +485,24 @@ async def evaluate_via_imv(
|
|||
cffi_session = _BrowserSessionAdapter(browser_fetcher, origin=WARMUP_URL)
|
||||
_own_session = True
|
||||
else:
|
||||
# Импортируем здесь чтобы избежать циклических зависимостей при тестах без сети
|
||||
try:
|
||||
from curl_cffi.requests import AsyncSession as CffiAsyncSession
|
||||
|
||||
_own_session = False
|
||||
if cffi_session is None:
|
||||
# Зеркалим production-набор из AvitoScraper.__aenter__ (avito.py:150-162):
|
||||
# impersonate=DEFAULT_IMPERSONATE TLS + document-заголовки + timeout.
|
||||
# Затем warm-up GET для seed anti-bot cookies — bare-session XHR Avito
|
||||
# банит на server-IP.
|
||||
# Прокси: пул за флагом use_proxy_pool_curl (#2163), иначе env
|
||||
# scraper_proxy_url. proxy=None → прямое подключение (dev).
|
||||
#
|
||||
# NB (#2361 F4a): НЕ мигрировано на build_document_session() из
|
||||
# scraper_kit.providers._base, хотя headers=_DOC_HEADERS — тот же
|
||||
# дублированный dict, что и там. _base.py делает module-level
|
||||
# `from curl_cffi.requests import AsyncSession` (биндится ОДИН раз
|
||||
# при первом импорте _base) — tests/scrapers/test_avito_imv_kit_parity.py
|
||||
# (#2334) патчит `curl_cffi.requests.AsyncSession` через unittest.mock,
|
||||
# что работает ТОЛЬКО если конструктор вызывается через СВЕЖИЙ lookup
|
||||
# атрибута на живом module-объекте (как этот локальный import ниже),
|
||||
# а не через имя, захваченное в чужом module namespace на момент его
|
||||
# импорта. Мигрировав, ловим 2 real failures
|
||||
# (test_kit_evaluate_via_imv_{with,without}_config_uses_proxy) — НЕ
|
||||
# тестовый шум, тест был зелёным на main. Production-поведение
|
||||
# идентично в обоих вариантах (сеть не отличает откуда взят класс),
|
||||
# но так как таск прямо запрещает "weaken or skip" parity-тесты —
|
||||
# оставляю это единственное место немигрированным и репортю как
|
||||
# finding, а не тихо чиню тест.
|
||||
_env = config.scraper_proxy_url if config is not None else None
|
||||
# acurl_proxy_url (#3398): операции пула в потоке — синхронный вход
|
||||
# стоял ДО первого await и держал event loop `/estimate`.
|
||||
_proxy_url = await _proxy_stack.enter_async_context(
|
||||
acurl_proxy_url(config, proxy_provider, "avito", env_fallback_url=_env)
|
||||
)
|
||||
_proxies = {"http": _proxy_url, "https": _proxy_url} if _proxy_url else None
|
||||
cffi_session = CffiAsyncSession(
|
||||
impersonate=DEFAULT_IMPERSONATE,
|
||||
timeout=_HTTP_TIMEOUT_SEC,
|
||||
proxies=_proxies,
|
||||
headers=_DOC_HEADERS,
|
||||
)
|
||||
_own_session = True
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"curl_cffi не установлен. Добавь 'curl-cffi>=0.7.0' в pyproject.toml."
|
||||
) from exc
|
||||
_own_session = False
|
||||
if cffi_session is None:
|
||||
# Общая document-сессия из _base (#2386): impersonate=DEFAULT_IMPERSONATE TLS +
|
||||
# DOCUMENT_HEADERS + timeout. Затем warm-up GET для seed anti-bot cookies —
|
||||
# bare-session XHR Avito банит на server-IP.
|
||||
# Прокси: пул за флагом use_proxy_pool_curl (#2163), иначе env
|
||||
# scraper_proxy_url. proxy=None → прямое подключение (dev).
|
||||
# Тесты подменяют конструктор через `scraper_kit.providers._base.AsyncSession`.
|
||||
_env = config.scraper_proxy_url if config is not None else None
|
||||
# acurl_proxy_url (#3398): операции пула в потоке — синхронный вход
|
||||
# стоял ДО первого await и держал event loop `/estimate`.
|
||||
_proxy_url = await _proxy_stack.enter_async_context(
|
||||
acurl_proxy_url(config, proxy_provider, "avito", env_fallback_url=_env)
|
||||
)
|
||||
cffi_session = build_document_session(
|
||||
proxy_url=_proxy_url, timeout=_HTTP_TIMEOUT_SEC
|
||||
)
|
||||
_own_session = True
|
||||
|
||||
try:
|
||||
if _own_session:
|
||||
|
|
@ -579,7 +536,7 @@ async def _warmup(session: Any) -> None:
|
|||
всё равно попробуют и дадут типизированную ошибку.
|
||||
"""
|
||||
try:
|
||||
resp = await session.get(WARMUP_URL, headers=_DOC_HEADERS)
|
||||
resp = await session.get(WARMUP_URL, headers=DOCUMENT_HEADERS)
|
||||
logger.info("IMV warm-up GET /evaluation/realty → HTTP %d", resp.status_code)
|
||||
except Exception as exc:
|
||||
# warm-up опционален: логируем и продолжаем — шаги ниже дадут типизированную
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue