Импортёр знает Яндекс: город из второго компонента адреса, ноль внешних вызовов #3454
2 changed files with 103 additions and 6 deletions
|
|
@ -71,9 +71,16 @@ source_id) считает сам кит (`ScrapedLot.compute_dedup_hash`), це
|
||||||
курсор идёт по `id` вью, так что порядок и полнота обхода от прогона к прогону
|
курсор идёт по `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 --dry-run
|
||||||
python -m app.tasks.msk_raw_import --limit 500
|
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 --geocode --geocode-limit 9000
|
||||||
python -m app.tasks.msk_raw_import --source avito --allow-unfiltered # аварийный
|
python -m app.tasks.msk_raw_import --source avito --allow-unfiltered # аварийный
|
||||||
"""
|
"""
|
||||||
|
|
@ -152,6 +159,7 @@ _ARTIFACT_RE = re.compile(r"Екатеринбург", re.IGNORECASE)
|
||||||
SOURCE_VIEWS = {
|
SOURCE_VIEWS = {
|
||||||
"cian": "msk_raw.cian_latest",
|
"cian": "msk_raw.cian_latest",
|
||||||
"avito": "msk_raw.avito_latest",
|
"avito": "msk_raw.avito_latest",
|
||||||
|
"yandex": "msk_raw.yandex_latest",
|
||||||
}
|
}
|
||||||
|
|
||||||
_PAGE_SQL = """
|
_PAGE_SQL = """
|
||||||
|
|
@ -460,6 +468,31 @@ def is_moscow_address(address: str | None) -> bool:
|
||||||
return _MOSCOW_OKRUG_RE.match(address.strip()) is not None
|
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:
|
def recompute_listing_segment(payload: dict) -> str:
|
||||||
"""Заново считаем сегмент: 'novostroyki' только при продаже застройщиком.
|
"""Заново считаем сегмент: 'novostroyki' только при продаже застройщиком.
|
||||||
|
|
||||||
|
|
@ -518,12 +551,15 @@ def import_msk_raw(
|
||||||
matcher = RealMatcherAdapter()
|
matcher = RealMatcherAdapter()
|
||||||
budget = GeocodeBudget(remaining=max(0, geocode_limit))
|
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")
|
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):
|
if not (dry_run or allow_unfiltered):
|
||||||
|
|
@ -551,7 +587,7 @@ def import_msk_raw(
|
||||||
if is_artifact_address(address):
|
if is_artifact_address(address):
|
||||||
counters.skipped_artifact += 1
|
counters.skipped_artifact += 1
|
||||||
continue
|
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
|
counters.skipped_not_moscow += 1
|
||||||
continue
|
continue
|
||||||
if geocode:
|
if geocode:
|
||||||
|
|
|
||||||
|
|
@ -400,3 +400,64 @@ def test_dry_run_survives_missing_geocode_cache(monkeypatch: pytest.MonkeyPatch)
|
||||||
|
|
||||||
assert db.cache_reads == 0
|
assert db.cache_reads == 0
|
||||||
assert list(points.values()) == [point]
|
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