refactor(tradein/scraper-kit): avito providers on shared _base.py (#2361)
All checks were successful
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m44s
All checks were successful
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m44s
Мигрирует avito/{serp,detail}.py на build_browser_fetcher()/
build_document_session() из scraper_kit.providers._base (Foundation #2358):
- serp.py: BrowserFetcher(source="avito", endpoint=...) в __aenter__ ->
build_browser_fetcher(config, "avito"); _build_cffi_session() (25s
timeout, document headers, scraper_proxy_url) -> build_document_session().
- detail.py: _build_detail_session() (тот же document-headers dict,
25s timeout) -> build_document_session(proxy_url=..., timeout=25).
Caveat (#2361, проверено explicit): serp.py __aenter__ раньше конструировал
BrowserFetcher БЕЗ proxy_provider/use_pool (implicit None/False). После
миграции use_pool читает config.use_proxy_pool_browser вместо hardcoded
False - НО proxy_provider остаётся None (build_browser_fetcher default), а
BrowserFetcher._use_pool гейтится `and self._proxy_provider is not None`
(browser_fetcher.py:215) - при proxy_provider=None use_pool всегда
эффективно False независимо от config. Verified: behavior-preserving,
не тихая смена прод-поведения.
imv.py: НЕ мигрирован (единственное отклонение от issue). Own-session
curl_cffi construction (headers=_DOC_HEADERS - тот же дублированный dict)
технически подходит под миграцию, но build_document_session() резолвит
AsyncSession через module-level импорт _base.py (biндится один раз при
первом импорте _base), а tests/scrapers/test_avito_imv_kit_parity.py
(#2334) патчит curl_cffi.requests.AsyncSession через unittest.mock -
это работает только с локальным call-time импортом (как было). Миграция
роняет 2 реальных теста (test_kit_evaluate_via_imv_{with,without}_config_
uses_proxy) - не шум, зелёные на main. Прод-поведение идентично в обоих
вариантах, но задача явно запрещает weaken/skip parity-тесты - оставлено
как есть + inline-комментарий с находкой. Follow-up для будущих групп
(cian/yandex/domclick F4b-d): тот же паттерн теста сломает миграцию любого
provider с аналогичным unittest.mock.patch("curl_cffi.requests.AsyncSession").
Tests: full backend suite 3278 passed, 1 known pre-existing flake
(test_search_api.py::test_search_cache_hit, #2208), 6 skipped. Все avito
kit_parity/golden_parity тесты + test_scraper_kit_pipeline_parity2.py
(cross-provider) зелёные.
This commit is contained in:
parent
53e7834f19
commit
b445c34863
3 changed files with 22 additions and 36 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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-туннель через
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue