feat(trade-in): регулярный сбор ДомКлика по Москве и области
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Successful in 4m52s

Миграция 305 завела регулярный сбор 77/50 для avito/cian/yandex и прямо
отложила ДомКлик: "у него нет wildcard-хендлера, а единственная возможная
строка source='domclick_city_sweep' занята Екатеринбургом". Это вторая
половина: код плюс строки расписания.

ДомКлик даёт около 23% корпуса обоих регионов — без него свежий срез 77/50
теряет почти четверть массы, а вместе с ней часть домов, по которым вообще
существуют аналоги.

Wildcard заведён на ОБЕИХ сторонах, и это не дубль. Продуктовый
_job_domclick_city_sweep перекрывает kit-native ради инъекции куки сессии
Sber ID (#3264); kit-native тело ходит на BFF без кук. Поставь wildcard
только в _default_kit_handlers — перекрытие осталось бы у ЕКБ и молча
пропало бы у регионов, а московская строка упёрлась бы в QRATOR ровно так,
как было до #3264. Новый тест
test_domclick_region_wildcard_resolves_to_product_handler пинит именно это:
все три source'а обязаны резолвиться на продуктовый job.

Гео-скоуп задаёт ТОЛЬКО region_code — профиль региона
(DOMCLICK_GEO_PROFILES) несёт address GUID, страницу прогрева QRATOR, bbox
гарда и имя города; профили 77 и 50 заведены и проверены живыми запросами
12-15.09.2026 (#3533). city_id в default_params намеренно не задан: он
vestigial, город штампуется из профиля. Неизвестный region_code роняет
прогон до первого запроса, тихого отката на ЕКБ нет.

Claude-Session: https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs
This commit is contained in:
bot-backend 2026-09-16 23:18:58 +03:00
parent 8ebd63780f
commit 07047750be
4 changed files with 96 additions and 2 deletions

View file

@ -1008,6 +1008,12 @@ def build_product_handlers(ctx: SchedulerContext) -> dict[str, Handler]:
),
# Override kit-native (#3264) — инъекция куки сессии, см. докстринг job'а выше.
"domclick_city_sweep": Handler(_job_domclick_city_sweep, "domclick_city_sweep"),
# Вне ЕКБ каждый регион получает СВОЙ source ("domclick_city_sweep_moskva"),
# потому что scrape_schedules.source UNIQUE. Wildcard обязан стоять и здесь,
# а не только в _default_kit_handlers: kit-native тело ходит БЕЗ кук сессии,
# и московская строка молча резолвилась бы на версию без инъекции (#3264),
# то есть на ту, что упирается в QRATOR. Ключ продукта перекрывает kit-native.
"domclick_city_sweep_*": Handler(_job_domclick_city_sweep, "domclick_city_sweep_*"),
"cadastral_geo_match": Handler(_job_cadastral_geo_match, "cadastral_geo_match"),
"osm_poi_ekb_refresh": Handler(_job_osm_poi_ekb_refresh, "osm_poi_ekb_refresh"),
"dtp_stat_refresh": Handler(_job_dtp_stat_refresh, "dtp_stat_refresh"),

View file

@ -0,0 +1,59 @@
-- 306_scrape_schedules_seed_domclick_msk.sql
-- Регулярный сбор ДомКлика по Москве (77) и Московской области (50).
--
-- Apply after: 305_scrape_schedules_seed_msk_city_sweeps.sql
--
-- WHY:
-- Миграция 305 завела регулярный сбор 77/50 для avito / cian / yandex и прямо
-- отложила ДомКлик: "у него нет wildcard-хендлера, а единственная возможная
-- строка source='domclick_city_sweep' занята Екатеринбургом. Нужна правка кода".
-- Правка сделана в этом же PR — "domclick_city_sweep_*" добавлен в
-- _default_kit_handlers (scraper-kit) И в build_product_handlers (app-side
-- override с инъекцией куки Sber ID, #3264). Эта миграция — вторая половина.
--
-- Цена вопроса: ДомКлик даёт ≈23% корпуса обоих регионов. Без него свежий срез
-- 77/50 теряет почти четверть массы, а вместе с ней — часть домов, по которым
-- вообще существуют аналоги.
--
-- Гео-скоуп задаётся ТОЛЬКО region_code: профиль региона
-- (providers/domclick/serp.py DOMCLICK_GEO_PROFILES) несёт address GUID,
-- страницу прогрева QRATOR, bbox гарда и имя города. Профили 77 и 50 заведены и
-- проверены живыми запросами 12-15.09.2026 (#3533). city_id здесь НЕ задаётся
-- намеренно: он vestigial — run_domclick_city_sweep штампует город из профиля,
-- а не из city_id (pipeline.py, комментарий у save_listings). Неизвестный
-- region_code роняет прогон ДО первого запроса, тихого отката на ЕКБ нет.
--
-- pages_per_anchor=100 — как у ЕКБ-строки: ДомКлик citywide, anchor-loop у него
-- отсутствует (fetch_around → NotImplementedError), "pages" здесь означает
-- глубину пагинации на комнатный бакет, а не число якорей. Москва отдаёт ≈23 690
-- лотов вторички, то есть глубину надо брать полную, а не свипную.
--
-- Окна 0-3 и 3-6 UTC: 305 занял 16-23, ЕКБ-домклик сидит в 3-6. Ставим Москву в
-- 0-3 (пусто), область — в 3-6 рядом с ЕКБ; прогоны всё равно сериализуются
-- планировщиком, окно лишь ограничивает старт.
--
-- interval_days=3 — как у остальных строк 305: корпус живёт 14 суток
-- (LISTINGS_FRESH_DAYS), трёхсуточный такт даёт четырёхкратный запас на
-- пропуски из-за QRATOR-банов.
--
-- ВКЛЮЧЕНЫ СРАЗУ (enabled=true) по той же причине, что и 305: регионы уходят
-- тестировщикам, корпус истекает.
--
-- ИДЕМПОТЕНТНОСТЬ: ON CONFLICT (source) DO NOTHING — повторный прогон no-op и не
-- сбрасывает enabled, если строку потом выключили вручную.
BEGIN;
SET LOCAL lock_timeout = '5s';
INSERT INTO scrape_schedules (source, enabled, window_start_hour, window_end_hour, default_params)
VALUES
('domclick_city_sweep_moskva', true, 0, 3,
'{"region_code": 77, "pages_per_anchor": 100, "interval_days": 3,
"request_delay_sec": 6}'::jsonb),
('domclick_city_sweep_moskovskaya_oblast', true, 3, 6,
'{"region_code": 50, "pages_per_anchor": 100, "interval_days": 3,
"request_delay_sec": 6}'::jsonb)
ON CONFLICT (source) DO NOTHING;
COMMIT;

View file

@ -108,6 +108,7 @@ _KIT_NATIVE_SOURCES = {
"cian_city_sweep_*",
"cian_full_load",
"domclick_city_sweep",
"domclick_city_sweep_*",
}
@ -180,9 +181,11 @@ def test_real_build_product_handlers_covers_all_scheduled_sources() -> None:
def test_kit_native_handler_set() -> None:
"""Ровно 11 kit-native sweep-обработчиков зарегистрированы встроенно (8 + 3 oblast
"""Ровно 12 kit-native sweep-обработчиков зарегистрированы встроенно (8 + 4 wildcard).
"*_city_sweep_*" wildcard, #179).
"*_city_sweep_*" per-city/per-region source'ы вне ЕКБ: avito/cian/yandex завела
oblast B1 rollout (region 66, #179), domclick — регулярный сбор 77/50 (миграция 306),
у него суффикс несёт регион, а не город.
"""
registry = build_registry()
assert set(registry) == _KIT_NATIVE_SOURCES
@ -190,6 +193,31 @@ def test_kit_native_handler_set() -> None:
assert resolve_handler(src, registry).log_name == src
def test_domclick_region_wildcard_resolves_to_product_handler() -> None:
"""'domclick_city_sweep_moskva' резолвится на ПРОДУКТОВЫЙ handler, не на kit-native.
Развилка не косметическая: kit-native тело ходит на BFF без кук сессии Sber ID, и
московская строка, севшая на него, упёрлась бы в QRATOR ровно так, как было до #3264.
Продуктовый ключ обязан перекрывать kit-native и на wildcard, а не только на точном
'domclick_city_sweep' иначе перекрытие есть у ЕКБ и молча отсутствует у регионов.
"""
from app.services.product_handlers import (
_job_domclick_city_sweep as product_job,
)
from app.services.product_handlers import build_product_handlers
# ctx=None безопасен — job'ы его не замыкают при сборке dict (см. тест выше).
registry = build_registry(build_product_handlers(ctx=None)) # type: ignore[arg-type]
for src in (
"domclick_city_sweep",
"domclick_city_sweep_moskva",
"domclick_city_sweep_moskovskaya_oblast",
):
handler = resolve_handler(src, registry)
assert handler is not None, src
assert handler.job is product_job, src
def test_unknown_source_resolves_none() -> None:
registry, _ = _build_recording_registry()
assert resolve_handler("totally_unknown_source", registry) is None

View file

@ -1296,6 +1296,7 @@ def _default_kit_handlers() -> dict[str, Handler]:
"cian_city_sweep_*": Handler(_job_cian_city_sweep, "cian_city_sweep_*"),
"cian_full_load": Handler(_job_cian_full_load, "cian_full_load"),
"domclick_city_sweep": Handler(_job_domclick_city_sweep, "domclick_city_sweep"),
"domclick_city_sweep_*": Handler(_job_domclick_city_sweep, "domclick_city_sweep_*"),
}