All checks were successful
Deploy Trade-In / changes (push) Successful in 15s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m50s
Deploy Trade-In / build-backend (push) Successful in 1m49s
Deploy Trade-In / deploy (push) Successful in 4m41s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m44s
ДомКлик: DomClickGeoProfile вместо зашитых _EKB_ADDRESS_GUID/_EKB_AREA_ID; GUID Москвы и области проверены живьём, aids вне ЕКБ не нужен, гард по bbox профиля. Страница прогрева 77/50 — апексный domclick.ru: субдомены msk./moskovskaya-oblast. отдают 301. Циан/Авито/Яндекс: CityLocation.cian_host, три новых скоупа (moskva, moskovskaya_oblast, moskva_i_mo), avito_slug_is_region, city=NULL у мультигородских скоупов. Неизвестный слаг теперь падает с ValueError вместо молчаливого отката на Екатеринбург. Якоря: Москва — сетка 25 точек под radius_m=8000; область — 22 города-спутника (10 добраны из Nominatim) плюс 22 кластера лот-массы. Замер на проде: города радиусом 10 км дают 73.8% лот-массы области, вместе с кластерами — 95.2%. Ценовые коридоры: планировщик plan_price_corridors со статистикой усечения плюс BisectionStats в живом движке. Провайдеры их пока не передают — отдельный заход. Расписаний scrape_schedules для 77/50 в этом PR нет: они пойдут после первого ручного прогона, подтверждающего живость профилей.
115 lines
5.1 KiB
Python
115 lines
5.1 KiB
Python
"""Гео-скоуп DomClick — параметр региона, а не зашитый Екатеринбург.
|
||
|
||
До этого GUID/aids/страница прогрева/bbox жили модульными константами `_EKB_*`,
|
||
и прогон с region_code 77/50 собрал бы ЕКБ под чужим регионом. Тесты стерегут
|
||
две вещи: профиль 66 не изменил НИ БАЙТА в запросах (прод-поведение прежнее),
|
||
а профили Москвы и области действительно меняют скоуп — без aids и без сверки
|
||
имени региона, которое вне ЕКБ ненадёжно.
|
||
"""
|
||
|
||
from types import SimpleNamespace
|
||
|
||
import pytest
|
||
from scraper_kit.providers.domclick.serp import (
|
||
DOMCLICK_GEO_PROFILES,
|
||
DomClickScraper,
|
||
_build_count_url,
|
||
_build_offers_url,
|
||
geo_profile_for_region,
|
||
)
|
||
|
||
# Литералы снятые с кода ДО параметризации: любое расхождение = смена прод-запроса.
|
||
_EKB_OFFERS_URL_BEFORE = (
|
||
"https://bff-search-web.domclick.ru/api/offers/v1"
|
||
"?address=0d475b79-88de-4054-818c-37d8f9d0d440&aids=20561"
|
||
"&deal_type=sale&category=living&offer_type=flat&rooms=5%2B"
|
||
"&sort=qi&sort_dir=desc&offset=40&limit=20"
|
||
"&sale_price__gte=1000000&sale_price__lte=5000000"
|
||
)
|
||
_EKB_COUNT_URL_BEFORE = (
|
||
"https://bff-search-web.domclick.ru/api/offers/count/v1"
|
||
"?address=0d475b79-88de-4054-818c-37d8f9d0d440&aids=20561"
|
||
"&deal_type=sale&category=living&offer_type=flat&rooms=2"
|
||
"&sort=qi&sort_dir=desc"
|
||
)
|
||
|
||
|
||
def _url_for(region_code: int, *, count: bool) -> str:
|
||
profile = geo_profile_for_region(region_code)
|
||
if count:
|
||
return _build_count_url(
|
||
"2", None, None, address_guid=profile.address_guid, aids=profile.aids
|
||
)
|
||
return _build_offers_url(
|
||
"5+",
|
||
1_000_000,
|
||
5_000_000,
|
||
40,
|
||
address_guid=profile.address_guid,
|
||
aids=profile.aids,
|
||
)
|
||
|
||
|
||
def test_ekb_offers_url_unchanged_byte_for_byte() -> None:
|
||
assert _url_for(66, count=False) == _EKB_OFFERS_URL_BEFORE
|
||
|
||
|
||
def test_ekb_count_url_unchanged_byte_for_byte() -> None:
|
||
assert _url_for(66, count=True) == _EKB_COUNT_URL_BEFORE
|
||
|
||
|
||
@pytest.mark.parametrize("region_code", [77, 50])
|
||
@pytest.mark.parametrize("count", [False, True])
|
||
def test_msk_and_oblast_urls_carry_no_aids(region_code: int, count: bool) -> None:
|
||
"""aids сужает выдачу до агрегатора; у Москвы и области счётчик без него
|
||
совпадает с сайтом (замер 12.09.2026), поэтому параметра быть не должно."""
|
||
url = _url_for(region_code, count=count)
|
||
assert "aids=" not in url
|
||
assert f"address={geo_profile_for_region(region_code).address_guid}" in url
|
||
|
||
|
||
def test_profiles_are_distinct_and_ekb_keeps_aids() -> None:
|
||
guids = {code: p.address_guid for code, p in DOMCLICK_GEO_PROFILES.items()}
|
||
assert len(set(guids.values())) == len(guids)
|
||
assert DOMCLICK_GEO_PROFILES[66].aids == "20561"
|
||
# Имя региона сверяется ТОЛЬКО у ЕКБ.
|
||
assert DOMCLICK_GEO_PROFILES[66].region_name == "Екатеринбург"
|
||
assert DOMCLICK_GEO_PROFILES[77].region_name is None
|
||
assert DOMCLICK_GEO_PROFILES[50].region_name is None
|
||
# Имя города для listings.city: у области его нет и угадывать нечего.
|
||
assert DOMCLICK_GEO_PROFILES[66].city_name == "Екатеринбург"
|
||
assert DOMCLICK_GEO_PROFILES[77].city_name == "Москва"
|
||
assert DOMCLICK_GEO_PROFILES[50].city_name is None
|
||
|
||
|
||
def _geo_ok(region_code: int, item: dict[str, object]) -> bool:
|
||
"""_is_geo_ok без конструирования скрейпера: метод читает только self._geo."""
|
||
fake = SimpleNamespace(_geo=geo_profile_for_region(region_code))
|
||
return DomClickScraper._is_geo_ok(fake, item) # type: ignore[arg-type]
|
||
|
||
|
||
def test_msk_accepts_novaya_moskva_with_foreign_region_name() -> None:
|
||
"""Новая Москва приходит как «г. Говорово» — сверка по имени вырезала бы
|
||
законную выдачу, поэтому вне 66 гард только по bbox."""
|
||
item = {"offerRegionName": "г. Говорово", "location": {"lat": 55.62, "lon": 37.35}}
|
||
assert _geo_ok(77, item) is True
|
||
|
||
|
||
def test_msk_rejects_point_outside_bbox() -> None:
|
||
ekb_point = {"offerRegionName": "Москва", "location": {"lat": 56.83, "lon": 60.60}}
|
||
assert _geo_ok(77, ekb_point) is False
|
||
|
||
|
||
def test_ekb_still_rejects_foreign_region_name() -> None:
|
||
item = {"offerRegionName": "г. Говорово", "location": {"lat": 56.83, "lon": 60.60}}
|
||
assert _geo_ok(66, item) is False
|
||
|
||
|
||
def test_oblast_bbox_accepts_podmoskovie_point() -> None:
|
||
item = {"offerRegionName": "г. Подольск", "location": {"lat": 55.43, "lon": 37.55}}
|
||
assert _geo_ok(50, item) is True
|
||
|
||
|
||
def test_unknown_region_code_raises() -> None:
|
||
with pytest.raises(ValueError, match="region_code=99"):
|
||
geo_profile_for_region(99)
|