fix(tradein): route sweep shared-session (SERP+detail) через mobile proxy (#623)

Корень банов city-sweep: pipeline создаёт собственную shared AsyncSession и
присваивает её scraper._cffi, ПЕРЕЗАТИРАЯ проксированную сессию из
AvitoScraper.__aenter__ (#788). Через эту же сессию идёт и SERP, и detail-энричмент
(fetch_detail), и houses. Сессия создавалась без proxies → detail-страницы летели
с datacenter-IP → HTTP 429 → весь run mark_banned (scrape_runs id=9: «Avito detail
HTTP 429 …»). Единичный /admin/scrape работал, т.к. зовёт fetch_around напрямую
(проксированный _cffi из __aenter__), а не pipeline.

- scrape_pipeline.py: helper _avito_proxies() (из settings.avito_proxy_url) →
  proxies= в обе shared AsyncSession (run_avito_pipeline + run_avito_city_sweep).
  Пусто/нет env → прямое подключение (dev no-op). n1/yandex не трогаем (им прокси
  не нужен, не жжём мобильный трафик).
- test: _avito_proxies None без env / {http,https} при заданном URL.

Проверено: 18 passed (anti-bot + serp_date). ruff clean. Требует deploy (cron
city-sweep каждые 2ч на проде продолжит банить detail-429 до выката).

Refs #623, #788, #562.
This commit is contained in:
lekss361 2026-05-30 21:02:26 +03:00
parent a9e4aecb54
commit 5bf650c843
2 changed files with 35 additions and 0 deletions

View file

@ -34,6 +34,7 @@ from curl_cffi.requests import AsyncSession
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.config import settings
from app.services import scrape_runs from app.services import scrape_runs
from app.services.scrapers.avito import AvitoScraper from app.services.scrapers.avito import AvitoScraper
from app.services.scrapers.avito_detail import fetch_detail, save_detail_enrichment from app.services.scrapers.avito_detail import fetch_detail, save_detail_enrichment
@ -66,6 +67,19 @@ _CHROME_HEADERS = {
} }
def _avito_proxies() -> dict[str, str] | None:
"""#623: mobile-proxy egress для shared-сессии sweep'а.
КРИТИЧНО: sweep создаёт собственную shared AsyncSession и присваивает её
`scraper._cffi`, перезатирая проксированную сессию из `AvitoScraper.__aenter__`.
Через эту же сессию идёт и SERP, и detail-энричмент (fetch_detail). Без прокси
detail-страницы летят с datacenter-IP HTTP 429 весь run mark_banned.
Пусто (env не задан) прямое подключение (dev/no-op).
"""
url = settings.avito_proxy_url
return {"http": url, "https": url} if url else None
# ── Result dataclasses ────────────────────────────────────────── # ── Result dataclasses ──────────────────────────────────────────
@ -133,6 +147,7 @@ async def run_avito_pipeline(
impersonate="chrome120", impersonate="chrome120",
timeout=25, timeout=25,
headers=_CHROME_HEADERS, headers=_CHROME_HEADERS,
proxies=_avito_proxies(),
) )
try: try:
@ -351,6 +366,7 @@ async def run_avito_city_sweep(
impersonate="chrome120", impersonate="chrome120",
timeout=25, timeout=25,
headers=_CHROME_HEADERS, headers=_CHROME_HEADERS,
proxies=_avito_proxies(),
) as session: ) as session:
try: try:
for idx, (lat, lon, name) in enumerate(_anchors, start=1): for idx, (lat, lon, name) in enumerate(_anchors, start=1):

View file

@ -411,3 +411,22 @@ async def test_fetch_around_blocked_when_rotation_exhausted(
await scraper.fetch_around(56.84, 60.60, 1000, pages=1) await scraper.fetch_around(56.84, 60.60, 1000, pages=1)
assert rot.await_count == 1 # одна попытка ротации, потом сдаёмся assert rot.await_count == 1 # одна попытка ротации, потом сдаёмся
# ── #623 sweep-pipeline proxy (SERP+detail через mobile proxy) ───
def test_avito_proxies_helper(monkeypatch: pytest.MonkeyPatch) -> None:
"""_avito_proxies: None без env, dict {http,https} при заданном AVITO_PROXY_URL.
Регрессия: sweep создаёт shared-сессию, через неё идут и SERP, и detail
без прокси detail летит с datacenter-IP 429 run banned (#623 followup).
"""
from app.services import scrape_pipeline as sp
monkeypatch.setattr(sp.settings, "avito_proxy_url", None)
assert sp._avito_proxies() is None
url = "http://u:p@mproxy.site:16928"
monkeypatch.setattr(sp.settings, "avito_proxy_url", url)
assert sp._avito_proxies() == {"http": url, "https": url}