refactor(tradein/scraper-kit): avito providers on shared _base.py (#2361) #2375

Merged
lekss361 merged 1 commit from feat/tradein-scraper-kit-f4a-avito into main 2026-07-04 06:35:00 +00:00
3 changed files with 22 additions and 36 deletions

View file

@ -40,6 +40,7 @@ from scraper_kit.avito_exceptions import (
AvitoRateLimitedError, AvitoRateLimitedError,
) )
from scraper_kit.geo import is_within_ekb_bbox_wide from scraper_kit.geo import is_within_ekb_bbox_wide
from scraper_kit.providers._base import build_document_session
from scraper_kit.providers.avito.serp import _clean_address, _is_firewall_page from scraper_kit.providers.avito.serp import _clean_address, _is_firewall_page
from scraper_kit.providers.avito.shared import RUS_MONTHS from scraper_kit.providers.avito.shared import RUS_MONTHS
from scraper_kit.repair_state_normalizer import infer_repair_state_from_text from scraper_kit.repair_state_normalizer import infer_repair_state_from_text
@ -265,22 +266,7 @@ def _build_detail_session(config: ScraperConfig | None = None) -> AsyncSession:
(scraper_kit.contracts) вместо прямого импорта app.core.config.settings. (scraper_kit.contracts) вместо прямого импорта app.core.config.settings.
""" """
proxy_url = config.scraper_proxy_url if config is not None else None proxy_url = config.scraper_proxy_url if config is not None else None
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None return build_document_session(proxy_url=proxy_url, timeout=25)
return AsyncSession(
impersonate="chrome120",
timeout=25,
proxies=proxies,
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",
},
)
async def warm_up_session(session: AsyncSession) -> bool: async def warm_up_session(session: AsyncSession) -> bool:

View file

@ -483,6 +483,23 @@ async def evaluate_via_imv(
# seed anti-bot cookies — bare-session XHR Avito банит на server-IP. # seed anti-bot cookies — bare-session XHR Avito банит на server-IP.
# Прокси: пул за флагом use_proxy_pool_curl (#2163), иначе env # Прокси: пул за флагом use_proxy_pool_curl (#2163), иначе env
# scraper_proxy_url. proxy=None → прямое подключение (dev). # 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 _env = config.scraper_proxy_url if config is not None else None
_proxy_url = _proxy_stack.enter_context( _proxy_url = _proxy_stack.enter_context(
curl_proxy_url(config, proxy_provider, "avito", env_fallback_url=_env) curl_proxy_url(config, proxy_provider, "avito", env_fallback_url=_env)

View file

@ -47,6 +47,7 @@ from scraper_kit.base import BaseScraper, ScrapedLot
from scraper_kit.browser_fetcher import BrowserFetcher from scraper_kit.browser_fetcher import BrowserFetcher
from scraper_kit.price_brackets import get_price_seed_brackets 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
from scraper_kit.providers._base import build_browser_fetcher, build_document_session
from scraper_kit.providers.avito.shared import _unix_to_date from scraper_kit.providers.avito.shared import _unix_to_date
if TYPE_CHECKING: if TYPE_CHECKING:
@ -386,9 +387,7 @@ class AvitoScraper(BaseScraper):
# FALLBACK — если браузер отдаёт firewall/пусто, _fetch_serp_html делает # FALLBACK — если браузер отдаёт firewall/пусто, _fetch_serp_html делает
# один retry через curl_cffi. _build_cffi_session самодостаточен # один retry через curl_cffi. _build_cffi_session самодостаточен
# (отдельный AsyncSession, не зависит от BaseScraper.__aenter__). # (отдельный AsyncSession, не зависит от BaseScraper.__aenter__).
self._browser = BrowserFetcher( self._browser = build_browser_fetcher(self._config, "avito")
source="avito", endpoint=self._config.browser_http_endpoint
)
await self._browser.__aenter__() await self._browser.__aenter__()
self._cffi = self._build_cffi_session() self._cffi = self._build_cffi_session()
logger.info( logger.info(
@ -407,25 +406,9 @@ class AvitoScraper(BaseScraper):
Вынесено из __aenter__ чтобы _reset_cffi мог пересоздать сессию (новый Вынесено из __aenter__ чтобы _reset_cffi мог пересоздать сессию (новый
CONNECT-туннель) для escape 403 на залоченном backconnect exit-IP. CONNECT-туннель) для escape 403 на залоченном backconnect exit-IP.
""" """
proxies = None
if self._config.scraper_proxy_url: if self._config.scraper_proxy_url:
proxies = {"http": self._config.scraper_proxy_url, "https": self._config.scraper_proxy_url}
logger.info("avito: routing through mobile proxy egress (#623)") logger.info("avito: routing through mobile proxy egress (#623)")
return AsyncSession( return build_document_session(proxy_url=self._config.scraper_proxy_url, timeout=25)
impersonate="chrome120",
timeout=25,
proxies=proxies,
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",
},
)
async def _reset_cffi(self) -> None: async def _reset_cffi(self) -> None:
"""Закрыть и пересоздать curl_cffi-сессию — новый CONNECT-туннель через """Закрыть и пересоздать curl_cffi-сессию — новый CONNECT-туннель через