fix(tradein/scraper): per-city SERP location for oblast city-sweep (#12) (#2525)
All checks were successful
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m37s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 5m1s
Deploy Trade-In / deploy (push) Successful in 6m47s
All checks were successful
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m37s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 5m1s
Deploy Trade-In / deploy (push) Successful in 6m47s
Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local>
This commit is contained in:
parent
67b50bf713
commit
698576e77b
6 changed files with 206 additions and 21 deletions
104
tradein-mvp/backend/tests/test_serp_city_location_urls.py
Normal file
104
tradein-mvp/backend/tests/test_serp_city_location_urls.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""Per-city SERP location URL builders (#12 fix EKB-leak в oblast city-sweep).
|
||||
|
||||
Проблема: avito/cian/yandex провайдеры хардкодили ЕКБ-локацию (путь /ekaterinburg/,
|
||||
CIAN_EKB_REGION_ID, _EKB_RGID) мимо anchor'а oblast-города → city-sweep целевого
|
||||
города (напр. Н.Тагил) фактически скрейпил ЕКБ, n_analogs=0. Фикс — новые
|
||||
kw-параметры (avito: target_city_slug уже был #2487, теперь и в ПУТИ URL;
|
||||
cian: city_region_id; yandex: city_rgid), все дефолт None → ЕКБ-поведение
|
||||
byte-identical прежнему (back-compat critical).
|
||||
|
||||
No network / no DB — чистые URL-билдеры на stub-конфиге.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||
|
||||
from scraper_kit.providers.avito.serp import NOVOSTROYKA_SLUG, AvitoScraper
|
||||
from scraper_kit.providers.cian.serp import CIAN_EKB_REGION_ID, CianScraper
|
||||
from scraper_kit.providers.yandex.serp import _EKB_RGID, YandexRealtyScraper
|
||||
|
||||
_STUB_CFG = SimpleNamespace()
|
||||
|
||||
|
||||
def _query(url: str) -> dict[str, list[str]]:
|
||||
return parse_qs(urlparse(url).query)
|
||||
|
||||
|
||||
# ── Avito: city-slug в ПУТЬ URL (не только SERP-фильтр) ────────────────────────
|
||||
|
||||
|
||||
def test_avito_web_url_uses_target_city_slug_in_path() -> None:
|
||||
s = AvitoScraper(_STUB_CFG, target_city_slug="nizhniy_tagil") # type: ignore[arg-type]
|
||||
url = s._build_web_url(57.910, 59.980, radius_km=2, page=1)
|
||||
assert "/nizhniy_tagil/" in url
|
||||
assert "/ekaterinburg/" not in url
|
||||
|
||||
|
||||
def test_avito_citywide_url_uses_target_city_slug_in_path() -> None:
|
||||
s = AvitoScraper(_STUB_CFG, target_city_slug="kamensk-uralskiy") # type: ignore[arg-type]
|
||||
url = s._build_citywide_url(page=2)
|
||||
assert "/kamensk-uralskiy/" in url
|
||||
assert "/ekaterinburg/" not in url
|
||||
|
||||
|
||||
def test_avito_newbuilding_url_uses_target_city_slug_in_path() -> None:
|
||||
s = AvitoScraper(_STUB_CFG, target_city_slug="verhnyaya_pyshma") # type: ignore[arg-type]
|
||||
url = s._build_newbuilding_url(page=1)
|
||||
assert "/verhnyaya_pyshma/" in url
|
||||
assert "/ekaterinburg/" not in url
|
||||
assert NOVOSTROYKA_SLUG in url
|
||||
|
||||
|
||||
def test_avito_rooms_url_uses_target_city_slug_in_path() -> None:
|
||||
s = AvitoScraper(_STUB_CFG, target_city_slug="serov") # type: ignore[arg-type]
|
||||
url = s._build_rooms_url("2k", page=1)
|
||||
assert "/serov/" in url
|
||||
assert "/ekaterinburg/" not in url
|
||||
|
||||
|
||||
def test_avito_urls_default_to_ekaterinburg_when_no_target_city_slug() -> None:
|
||||
"""Back-compat: target_city_slug=None → путь /ekaterinburg/ (ЕКБ full_load/pipeline)."""
|
||||
s = AvitoScraper(_STUB_CFG, target_city_slug=None) # type: ignore[arg-type]
|
||||
assert "/ekaterinburg/" in s._build_web_url(56.84, 60.60, radius_km=1, page=1)
|
||||
assert "/ekaterinburg/" in s._build_citywide_url(page=1)
|
||||
assert "/ekaterinburg/" in s._build_newbuilding_url(page=1)
|
||||
assert "/ekaterinburg/" in s._build_rooms_url("2k", page=1)
|
||||
|
||||
|
||||
# ── Cian: city_region_id → region= (subdomain НЕ трогается) ────────────────────
|
||||
|
||||
|
||||
def test_cian_build_url_uses_city_region_id() -> None:
|
||||
s = CianScraper(_STUB_CFG, city_region_id=4886) # type: ignore[arg-type]
|
||||
url = s._build_url()
|
||||
assert _query(url)["region"] == ["4886"]
|
||||
assert urlparse(url).hostname == "ekb.cian.ru"
|
||||
|
||||
|
||||
def test_cian_build_url_defaults_to_ekb_region_when_no_city_region_id() -> None:
|
||||
"""Back-compat: city_region_id=None → region=CIAN_EKB_REGION_ID (4743)."""
|
||||
s = CianScraper(_STUB_CFG, city_region_id=None) # type: ignore[arg-type]
|
||||
url = s._build_url()
|
||||
assert _query(url)["region"] == [str(CIAN_EKB_REGION_ID)]
|
||||
assert urlparse(url).hostname == "ekb.cian.ru"
|
||||
|
||||
|
||||
# ── Yandex: city_rgid → rgid= ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_yandex_build_url_uses_city_rgid() -> None:
|
||||
s = YandexRealtyScraper(_STUB_CFG, city_rgid=328286) # type: ignore[arg-type]
|
||||
url = s._build_url()
|
||||
assert _query(url)["rgid"] == ["328286"]
|
||||
|
||||
|
||||
def test_yandex_build_url_defaults_to_ekb_rgid_when_no_city_rgid() -> None:
|
||||
"""Back-compat: city_rgid=None → rgid=_EKB_RGID (559132)."""
|
||||
s = YandexRealtyScraper(_STUB_CFG, city_rgid=None) # type: ignore[arg-type]
|
||||
url = s._build_url()
|
||||
assert _query(url)["rgid"] == [str(_EKB_RGID)]
|
||||
|
|
@ -310,6 +310,39 @@ def get_city_anchors(city_slug: str | None) -> list[tuple[float, float, str]] |
|
|||
return CITY_ANCHORS.get(city_slug)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CityLocation:
|
||||
"""Per-provider гео-идентификаторы города-цели SERP-запроса (oblast rollout).
|
||||
|
||||
avito_slug — city-slug в пути avito URL (ОТЛИЧАЕТСЯ от нашего slug у некоторых
|
||||
городов: kamensk-uralskiy через дефис, verhnyaya_pyshma без kh).
|
||||
cian_region_id — числовой region= в ekb.cian.ru/cat.php (ГОРОД, не область).
|
||||
yandex_rgid — rgid в gate-API Яндекс.Недвижимости (город).
|
||||
"""
|
||||
|
||||
avito_slug: str
|
||||
cian_region_id: int
|
||||
yandex_rgid: int
|
||||
|
||||
|
||||
# Референс ЕКБ (avito ekaterinburg / cian 4743 / yandex 559132) — дефолты в провайдерах,
|
||||
# в карте держать НЕ обязательно (None-путь = ЕКБ). Города Свердловской обл. (region 66):
|
||||
CITY_LOCATIONS: dict[str, CityLocation] = {
|
||||
"nizhniy_tagil": CityLocation("nizhniy_tagil", 4886, 328286),
|
||||
"kamensk_uralskiy": CityLocation("kamensk-uralskiy", 4781, 328468),
|
||||
"pervouralsk": CityLocation("pervouralsk", 4925, 328379),
|
||||
"verkhnyaya_pyshma": CityLocation("verhnyaya_pyshma", 4697, 166366),
|
||||
"serov": CityLocation("serov", 4982, 328238),
|
||||
}
|
||||
|
||||
|
||||
def get_city_location(city_slug: str | None) -> CityLocation | None:
|
||||
"""CityLocation по slug (CITY_LOCATIONS). Неизвестный slug/None → None (=ЕКБ-дефолт)."""
|
||||
if city_slug is None:
|
||||
return None
|
||||
return CITY_LOCATIONS.get(city_slug)
|
||||
|
||||
|
||||
_CHROME_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",
|
||||
|
|
@ -855,9 +888,14 @@ async def run_avito_city_sweep(
|
|||
зовётся через enrichment (EnrichmentJobs Protocol).
|
||||
"""
|
||||
# city_slug (#2487): слаг города-цели (напр. "nizhniy_tagil") прокидывается в
|
||||
# AvitoScraper.target_city_slug → SERP-фильтр оставляет карточки этого города.
|
||||
# AvitoScraper.target_city_slug → SERP-фильтр оставляет карточки этого города,
|
||||
# а avito_slug (#12) — в путь URL, скоупя сам запрос на город-цель вместо ЕКБ.
|
||||
# None → ЕКБ-дефолт (совпадает с anchors=EKB_ANCHORS fallback ниже).
|
||||
_anchors = anchors if anchors is not None else EKB_ANCHORS
|
||||
_loc = get_city_location(city_slug)
|
||||
# kamensk-uralskiy (дефис) / verhnyaya_pyshma (kh→h) отличаются от нашего city_slug —
|
||||
# вычисляем один раз до цикла anchor'ов, не внутри closure на каждый anchor.
|
||||
_avito_slug = _loc.avito_slug if _loc else city_slug
|
||||
counters = CitySweepCounters(anchors_total=len(_anchors))
|
||||
all_touched_house_ids: set[int] = set()
|
||||
|
||||
|
|
@ -964,8 +1002,9 @@ async def run_avito_city_sweep(
|
|||
|
||||
# ── Phase 1+2: SERP + save ─────────────────────────────────
|
||||
# #2487: target_city_slug → _parse_html оставляет карточки
|
||||
# города-цели (oblast slug), а не хардкод /ekaterinburg/. None → ЕКБ.
|
||||
scraper = AvitoScraper(config, target_city_slug=city_slug)
|
||||
# города-цели (oblast slug). #12: _avito_slug (может отличаться
|
||||
# от city_slug) идёт в путь URL — не хардкод /ekaterinburg/. None → ЕКБ.
|
||||
scraper = AvitoScraper(config, target_city_slug=_avito_slug)
|
||||
if browser_mode:
|
||||
scraper._browser = shared_bf
|
||||
# Shared-browser режим: _cffi=None → curl_cffi-fallback на
|
||||
|
|
@ -1706,6 +1745,7 @@ async def run_yandex_city_sweep(
|
|||
shutdown_requested: Callable[[], bool] = lambda: False,
|
||||
proxy_provider: ProxyProvider | None = None,
|
||||
anchors: list[tuple[float, float, str]] | None = None,
|
||||
city_slug: str | None = None,
|
||||
radius_m: int = 25000,
|
||||
pages_per_anchor: int = 2,
|
||||
request_delay_sec: float | None = None,
|
||||
|
|
@ -1739,6 +1779,10 @@ async def run_yandex_city_sweep(
|
|||
else:
|
||||
_anchors = [(56.8400, 60.6050, "Центр (combos)")]
|
||||
|
||||
# city_slug (#12): rgid города-цели → YandexRealtyScraper.city_rgid скоупит SERP
|
||||
# на город вместо дефолтного ЕКБ. None/неизвестный slug → ЕКБ-дефолт в конструкторе.
|
||||
_loc = get_city_location(city_slug)
|
||||
|
||||
_rooms_list = rooms_list or list(ROOM_PATH.keys())
|
||||
_price_ranges = price_ranges or DEFAULT_PRICE_RANGES
|
||||
# newFlat-сегменты: оба прохода (vtorichka + novostroyki) по одним combos.
|
||||
|
|
@ -1864,7 +1908,11 @@ async def run_yandex_city_sweep(
|
|||
counters.lots_fetched,
|
||||
)
|
||||
|
||||
async with YandexRealtyScraper(config, proxy_provider=proxy_provider) as scraper:
|
||||
async with YandexRealtyScraper(
|
||||
config,
|
||||
proxy_provider=proxy_provider,
|
||||
city_rgid=(_loc.yandex_rgid if _loc else None),
|
||||
) as scraper:
|
||||
await scraper.fetch_around_multi_room(
|
||||
_a_lat,
|
||||
_a_lon,
|
||||
|
|
@ -2213,6 +2261,7 @@ async def run_cian_city_sweep(
|
|||
shutdown_requested: Callable[[], bool] = lambda: False,
|
||||
proxy_provider: ProxyProvider | None = None,
|
||||
anchors: list[tuple[float, float, str]] | None = None,
|
||||
city_slug: str | None = None,
|
||||
radius_m: int = 1500,
|
||||
pages_per_anchor: int = 3,
|
||||
request_delay_sec: float = 5.0,
|
||||
|
|
@ -2236,6 +2285,9 @@ async def run_cian_city_sweep(
|
|||
mark_done вызывается ВСЕГДА (finally outer).
|
||||
"""
|
||||
_anchors = anchors if anchors is not None else EKB_ANCHORS
|
||||
# city_slug (#12): region_id города-цели → CianScraper.city_region_id скоупит SERP
|
||||
# на город вместо дефолтного ЕКБ. None/неизвестный slug → ЕКБ-дефолт в конструкторе.
|
||||
_loc = get_city_location(city_slug)
|
||||
counters = CianCitySweepCounters(anchors_total=len(_anchors))
|
||||
consecutive_failures = 0
|
||||
cian_rotations_done = 0 # #1848: бюджет IP-ротаций на весь sweep
|
||||
|
|
@ -2310,7 +2362,11 @@ async def run_cian_city_sweep(
|
|||
nonlocal anchor_lots, consecutive_failures, cian_rotations_done
|
||||
|
||||
# ── Phase 1+2: SERP + save ─────────────────────────────────
|
||||
async with CianScraper(config, proxy_provider=proxy_provider) as scraper:
|
||||
async with CianScraper(
|
||||
config,
|
||||
proxy_provider=proxy_provider,
|
||||
city_region_id=(_loc.cian_region_id if _loc else None),
|
||||
) as scraper:
|
||||
anchor_lots = await scraper.fetch_around_multi_room(
|
||||
_a_lat,
|
||||
_a_lon,
|
||||
|
|
|
|||
|
|
@ -538,6 +538,7 @@ async def _job_yandex_city_sweep(
|
|||
shutdown_requested=ctx.shutdown_requested,
|
||||
proxy_provider=ctx.proxy_provider,
|
||||
anchors=anchors,
|
||||
city_slug=city,
|
||||
pages_per_anchor=int(params.get("pages_per_anchor", 2)),
|
||||
request_delay_sec=float(params.get("request_delay_sec", 9.0)),
|
||||
radius_m=int(params.get("radius_m", 1500)),
|
||||
|
|
@ -560,6 +561,7 @@ async def _job_cian_city_sweep(
|
|||
shutdown_requested=ctx.shutdown_requested,
|
||||
proxy_provider=ctx.proxy_provider,
|
||||
anchors=anchors,
|
||||
city_slug=city,
|
||||
pages_per_anchor=int(params.get("pages_per_anchor", 3)),
|
||||
request_delay_sec=float(params.get("request_delay_sec", 5.0)),
|
||||
radius_m=int(params.get("radius_m", 1500)),
|
||||
|
|
|
|||
|
|
@ -859,6 +859,11 @@ class AvitoScraper(BaseScraper):
|
|||
return all_lots
|
||||
|
||||
# ── Strategy B: HTML scrape ─────────────────────────────────────────────
|
||||
def _city_seg(self) -> str:
|
||||
"""City-slug для пути avito URL (#12: раньше хардкод /ekaterinburg/ во всех
|
||||
билдерах скоупил oblast city-sweep на ЕКБ, невзирая на geoCoords/фильтр)."""
|
||||
return self._target_city_slug or "ekaterinburg"
|
||||
|
||||
def _build_web_url(self, lat: float, lon: float, radius_km: int, page: int = 1) -> str:
|
||||
params = {
|
||||
"geoCoords": f"{lat},{lon}",
|
||||
|
|
@ -866,24 +871,30 @@ class AvitoScraper(BaseScraper):
|
|||
"s": 104, # sort by date
|
||||
"p": page,
|
||||
}
|
||||
return f"{self.base_url}/ekaterinburg/kvartiry/prodam-ASgBAgICAUSSA8YQ?{urlencode(params)}"
|
||||
return (
|
||||
f"{self.base_url}/{self._city_seg()}/kvartiry/prodam-ASgBAgICAUSSA8YQ"
|
||||
f"?{urlencode(params)}"
|
||||
)
|
||||
|
||||
def _build_citywide_url(self, page: int = 1) -> str:
|
||||
"""T6: URL всего города ЕКБ без geo-фильтра, сортировка по дате.
|
||||
"""T6: URL всего города (по умолчанию ЕКБ) без geo-фильтра, сортировка по дате.
|
||||
|
||||
Avito отдаёт все объявления города (cap ~5000 = 100 страниц × 50 карточек).
|
||||
"""
|
||||
params = {"s": 104, "p": page}
|
||||
return f"{self.base_url}/ekaterinburg/kvartiry/prodam-ASgBAgICAUSSA8YQ?{urlencode(params)}"
|
||||
return (
|
||||
f"{self.base_url}/{self._city_seg()}/kvartiry/prodam-ASgBAgICAUSSA8YQ"
|
||||
f"?{urlencode(params)}"
|
||||
)
|
||||
|
||||
def _build_newbuilding_url(self, page: int = 1) -> str:
|
||||
"""URL ЕКБ-выборки только новостроек (novostroyka-filter), сортировка по дате.
|
||||
"""URL городской выборки только новостроек (novostroyka-filter), сортировка по дате.
|
||||
|
||||
Тот же shape параметров, что _build_citywide_url (s=104, p=page), но slug
|
||||
категории NOVOSTROYKA_SLUG отдаёт исключительно new-build карточки.
|
||||
"""
|
||||
params = {"s": 104, "p": page}
|
||||
path = f"/ekaterinburg/kvartiry/prodam/{NOVOSTROYKA_SLUG}"
|
||||
path = f"/{self._city_seg()}/kvartiry/prodam/{NOVOSTROYKA_SLUG}"
|
||||
return f"{self.base_url}{path}?{urlencode(params)}"
|
||||
|
||||
def _build_rooms_url(
|
||||
|
|
@ -893,7 +904,7 @@ class AvitoScraper(BaseScraper):
|
|||
min_price: int | None = None,
|
||||
max_price: int | None = None,
|
||||
) -> str:
|
||||
"""T6: URL с фильтром по комнатности для всего ЕКБ (no geo).
|
||||
"""T6: URL с фильтром по комнатности для всего города (no geo).
|
||||
|
||||
Дробление по комнатности позволяет обойти cap ~5000 на category.
|
||||
room_slug — берётся из ROOM_SLUGS (кодирует ASgB-параметры выборки).
|
||||
|
|
@ -907,7 +918,7 @@ class AvitoScraper(BaseScraper):
|
|||
params["pmin"] = min_price
|
||||
if max_price is not None:
|
||||
params["pmax"] = max_price
|
||||
return f"{self.base_url}/ekaterinburg/kvartiry/prodam/{room_slug}?{urlencode(params)}"
|
||||
return f"{self.base_url}/{self._city_seg()}/kvartiry/prodam/{room_slug}?{urlencode(params)}"
|
||||
|
||||
def _extract_total_count(self, html: str) -> int | None:
|
||||
"""Извлечь общее число результатов из Avito SERP (`page-title/count`).
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ class CianScraper(BaseScraper):
|
|||
*,
|
||||
delay_provider: Callable[[str], float] | None = None,
|
||||
proxy_provider: ProxyProvider | None = None,
|
||||
city_region_id: int | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
# Strangler-инжекция (#2133): конфиг и провайдер задержки приходят снаружи
|
||||
|
|
@ -133,6 +134,9 @@ class CianScraper(BaseScraper):
|
|||
# None → BrowserFetcher без пула = env-прокси браузера (ship-dark, как сейчас).
|
||||
self._proxy_provider = proxy_provider
|
||||
self._browser: BrowserFetcher | None = None
|
||||
# #12 (oblast rollout): region= города-цели SERP-запроса (ekb.cian.ru — общий
|
||||
# поддомен всей Свердловской обл., НЕ меняется). None → ЕКБ-дефолт (CIAN_EKB_REGION_ID).
|
||||
self._region_id = city_region_id or CIAN_EKB_REGION_ID
|
||||
|
||||
async def __aenter__(self) -> CianScraper:
|
||||
await super().__aenter__()
|
||||
|
|
@ -173,10 +177,12 @@ class CianScraper(BaseScraper):
|
|||
rooms: tuple[int, ...] | None = None,
|
||||
page: int = 1,
|
||||
) -> list[ScrapedLot]:
|
||||
"""Найти объявления Циан по ЕКБ (lat/lon для reference; Cian не поддерживает bbox).
|
||||
"""Найти объявления Циан по городу-цели (region_id; lat/lon для reference —
|
||||
Cian не поддерживает bbox).
|
||||
|
||||
Cian SERP отдаёт весь ЕКБ — фильтрация по радиусу происходит в postgres
|
||||
через ST_DWithin после сохранения (точные coords из state).
|
||||
Cian SERP отдаёт весь город (self._region_id, ЕКБ-дефолт) — фильтрация по
|
||||
радиусу происходит в postgres через ST_DWithin после сохранения (точные
|
||||
coords из state).
|
||||
|
||||
rooms — список из 1,2,3,4 (Cian's room codes). Если None — все.
|
||||
page — страница выдачи (Cian ~28 объявлений на страницу).
|
||||
|
|
@ -241,17 +247,18 @@ class CianScraper(BaseScraper):
|
|||
min_price: int | None = None,
|
||||
max_price: int | None = None,
|
||||
) -> str:
|
||||
"""URL для Cian каталога вторички ЕКБ.
|
||||
"""URL для Cian каталога вторички города-цели (по умолчанию ЕКБ).
|
||||
|
||||
Используем ekb.cian.ru (city-specific subdomain).
|
||||
Регион задаётся через region= param как fallback для reliability.
|
||||
Используем ekb.cian.ru (city-specific subdomain — общий для всей Свердловской
|
||||
обл., НЕ меняется per-city). region= param — фактический скоуп выборки
|
||||
(#12: self._region_id, ЕКБ-дефолт CIAN_EKB_REGION_ID если city_region_id не задан).
|
||||
min_price / max_price — опциональные ценовые границы (руб.) для price-бакетов.
|
||||
"""
|
||||
params: list[tuple[str, Any]] = [
|
||||
("deal_type", "sale"),
|
||||
("engine_version", "2"),
|
||||
("offer_type", "flat"),
|
||||
("region", CIAN_EKB_REGION_ID),
|
||||
("region", self._region_id),
|
||||
("sort", "creation_date_desc"),
|
||||
]
|
||||
if rooms:
|
||||
|
|
|
|||
|
|
@ -480,6 +480,7 @@ class YandexRealtyScraper(BaseScraper):
|
|||
*,
|
||||
delay_provider: Callable[[str], float] | None = None,
|
||||
proxy_provider: ProxyProvider | None = None,
|
||||
city_rgid: int | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.city = city
|
||||
|
|
@ -494,6 +495,9 @@ class YandexRealtyScraper(BaseScraper):
|
|||
# None → BrowserFetcher без пула = env-прокси браузера (ship-dark, как сейчас).
|
||||
self._proxy_provider = proxy_provider
|
||||
self._browser: BrowserFetcher | None = None
|
||||
# #12 (oblast rollout): rgid города-цели SERP-запроса (gate-API). None →
|
||||
# ЕКБ-дефолт (_EKB_RGID).
|
||||
self._rgid = city_rgid or _EKB_RGID
|
||||
# _cffi_session retained only for _rotate_ip (changeip call).
|
||||
self._cffi_session: _CurlCffiSession | None = None
|
||||
self._cookies: dict[str, str] = {}
|
||||
|
|
@ -591,7 +595,7 @@ class YandexRealtyScraper(BaseScraper):
|
|||
"YES" -> novostroyki (primary-sale / reassignment segment).
|
||||
"""
|
||||
params: dict[str, str | int] = {
|
||||
"rgid": _EKB_RGID,
|
||||
"rgid": self._rgid,
|
||||
"type": "SELL",
|
||||
"category": "APARTMENT",
|
||||
"newFlat": new_flat,
|
||||
|
|
@ -648,7 +652,8 @@ class YandexRealtyScraper(BaseScraper):
|
|||
price_max: int | None = None,
|
||||
new_flat: str = "NO",
|
||||
) -> list[ScrapedLot]:
|
||||
"""Fetch ONE page of gate-API results. lat/lon/radius_m ignored (uses rgid).
|
||||
"""Fetch ONE page of gate-API results. lat/lon/radius_m ignored (uses rgid,
|
||||
city-scoped — self._rgid, ЕКБ-дефолт если city_rgid не задан).
|
||||
|
||||
page: 1-based. Tarpit resilience: status_code==0 or JSON error
|
||||
-> rotate IP + retry up to _YANDEX_TARPIT_MAX_RETRIES times.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue