Импортёр знает Яндекс: город из второго компонента адреса, ноль внешних вызовов #3454
2 changed files with 103 additions and 6 deletions
|
|
@ -71,9 +71,16 @@ source_id) считает сам кит (`ScrapedLot.compute_dedup_hash`), це
|
|||
курсор идёт по `id` вью, так что порядок и полнота обхода от прогона к прогону
|
||||
одинаковы.
|
||||
|
||||
Отбор Москвы (source=yandex) стоит ноль вызовов: адрес приходит полным и
|
||||
нормализованным («Россия, Москва, Коробейников переулок, 1»), регион читается
|
||||
вторым компонентом. Замер по 21 393 карточкам первого прохода — ровно два
|
||||
значения, «Москва» 10 610 и «Московская область» 10 783. Координаты у Яндекса
|
||||
заполнены у 100% карточек, поэтому ни геокод, ни `geocode_missing` ему не нужны.
|
||||
|
||||
Запуск:
|
||||
python -m app.tasks.msk_raw_import --dry-run
|
||||
python -m app.tasks.msk_raw_import --limit 500
|
||||
python -m app.tasks.msk_raw_import --source yandex
|
||||
python -m app.tasks.msk_raw_import --source avito --geocode --geocode-limit 9000
|
||||
python -m app.tasks.msk_raw_import --source avito --allow-unfiltered # аварийный
|
||||
"""
|
||||
|
|
@ -152,6 +159,7 @@ _ARTIFACT_RE = re.compile(r"Екатеринбург", re.IGNORECASE)
|
|||
SOURCE_VIEWS = {
|
||||
"cian": "msk_raw.cian_latest",
|
||||
"avito": "msk_raw.avito_latest",
|
||||
"yandex": "msk_raw.yandex_latest",
|
||||
}
|
||||
|
||||
_PAGE_SQL = """
|
||||
|
|
@ -460,6 +468,31 @@ def is_moscow_address(address: str | None) -> bool:
|
|||
return _MOSCOW_OKRUG_RE.match(address.strip()) is not None
|
||||
|
||||
|
||||
def is_moscow_yandex_address(address: str | None) -> bool:
|
||||
"""У Яндекса регион — второй компонент полного адреса.
|
||||
|
||||
Адрес приходит нормализованным и с городом: «Россия, Москва, Коробейников
|
||||
переулок, 1». Замер по 21 393 карточкам первого прохода: во втором
|
||||
компоненте ровно ДВА значения — «Москва» 10 610 и «Московская область»
|
||||
10 783, третьего не встречается. Поэтому ни префикса округа (как у Циана),
|
||||
ни внешнего геокода (как у Авито) источнику не нужно: разделение 77 и 50
|
||||
читается из самой карточки и стоит ноль вызовов.
|
||||
|
||||
Новая Москва отдельным значением НЕ приходит — Троицк и Зеленоград Яндекс
|
||||
кладёт под «Москва», что совпадает с кодом региона 77.
|
||||
"""
|
||||
parts = [part.strip() for part in (address or "").split(",")]
|
||||
return len(parts) > 1 and parts[1] == "Москва"
|
||||
|
||||
|
||||
# Источники, у которых город виден в самой карточке. Ключ отсутствует —
|
||||
# источник про город молчит, и без пред-геокода писать его нельзя (avito).
|
||||
CITY_FILTERS = {
|
||||
"cian": is_moscow_address,
|
||||
"yandex": is_moscow_yandex_address,
|
||||
}
|
||||
|
||||
|
||||
def recompute_listing_segment(payload: dict) -> str:
|
||||
"""Заново считаем сегмент: 'novostroyki' только при продаже застройщиком.
|
||||
|
||||
|
|
@ -518,12 +551,15 @@ def import_msk_raw(
|
|||
matcher = RealMatcherAdapter()
|
||||
budget = GeocodeBudget(remaining=max(0, geocode_limit))
|
||||
|
||||
filter_by_okrug = source == "cian"
|
||||
if geocode and filter_by_okrug:
|
||||
# У Циана город опознаётся префиксом округа даром и без ошибок —
|
||||
# тратить на него внешнюю квоту незачем.
|
||||
# Источник, который сам говорит про город: у Циана это префикс округа, у
|
||||
# Яндекса — второй компонент полного адреса. Авито не говорит ничего, ему
|
||||
# нужен пред-геокод, поэтому его в словаре нет.
|
||||
city_filter = CITY_FILTERS.get(source)
|
||||
if geocode and city_filter is not None:
|
||||
# Город опознаётся даром и без ошибок — тратить на него внешнюю квоту
|
||||
# незачем.
|
||||
raise SystemExit(f"source={source}: --geocode нужен только для avito")
|
||||
if not filter_by_okrug and not geocode:
|
||||
if city_filter is None and not geocode:
|
||||
# Без пред-геокода у Авито по-прежнему нечем отделить Москву от области:
|
||||
# ни города в адресе, ни координат. Пишем только по явному разрешению.
|
||||
if not (dry_run or allow_unfiltered):
|
||||
|
|
@ -551,7 +587,7 @@ def import_msk_raw(
|
|||
if is_artifact_address(address):
|
||||
counters.skipped_artifact += 1
|
||||
continue
|
||||
if filter_by_okrug and not is_moscow_address(address):
|
||||
if city_filter is not None and not city_filter(address):
|
||||
counters.skipped_not_moscow += 1
|
||||
continue
|
||||
if geocode:
|
||||
|
|
|
|||
|
|
@ -400,3 +400,64 @@ def test_dry_run_survives_missing_geocode_cache(monkeypatch: pytest.MonkeyPatch)
|
|||
|
||||
assert db.cache_reads == 0
|
||||
assert list(points.values()) == [point]
|
||||
|
||||
|
||||
# ── Яндекс: город из второго компонента адреса ───────────────────────────────
|
||||
|
||||
|
||||
def _yandex(**over: Any) -> dict[str, Any]:
|
||||
base = _payload(
|
||||
source="yandex",
|
||||
source_url="https://realty.yandex.ru/offer/1234567890123456789/",
|
||||
source_id="1234567890123456789",
|
||||
address="Россия, Москва, Коробейников переулок, 1",
|
||||
lat=55.737312,
|
||||
lon=37.601006,
|
||||
)
|
||||
base.update(over)
|
||||
return base
|
||||
|
||||
|
||||
def test_yandex_moscow_is_recognised_without_geocode() -> None:
|
||||
assert mod.is_moscow_yandex_address("Россия, Москва, Коробейников переулок, 1")
|
||||
assert mod.is_moscow_yandex_address("Россия, Москва, Зеленоград, корпус 1130")
|
||||
|
||||
|
||||
def test_yandex_oblast_is_not_moscow() -> None:
|
||||
"""Ровно те два значения, что встречаются в корпусе; область писать нельзя."""
|
||||
oblast = "Россия, Московская область, Химки, Ивановская улица, 5"
|
||||
assert not mod.is_moscow_yandex_address(oblast)
|
||||
assert not mod.is_moscow_yandex_address("Москва, Коробейников переулок, 1") # без страны
|
||||
assert not mod.is_moscow_yandex_address("")
|
||||
assert not mod.is_moscow_yandex_address(None)
|
||||
|
||||
|
||||
def test_yandex_oblast_rows_do_not_reach_the_writer(captured: list[dict[str, Any]]) -> None:
|
||||
rows = [
|
||||
{"id": 1, "payload": _yandex(source_id="1")},
|
||||
{
|
||||
"id": 2,
|
||||
"payload": _yandex(
|
||||
source_id="2", address="Россия, Московская область, Химки, Ивановская улица, 5"
|
||||
),
|
||||
},
|
||||
]
|
||||
counters = mod.import_msk_raw(_FakeSession(rows), source="yandex", batch_size=10)
|
||||
assert [lot.source_id for lot in captured[0]["lots"]] == ["1"]
|
||||
assert counters.skipped_not_moscow == 1
|
||||
|
||||
|
||||
def test_yandex_keeps_its_own_coordinates(captured: list[dict[str, Any]]) -> None:
|
||||
"""Координаты у Яндекса свои — геокод ему не нужен, geom появляется сразу."""
|
||||
rows = [{"id": 1, "payload": _yandex()}]
|
||||
mod.import_msk_raw(_FakeSession(rows), source="yandex", batch_size=10)
|
||||
lot = captured[0]["lots"][0]
|
||||
assert (lot.lat, lot.lon) == (55.737312, 37.601006)
|
||||
|
||||
|
||||
def test_geocode_is_refused_for_yandex(captured: list[dict[str, Any]]) -> None:
|
||||
"""Квота нужна только Авито: у Яндекса город и точка есть даром."""
|
||||
with pytest.raises(SystemExit):
|
||||
mod.import_msk_raw(
|
||||
_FakeSession([{"id": 1, "payload": _yandex()}]), source="yandex", geocode=True
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue