feat(msk-import): ДомКлик как четвёртый источник переливки сырья в listings
All checks were successful
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 7m21s

Сбор ДомКлика по Москве пошёл (PR #3496), но переливать его в listings было
нечем: в SOURCE_VIEWS стояли только Циан, Авито и Яндекс. Вью
msk_raw.domclick_latest в проде уже существует, не хватало двух записей в
словарях и распознавания города.

## Город читается первым компонентом адреса

У Циана города в адресе нет вовсе и Москву выдаёт префикс округа; у Яндекса
регион стоит вторым компонентом после страны. У ДомКлика адрес начинается
прямо с города: «Москва, улица Нижние Мнёвники, 16 к2».

Замер по 5 024 карточкам первого прохода: первый компонент имеет ровно одно
значение — «Москва», областных нет ни одной. Это ожидаемо, сырьё собрано
запросом с GUID-ом Москвы и отфильтровано по bbox ещё на стороне сборщика.

Поэтому фильтр здесь — вторая линия, а не единственная. Сменится GUID в
сборщике или приедет в ту же таблицу второй город — импорт не потащит его в
Москву молча, а отложит в skipped_not_moscow.

Новая Москва регион не ломает: «Москва, пос. Птичное, …», «Москва, x.
Ильичевка, …» — посёлок стоит ВТОРЫМ компонентом, первым всегда город, что и
соответствует коду 77.

Сравнение идёт по компоненту целиком, а не по префиксу строки: иначе
«Московская область» и «Московский» прошли бы как Москва. Оба случая
закреплены тестом.

## Тесты

Распознавание Москвы, включая две формы Новой Москвы; отбраковка области,
Екатеринбурга, похожего на префикс «Московский», пустой строки и None;
проводка источника в оба словаря. 45 passed, ruff чистый.

Claude-Session: https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs
This commit is contained in:
bot-backend 2026-09-12 15:16:07 +03:00
parent 628f59dcc0
commit f6938452d0
2 changed files with 53 additions and 0 deletions

View file

@ -77,10 +77,21 @@ source_id) считает сам кит (`ScrapedLot.compute_dedup_hash`), це
значения, «Москва» 10 610 и «Московская область» 10 783. Координаты у Яндекса
заполнены у 100% карточек, поэтому ни геокод, ни `geocode_missing` ему не нужны.
Отбор Москвы (source=domclick) тоже стоит ноль вызовов, но по другой причине:
сырьё собрано запросом с GUID-ом Москвы и отфильтровано по bbox ещё на стороне
сборщика, поэтому областных карточек в нём нет по построению. Регион читается
ПЕРВЫМ компонентом адреса («Москва, улица Нижние Мнёвники, 16 к2»), и фильтр
оставлен второй линией на случай, если в ту же таблицу когда-нибудь приедет
второй город. Замер по 5 024 карточкам первого прохода: единственное значение
первого компонента «Москва». Новая Москва приходит как «Москва, пос. Птичное,
», то есть посёлок стоит вторым компонентом и регион 77 не ломает. Координаты
заполнены у 100% карточек.
Запуск:
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 domclick
python -m app.tasks.msk_raw_import --source avito --geocode --geocode-limit 9000
python -m app.tasks.msk_raw_import --source avito --allow-unfiltered # аварийный
"""
@ -160,6 +171,7 @@ SOURCE_VIEWS = {
"cian": "msk_raw.cian_latest",
"avito": "msk_raw.avito_latest",
"yandex": "msk_raw.yandex_latest",
"domclick": "msk_raw.domclick_latest",
}
_PAGE_SQL = """
@ -485,11 +497,30 @@ def is_moscow_yandex_address(address: str | None) -> bool:
return len(parts) > 1 and parts[1] == "Москва"
def is_moscow_domclick_address(address: str | None) -> bool:
"""У ДомКлика регион — ПЕРВЫЙ компонент адреса: «Москва, улица …».
Источник собран запросом с GUID-ом Москвы в параметре address и дополнительно
отфильтрован по bbox на стороне сборщика, так что областных карточек в сырье
и не должно быть. Фильтр здесь вторая линия: сменится GUID в сборщике или
появится второй город в той же таблице импорт не потащит его в Москву
молча. Замер по 5 024 карточкам первого прохода: первый компонент имеет ровно
одно значение, «Москва», областных нет ни одной.
Новая Москва отдельным значением НЕ приходит: «Москва, x. Ильичевка, »,
«Москва, пос. Птичное, » посёлок стоит ВТОРЫМ компонентом, первый всегда
город, что совпадает с кодом региона 77.
"""
parts = [part.strip() for part in (address or "").split(",")]
return bool(parts) and parts[0] == "Москва"
# Источники, у которых город виден в самой карточке. Ключ отсутствует —
# источник про город молчит, и без пред-геокода писать его нельзя (avito).
CITY_FILTERS = {
"cian": is_moscow_address,
"yandex": is_moscow_yandex_address,
"domclick": is_moscow_domclick_address,
}

View file

@ -461,3 +461,25 @@ def test_geocode_is_refused_for_yandex(captured: list[dict[str, Any]]) -> None:
mod.import_msk_raw(
_FakeSession([{"id": 1, "payload": _yandex()}]), source="yandex", geocode=True
)
def test_domclick_moscow_is_recognised_without_geocode() -> None:
"""Регион ДомКлика — первый компонент адреса; Новая Москва тоже Москва."""
assert mod.is_moscow_domclick_address("Москва, улица Нижние Мнёвники, 16 к2")
assert mod.is_moscow_domclick_address("Москва, пос. Птичное, Лесная улица, 82")
assert mod.is_moscow_domclick_address("Москва, x. Ильичевка, хутор Ильичёвка, 1")
def test_domclick_non_moscow_is_rejected() -> None:
"""Вторая линия обороны: GUID в сборщике сменится — импорт не смолчит."""
assert not mod.is_moscow_domclick_address("Московская область, Химки, 9 Мая, 21к3")
assert not mod.is_moscow_domclick_address("Екатеринбург, улица Ясная, 2")
# «Москва» должна быть отдельным компонентом, а не префиксом другого слова.
assert not mod.is_moscow_domclick_address("Московский, 1-я улица, 5")
assert not mod.is_moscow_domclick_address("")
assert not mod.is_moscow_domclick_address(None)
def test_domclick_view_is_wired_into_source_views() -> None:
assert mod.SOURCE_VIEWS["domclick"] == "msk_raw.domclick_latest"
assert mod.CITY_FILTERS["domclick"] is mod.is_moscow_domclick_address