Импорт msk_raw умеет областной ДомКлик, а не падает на нём
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m32s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m32s
Запрет на --source domclick --region 50 был верен ровно до тех пор, пока областного корпуса не существовало. Теперь он собран отдельным прогоном с областным GUID, поэтому запрет снят и заменён настоящим фильтром. Регион читается первым компонентом адреса. Замер живьём на API ДомКлика 12.09.2026: у московских карточек первый компонент «Москва» — так у всех 22 836 строк прод-корпуса; у областных «Московская область» — так у всех 140 карточек выборки с семи разных смещений выдачи. Разделение полное и симметричное тому, что уже сделано для Яндекса. Фильтр здесь не вторая линия обороны, а единственный способ развести регионы: обе партии лежат в одной таблице msk_raw.domclick_cards, колонки региона в ней нет, а вью domclick_latest отдаёт их вперемешку. Поэтому в тестах появился случай со смешанной страницей сырья: из двух карточек при region=50 отбирается ровно одна, вторая уходит в счётчик не целевого региона. Прежний тест ожидал SystemExit и заменён поведенческими. Московский путь не тронут.
This commit is contained in:
parent
8863781589
commit
a7470f0c3d
2 changed files with 148 additions and 29 deletions
|
|
@ -89,17 +89,26 @@ source_id) считает сам кит (`ScrapedLot.compute_dedup_hash`), це
|
|||
значения, «Москва» 10 610 и «Московская область» 10 783. Координаты у Яндекса
|
||||
заполнены у 100% карточек, поэтому ни геокод, ни `geocode_missing` ему не нужны.
|
||||
|
||||
Отбор Москвы (source=domclick) тоже стоит ноль вызовов, но по другой причине:
|
||||
сырьё собрано запросом с GUID-ом Москвы и отфильтровано по bbox ещё на стороне
|
||||
сборщика, поэтому областных карточек в нём нет по построению. Регион читается
|
||||
ПЕРВЫМ компонентом адреса («Москва, улица Нижние Мнёвники, 16 к2»), и фильтр
|
||||
оставлен второй линией — на случай, если в ту же таблицу когда-нибудь приедет
|
||||
второй город. Замер по 5 024 карточкам первого прохода: единственное значение
|
||||
первого компонента — «Москва». Новая Москва приходит как «Москва, пос. Птичное,
|
||||
…», то есть посёлок стоит вторым компонентом и регион 77 не ломает. Координаты
|
||||
заполнены у 100% карточек. Для региона 50 у ДомКлика КОРПУСА НЕТ ВООБЩЕ — запуск
|
||||
`--source domclick --region 50` падает `SystemExit` с явным текстом, а не тихо
|
||||
отдаёт ноль карточек.
|
||||
Отбор региона (source=domclick) стоит ноль вызовов, но таблица одна на ДВА
|
||||
РАЗНЫХ прогона сборщика с разными GUID: московский (батч
|
||||
`msk-serp-domclick-20260912`) и областной (отдельный запуск, батч
|
||||
`mo-serp-domclick-20260912`) — оба пишут в один и тот же
|
||||
`msk_raw.domclick_cards`. Колонки региона в таблице НЕТ, а вью
|
||||
`msk_raw.domclick_latest` отдаёт обе партии вперемешку — курсор по `id` не
|
||||
различает, из какого прогона строка. Поэтому фильтр по адресу здесь не
|
||||
опциональная вторая линия, а единственный способ развести регионы.
|
||||
|
||||
Регион читается ПЕРВЫМ компонентом адреса. Замер живьём на API ДомКлика
|
||||
12.09.2026: московские карточки — «Москва, Генерала Дорохова проспект, 49» и
|
||||
подобные, первый компонент «Москва» у всех 22 836 карточек прод-корпуса;
|
||||
областные карточки — «Московская область, Химки, 7-й м-н, проспект
|
||||
Мельникова, 33», «Московская область, Одинцовский городской округ,
|
||||
Звенигород, 3-й м-н, 28» и подобные, первый компонент «Московская область» у
|
||||
всех 140 карточек выборки с семи разных смещений выдачи. Разделение полное и
|
||||
симметричное `is_moscow_yandex_address`/`is_oblast_yandex_address`. Новая
|
||||
Москва приходит как «Москва, пос. Птичное, …» — посёлок стоит вторым
|
||||
компонентом, первый по-прежнему «Москва», регион 77 не ломается. Координаты
|
||||
заполнены у 100% карточек в обоих прогонах.
|
||||
|
||||
Запуск:
|
||||
python -m app.tasks.msk_raw_import --dry-run
|
||||
|
|
@ -108,6 +117,7 @@ source_id) считает сам кит (`ScrapedLot.compute_dedup_hash`), це
|
|||
python -m app.tasks.msk_raw_import --source yandex --region 50
|
||||
python -m app.tasks.msk_raw_import --source cian --region 50
|
||||
python -m app.tasks.msk_raw_import --source domclick
|
||||
python -m app.tasks.msk_raw_import --source domclick --region 50
|
||||
python -m app.tasks.msk_raw_import --source avito --geocode --geocode-limit 9000
|
||||
python -m app.tasks.msk_raw_import --source avito --geocode --region 50
|
||||
python -m app.tasks.msk_raw_import --source avito --allow-unfiltered # аварийный
|
||||
|
|
@ -579,6 +589,23 @@ def is_moscow_domclick_address(address: str | None) -> bool:
|
|||
return bool(parts) and parts[0] == "Москва"
|
||||
|
||||
|
||||
def is_oblast_domclick_address(address: str | None) -> bool:
|
||||
"""Регион 50 у ДомКлика: первый компонент адреса — «Московская область».
|
||||
|
||||
Симметрично `is_moscow_domclick_address`. Таблица `msk_raw.domclick_cards`
|
||||
копит ДВА разных прогона сборщика (московский батч
|
||||
`msk-serp-domclick-20260912`, областной `mo-serp-domclick-20260912`) без
|
||||
своей колонки региона, а вью `msk_raw.domclick_latest` отдаёт обе партии
|
||||
вперемешку — фильтр по адресу обязателен, не опционален. Замер живьём на
|
||||
API ДомКлика 12.09.2026: «Московская область, Химки, 7-й м-н, проспект
|
||||
Мельникова, 33», «Московская область, Одинцовский городской округ,
|
||||
Звенигород, 3-й м-н, 28» и подобные — первый компонент «Московская
|
||||
область» у всех 140 карточек выборки с семи разных смещений выдачи.
|
||||
"""
|
||||
parts = [part.strip() for part in (address or "").split(",")]
|
||||
return bool(parts) and parts[0] == "Московская область"
|
||||
|
||||
|
||||
def _by_address(fn: Callable[[str | None], bool]) -> Callable[[dict], bool]:
|
||||
"""Адаптер: фильтр по адресу → фильтр по всему payload'у (для реестра)."""
|
||||
return lambda payload: fn(payload.get("address"))
|
||||
|
|
@ -586,16 +613,16 @@ def _by_address(fn: Callable[[str | None], bool]) -> Callable[[dict], bool]:
|
|||
|
||||
# Реестр (source, целевой регион) → фильтр по ВСЕМУ payload'у, не только
|
||||
# адресу: у Циана признак региона 50 лежит в `source_url`, адрес про него
|
||||
# молчит. Ключа нет — либо источник вообще не умеет отличать регион без
|
||||
# пред-геокода (avito, для ЛЮБОГО региона), либо корпус физически не содержит
|
||||
# нужных карточек (domclick + регион 50 — своя явная проверка в
|
||||
# `import_msk_raw`, а не тихий ноль здесь).
|
||||
# молчит. Ключа нет только для источника, который вообще не умеет отличать
|
||||
# регион без пред-геокода (avito, для ЛЮБОГО региона) — для всех прочих пар
|
||||
# фильтр обязан быть в реестре явно.
|
||||
REGION_FILTERS: dict[tuple[str, int], Callable[[dict], bool]] = {
|
||||
("cian", MOSCOW_REGION_CODE): _by_address(is_moscow_address),
|
||||
("cian", OBLAST_REGION_CODE): is_cian_oblast_payload,
|
||||
("yandex", MOSCOW_REGION_CODE): _by_address(is_moscow_yandex_address),
|
||||
("yandex", OBLAST_REGION_CODE): _by_address(is_oblast_yandex_address),
|
||||
("domclick", MOSCOW_REGION_CODE): _by_address(is_moscow_domclick_address),
|
||||
("domclick", OBLAST_REGION_CODE): _by_address(is_oblast_domclick_address),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -655,12 +682,6 @@ def import_msk_raw(
|
|||
"""Переливает сырьё `msk_raw` в `listings`. Коммит — на каждом батче."""
|
||||
if region not in SUPPORTED_REGIONS:
|
||||
raise SystemExit(f"region={region}: регион не поддержан, доступны {SUPPORTED_REGIONS}")
|
||||
if source == "domclick" and region == OBLAST_REGION_CODE:
|
||||
raise SystemExit(
|
||||
"source=domclick region=50: корпус собран под Москву (GUID Москвы в "
|
||||
"параметре сборщика), областных карточек в нём нет вовсе — нужен "
|
||||
"отдельный сбор, этот импорт их не найдёт."
|
||||
)
|
||||
|
||||
view = SOURCE_VIEWS[source]
|
||||
counters = ImportCounters()
|
||||
|
|
|
|||
|
|
@ -480,11 +480,26 @@ def test_domclick_non_moscow_is_rejected() -> None:
|
|||
assert not mod.is_moscow_domclick_address(None)
|
||||
|
||||
|
||||
def test_domclick_oblast_is_recognised_without_geocode() -> None:
|
||||
"""Регион 50 у ДомКлика — первый компонент адреса «Московская область»."""
|
||||
assert mod.is_oblast_domclick_address("Московская область, Химки, 9 Мая, 21к3")
|
||||
assert mod.is_oblast_domclick_address(
|
||||
"Московская область, Одинцовский городской округ, Звенигород, 3-й м-н, 28"
|
||||
)
|
||||
assert not mod.is_oblast_domclick_address("Москва, улица Нижние Мнёвники, 16 к2")
|
||||
assert not mod.is_oblast_domclick_address("")
|
||||
assert not mod.is_oblast_domclick_address(None)
|
||||
|
||||
|
||||
def test_domclick_view_is_wired_into_source_views() -> None:
|
||||
assert mod.SOURCE_VIEWS["domclick"] == "msk_raw.domclick_latest"
|
||||
filt = mod.REGION_FILTERS[("domclick", 77)]
|
||||
assert filt(_payload(source="domclick", address="Москва, улица Нижние Мнёвники, 16 к2"))
|
||||
assert not filt(_payload(source="domclick", address="Московская область, Химки, 9 Мая, 21к3"))
|
||||
filt77 = mod.REGION_FILTERS[("domclick", 77)]
|
||||
assert filt77(_payload(source="domclick", address="Москва, улица Нижние Мнёвники, 16 к2"))
|
||||
assert not filt77(_payload(source="domclick", address="Московская область, Химки, 9 Мая, 21к3"))
|
||||
|
||||
filt50 = mod.REGION_FILTERS[("domclick", 50)]
|
||||
assert filt50(_payload(source="domclick", address="Московская область, Химки, 9 Мая, 21к3"))
|
||||
assert not filt50(_payload(source="domclick", address="Москва, улица Нижние Мнёвники, 16 к2"))
|
||||
|
||||
|
||||
# ── регион 50 (Московская область) ───────────────────────────────────────────
|
||||
|
|
@ -547,11 +562,94 @@ def test_yandex_oblast_region_50_is_selected(captured: list[dict[str, Any]]) ->
|
|||
assert counters_77.selected == 0
|
||||
|
||||
|
||||
def test_domclick_region_50_refuses_to_run() -> None:
|
||||
"""Корпус ДомКлика собран под Москву — областных карточек в нём нет вовсе."""
|
||||
rows = [{"id": 1, "payload": _payload(source="domclick", address="Москва, ул. Тестовая, 1")}]
|
||||
with pytest.raises(SystemExit):
|
||||
mod.import_msk_raw(_FakeSession(rows), source="domclick", region=50, batch_size=10)
|
||||
def test_domclick_oblast_region_50_is_selected(captured: list[dict[str, Any]]) -> None:
|
||||
"""Областной прогон сборщика (батч mo-serp-domclick-20260912) отбирается по region=50."""
|
||||
rows = [
|
||||
{
|
||||
"id": 1,
|
||||
"payload": _payload(
|
||||
source="domclick",
|
||||
source_id="oblast-1",
|
||||
address="Московская область, Химки, проспект Мельникова, 33",
|
||||
),
|
||||
}
|
||||
]
|
||||
counters_50 = mod.import_msk_raw(
|
||||
_FakeSession(rows), source="domclick", region=50, batch_size=10
|
||||
)
|
||||
assert counters_50.selected == 1
|
||||
|
||||
counters_77 = mod.import_msk_raw(
|
||||
_FakeSession(rows), source="domclick", region=77, batch_size=10
|
||||
)
|
||||
assert counters_77.selected == 0
|
||||
|
||||
|
||||
def test_domclick_moscow_region_77_is_selected(captured: list[dict[str, Any]]) -> None:
|
||||
"""Московский прогон (батч msk-serp-domclick-20260912) отбирается по region=77."""
|
||||
rows = [
|
||||
{
|
||||
"id": 1,
|
||||
"payload": _payload(
|
||||
source="domclick",
|
||||
source_id="moscow-1",
|
||||
address="Москва, Генерала Дорохова проспект, 49",
|
||||
),
|
||||
}
|
||||
]
|
||||
counters_77 = mod.import_msk_raw(
|
||||
_FakeSession(rows), source="domclick", region=77, batch_size=10
|
||||
)
|
||||
assert counters_77.selected == 1
|
||||
|
||||
counters_50 = mod.import_msk_raw(
|
||||
_FakeSession(rows), source="domclick", region=50, batch_size=10
|
||||
)
|
||||
assert counters_50.selected == 0
|
||||
|
||||
|
||||
def test_domclick_writer_gets_region_50_and_city_none(captured: list[dict[str, Any]]) -> None:
|
||||
rows = [
|
||||
{
|
||||
"id": 1,
|
||||
"payload": _payload(
|
||||
source="domclick",
|
||||
source_id="oblast-2",
|
||||
address="Московская область, Химки, проспект Мельникова, 33",
|
||||
),
|
||||
}
|
||||
]
|
||||
mod.import_msk_raw(_FakeSession(rows), source="domclick", region=50, batch_size=10)
|
||||
assert captured[0]["region_code"] == 50
|
||||
assert captured[0]["city"] is None
|
||||
|
||||
|
||||
def test_domclick_mixed_batches_are_told_apart_by_address(captured: list[dict[str, Any]]) -> None:
|
||||
"""Вью отдаёт московский и областной прогоны вперемешку — региональный фильтр
|
||||
обязан развести их сам, курсор по `id` тут не помощник."""
|
||||
rows = [
|
||||
{
|
||||
"id": 1,
|
||||
"payload": _payload(
|
||||
source="domclick",
|
||||
source_id="oblast-3",
|
||||
address="Московская область, Одинцовский городской округ, Звенигород, 3-й м-н, 28",
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"payload": _payload(
|
||||
source="domclick",
|
||||
source_id="moscow-2",
|
||||
address="Москва, Генерала Дорохова проспект, 49",
|
||||
),
|
||||
},
|
||||
]
|
||||
counters = mod.import_msk_raw(_FakeSession(rows), source="domclick", region=50, batch_size=10)
|
||||
assert [lot.source_id for lot in captured[0]["lots"]] == ["oblast-3"]
|
||||
assert counters.selected == 1
|
||||
assert counters.skipped_not_target_region == 1
|
||||
assert counters.check(), "счётчики обязаны сходиться с прочитанным"
|
||||
|
||||
|
||||
def test_writer_gets_city_none_for_oblast(captured: list[dict[str, Any]]) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue