feat(tradein/scrapers): domclick вторичка через bff-search-web JSON API (Layer A) #1982

Closed
lekss361 wants to merge 3 commits from fix/tradein-domclick-bff-api into main
Owner

Что

Реанимация DomClick-скрейпера вторички ЕКБ. Старый domclick.py (#796, HTML/camoufox через domclick.ru/search) МЁРТВ — каждый domclick_city_sweep давал done но total_seen=0 (URL redesigned на React+Vite + datacenter-IP забанен QRATOR). Переписан на bff-search-web JSON API (Layer A — SERP-список).

Контракт верифицирован живьём (2026-06-27, playwright + прод browser-pool): BrowserFetcher(source="domclick").fetch(<bff offers/v1 url>)len=114711, blocked=NO, api_items=20. Прокси уже подключён (browser-pool роутит domclick.ru → generic → shared RU mobile proxy). Zero правок browser-server, новый аккаунт не нужен.

Как

  • GET bff-search-web.domclick.ru/api/offers/v1 (список) + /api/offers/count/v1 (сайзер бакетов).
  • Параметры ground-truth: address=EKB region GUID, aids=20561, deal_type=sale, category=living, offer_type=flat, rooms {st,1..4,5+}, sale_price__gte/__lte, offset/limit=20.
  • Bucketing rooms × price с рекурсивным бинарным делением цены, т.к. жёсткий кап offset < 2000 (макс 2000 листингов/запрос). EKB вторичка ≈6400; только rooms=2 (≈2215) превышает кап → дробится по цене.
  • Координаты теперь есть (location.lat/lon, 100% покрытие — главный выигрыш vs старый scraper, который отдавал lat=lon=None).
  • Per-card гео-guard (offerRegionName=='Екатеринбург' ∨ bbox) как страховка.
  • Честный статус (#1968): 0 лотов + ошибки → mark_failed, не фейковый done.
  • Источник остаётся domklik. listing_segment='vtorichka'.

Маппинг полей → listings

Все структурные поля → типизированные колонки (source_id, source_url, lat, lon, address, area_m2, rooms, floor, total_floors, year_built, price_rub, price_per_m2, publish_date, listing_date); вспомогательные (isApartment, isRosreestrApproved, updatedDate, lastPriceHistoryState, offerRegionName, flatComplex) → raw_payload jsonb (паттерн как у cian). Ничего не дропается.

Расписание

138_update_domclick_sweep_params.sql — UPDATE default_params (rooms [0..5], pages_per_anchor=100, request_delay_sec=3). DORMANT остаётся (enabled=false) — включать вручную после prod-smoke:

UPDATE scrape_schedules SET enabled=true WHERE source='domclick_city_sweep';

Review / тесты

Прошёл adversarial multi-dimension review (5 измерений: API-контракт, bucketing/pagination, geo+dedup+DB-save vs новый UNIQUE(source,source_id), регрессия/honest-status, тесты). Critical/high — 0. Исправлены 2 medium + low:

  • open-band рекурсия: depth-guard + progressing anchor (lo+_HIGH_ANCHOR) — нет infinite-recursion / tail-loss;
  • COUNT schema-guard (missing snippetsCountmark_failed, не silent-0);
  • 5+ fallback → rooms=5; geo-drop-all canary;
  • тесты: closed-band bisect (реальный rooms=2 путь) + проверка offset<2000 (max=1980); studio force; реальный save-error sweep-тест.

169 passed (полный tests/scrapers/ + sweep), ruff + pre-commit clean.

Scope

Layer A только. Detail-карточка (renovation/wallType/EGRN/история цен — Layer B) и аналитические XHR (AVM Домклика, sold_similar — Layer C) — отдельными PR.

## Что Реанимация DomClick-скрейпера вторички ЕКБ. Старый `domclick.py` (#796, HTML/camoufox через `domclick.ru/search`) МЁРТВ — каждый `domclick_city_sweep` давал `done` но `total_seen=0` (URL redesigned на React+Vite + datacenter-IP забанен QRATOR). Переписан на **bff-search-web JSON API** (Layer A — SERP-список). Контракт верифицирован живьём (2026-06-27, playwright + прод browser-pool): `BrowserFetcher(source="domclick").fetch(<bff offers/v1 url>)` → `len=114711, blocked=NO, api_items=20`. Прокси уже подключён (browser-pool роутит `domclick.ru` → generic → shared RU mobile proxy). **Zero правок browser-server, новый аккаунт не нужен.** ## Как - `GET bff-search-web.domclick.ru/api/offers/v1` (список) + `/api/offers/count/v1` (сайзер бакетов). - Параметры ground-truth: `address`=EKB region GUID, `aids=20561`, `deal_type=sale`, `category=living`, `offer_type=flat`, `rooms` {st,1..4,5+}, `sale_price__gte/__lte`, `offset`/`limit=20`. - **Bucketing rooms × price** с рекурсивным бинарным делением цены, т.к. жёсткий кап `offset < 2000` (макс 2000 листингов/запрос). EKB вторичка ≈6400; только `rooms=2` (≈2215) превышает кап → дробится по цене. - **Координаты теперь есть** (`location.lat/lon`, 100% покрытие — главный выигрыш vs старый scraper, который отдавал `lat=lon=None`). - Per-card гео-guard (`offerRegionName=='Екатеринбург'` ∨ bbox) как страховка. - **Честный статус** (#1968): `0 лотов + ошибки → mark_failed`, не фейковый `done`. - Источник остаётся `domklik`. `listing_segment='vtorichka'`. ## Маппинг полей → `listings` Все структурные поля → типизированные колонки (`source_id, source_url, lat, lon, address, area_m2, rooms, floor, total_floors, year_built, price_rub, price_per_m2, publish_date, listing_date`); вспомогательные (`isApartment, isRosreestrApproved, updatedDate, lastPriceHistoryState, offerRegionName, flatComplex`) → `raw_payload` jsonb (паттерн как у cian). Ничего не дропается. ## Расписание `138_update_domclick_sweep_params.sql` — UPDATE `default_params` (`rooms [0..5]`, `pages_per_anchor=100`, `request_delay_sec=3`). **DORMANT остаётся** (`enabled=false`) — включать вручную после prod-smoke: ``` UPDATE scrape_schedules SET enabled=true WHERE source='domclick_city_sweep'; ``` ## Review / тесты Прошёл adversarial multi-dimension review (5 измерений: API-контракт, bucketing/pagination, geo+dedup+DB-save vs новый `UNIQUE(source,source_id)`, регрессия/honest-status, тесты). Critical/high — 0. Исправлены 2 medium + low: - open-band рекурсия: depth-guard + progressing anchor (`lo+_HIGH_ANCHOR`) — нет infinite-recursion / tail-loss; - COUNT schema-guard (missing `snippetsCount` → `mark_failed`, не silent-0); - `5+` fallback → `rooms=5`; geo-drop-all canary; - тесты: closed-band bisect (реальный rooms=2 путь) + проверка `offset<2000` (max=1980); studio force; реальный save-error sweep-тест. **169 passed** (полный `tests/scrapers/` + sweep), ruff + pre-commit clean. ## Scope Layer A только. Detail-карточка (renovation/wallType/EGRN/история цен — Layer B) и аналитические XHR (AVM Домклика, sold_similar — Layer C) — отдельными PR.
lekss361 added 3 commits 2026-06-27 11:14:49 +00:00
- DomClickScraper переписан на GET bff-search-web.domclick.ru/api/offers/v1
  с рекурсивным бинарным price-band bucketing (OFFSET_CAP=2000, open-band split
  через _HIGH_ANCHOR=30M); geo-guard по offerRegionName==ЕКБ / bbox
- scrape_pipeline.py: rooms=[0..5] (добавлен 5+), pages default=100,
  _DOMCLICK_PER_FETCH_S=4.0, честный статус mark_failed при 0 лотов + errors
- config.py: bff-search-web.domclick.ru добавлен в scrape_allowed_hosts
- data/sql/132_update_domclick_sweep_params.sql: UPDATE default_params (DORMANT)
fix(tradein/domclick): open-band recursion guard, count schema-guard, 5+ rooms, geo canary + tests
All checks were successful
CI / changes (pull_request) Successful in 9s
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
ab4bc229b2
- _walk_price_range open band: depth guard + progressing anchor (lo+_HIGH_ANCHOR)
  → forward progress, no infinite recursion / tail-loss on >cap open tail
- COUNT: raise DomClickBlockedError on missing snippetsCount (schema regression)
- _offer_to_lot: "5+" bucket fallback → rooms=5 (was None)
- _parse_page: canary when raw>0 and all geo-dropped (ekb=0)
- tests: closed-band bisect + real offset-cap (max offset 1980); studio force
  rooms=0 over present value; real save_listings-error sweep test
- 138 migration header filename 132→138
Author
Owner

Закрываю как дубликат: эквивалентная реализация уже смержена в main через #1979 (fix/tradein-domclick-bff-rewrite, commit 52fc9205) — параллельное окно зашипило тот же BFF JSON API rewrite, закрывает те же #1846 / #1968. Две реализации разошлись во времени (multi-window).

Шипнутая версия solid (bounded price-bisection до LTE_MAX, geo-guard, honest mark_failed, QRATOR-detection + <pre>-unwrap). Перенёс наблюдения из adversarial-review этой ветки в follow-up заметку (path absolute-vs-relative для source_url; bff-search-web.domclick.ru отсутствует в scrape_allowed_hosts → только admin manual-fetch). Ветку удаляю.

Закрываю как дубликат: эквивалентная реализация уже смержена в main через #1979 (`fix/tradein-domclick-bff-rewrite`, commit 52fc9205) — параллельное окно зашипило тот же BFF JSON API rewrite, закрывает те же #1846 / #1968. Две реализации разошлись во времени (multi-window). Шипнутая версия solid (bounded price-bisection до LTE_MAX, geo-guard, honest mark_failed, QRATOR-detection + `<pre>`-unwrap). Перенёс наблюдения из adversarial-review этой ветки в follow-up заметку (path absolute-vs-relative для source_url; `bff-search-web.domclick.ru` отсутствует в `scrape_allowed_hosts` → только admin manual-fetch). Ветку удаляю.
lekss361 closed this pull request 2026-06-27 11:22:04 +00:00
All checks were successful
CI / changes (pull_request) Successful in 9s
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

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1982
No description provided.