ДомКлик — четвёртая площадка ручного сбора Москвы #3496

Merged
lekss361 merged 1 commit from feat/msk-domclick-collector into main 2026-09-12 12:05:19 +00:00

1 commit

Author SHA1 Message Date
bot-backend
b4ca450f18 feat(msk-collector): адаптер DomClick — четвёртая площадка ручного сбора Москвы
All checks were successful
CI Trade-In / changes (pull_request) Successful in 16s
CI / changes (pull_request) Successful in 16s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (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
Авито, Циан и Яндекс по Москве уже залиты этим скриптом; ДомКлик был
единственным непокрытым поставщиком, а таблица msk_raw.domclick_cards стояла
пустой. Адаптер закрывает пробел.

## Почему локальный сборщик, а не параметризация боевого скрейпера

Разведка настаивала на прод-пути, ссылаясь на замер «навигацией на BFF
приходит только ChallengeTimeout, 9/9 и 6/6». Замер снят через пул мобильных
прокси и к браузеру владельца не относится: оттуда повторная навигация на ту
же ручку отдаёт 200 и валидный JSON — кука QRATOR в контексте уже решена, а
сессия Сбер ID жива (auth/me → 200). Проверено 12.09.2026.

Параметризация боевого скрейпера по городу остаётся отдельной задачей под
РЕГУЛЯРНЫЙ сбор. Здесь нужна разовая заливка тем же инструментом, которым
сделаны три предыдущие площадки.

## Устройство

Регион задаётся GUID-ом Москвы в параметре address; aids, захардкоженный для
ЕКБ, Москве не нужен — без него счётчик сходится с числом в заголовке выдачи.

Бисекция — по цене, без комнатных корзин. Потолок пагинации замерен: offset
1980 отдаёт 20 карточек, 2000 даёт HTTP 400. Отсюда page_size 20, max_pages
100, hard_cap 2000. Полная вторичка Москвы — 23 690 по pagination.total, и
план из 24 коридоров укладывается в потолок с запасом (максимум 1425).

Счётчик берётся из pagination.total самой выдачи, а НЕ из отдельной
count-ручки: та считает офферы вместе с дублями (25 898 против 7 651 на
двушках) и для бисекции непригодна — разница пагинацией недостижима.

Маппинг переиспользует DomClickScraper._map_item из кита. Гео-гард —
собственный, по bbox Москвы с ТиНАО: _is_geo_ok кита сверяет
offerRegionName с «Екатеринбургом», а в московской выдаче встречаются
«x. Говорово», «пос. Птичное», «x. Ильичевка» — гард по названию региона
молча резал бы легитимные лоты Новой Москвы.

## Экранирование в <pre>

page.content() отдаёт сериализованный документ, а не тело ответа, и Chrome
экранирует в текстовом узле амперсанд, угловые скобки и неразрывный пробел.
Синтаксис JSON от этого не страдает, поэтому дефект тихий: разбор проходит, а
в адресе и описании оседают &amp; и &nbsp; — 29 штук на одной странице из
двадцати карточек. Добавлен _unescape_pre_entities.

Кавычки в список замен не входят намеренно: сериализатор экранирует их только
в значениях атрибутов, а в текстовом узле кавычка остаётся собой. Разворот
&quot; превратил бы экранированную кавычку внутри строки JSON в невалидную
последовательность и уронил бы разбор страницы целиком.

## Проверки

Живой запрос через Loader: 20 карточек, geo_filtered 0, parse_failures 0.
Первая тысяча строк на проде — 0 без цены, 0 без координат, 0 без площади, 0
с остатками экранирования, координаты внутри bbox.

Нечисловая координата отбрасывает одну карточку, а не роняет прогон: сбор
идёт часами без присмотра, и падение на середине коридора стоило бы дороже
потерянного лота.

Claude-Session: https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs
2026-09-12 15:03:44 +03:00