feat(tradein): DomClick вторичка scraper via BrowserFetcher (#796) #904

Merged
lekss361 merged 2 commits from feat/796-domclick-scraper into main 2026-05-31 14:43:40 +00:00
Collaborator

Summary

  • Новый DomClickScraper — парсит вторичку через headless BrowserFetcher (DataDome проходит, curl_cffi 401)
  • fetch_city(city_id, rooms, pages) — citywide sweep с разбивкой по комнатности
  • DOM-extract: a[href*="/card/"] → source_id + title-parse (rooms/area/floor/price)
  • Proxy автоматически из settings.scraper_proxy_url (BrowserFetcher читает сам)
  • domclick.ru добавлен в scrape_allowed_hosts
  • 37 unit-тестов (mock HTML)

Closes #796 (PoC steps 1-3 done; step 4 ЦИАН B2B — отдельный issue)

Test plan

  • uv run pytest tradein-mvp/backend/tests/scrapers/test_domclick.py -v — 37/37
  • Ручной прогон fetch_city(city_id=4, rooms=[2]) с proxy на проде (city_id=4 = ЕКБ)
  • Проверить parse_failures counter после первого реального прогона
## Summary - Новый `DomClickScraper` — парсит вторичку через headless BrowserFetcher (DataDome проходит, curl_cffi 401) - `fetch_city(city_id, rooms, pages)` — citywide sweep с разбивкой по комнатности - DOM-extract: `a[href*="/card/"]` → source_id + title-parse (rooms/area/floor/price) - Proxy автоматически из `settings.scraper_proxy_url` (BrowserFetcher читает сам) - `domclick.ru` добавлен в `scrape_allowed_hosts` - 37 unit-тестов (mock HTML) ## Closes / Related Closes #796 (PoC steps 1-3 done; step 4 ЦИАН B2B — отдельный issue) ## Test plan - [ ] `uv run pytest tradein-mvp/backend/tests/scrapers/test_domclick.py -v` — 37/37 - [ ] Ручной прогон `fetch_city(city_id=4, rooms=[2])` с proxy на проде (city_id=4 = ЕКБ) - [ ] Проверить `parse_failures` counter после первого реального прогона
bot-backend added 1 commit 2026-05-31 13:59:27 +00:00
bot-backend added the
scope/backend
scrapers
status/review
tradein
labels 2026-05-31 13:59:40 +00:00
Collaborator

Code review — ⚠️ запрос изменений (1 блокер + 2 follow-up)

Хорошая реализация: BrowserFetcher через async with (нет утечки ресурсов), двухуровневая дедупликация (seen_ids в памяти + ON CONFLICT в save_listings), proxy через BrowserFetcher._proxy_dict(), нет hardcoded secrets/SQLi, request_delay_sec=8.0 > Cian консервативно, ruff ≤100. 37 тестов на парсинг проходят.

🔴 Блокер (нужно до merge) — source name mismatch

domclick.py пишет source="domclick", но весь остальной кодебейс использует "domklik":

  • trade_in_pdf.py:182/192 — PDF-экспортёр ищет "domklik" для цвета плашки + display-name «Домклик · Сбер»
  • trade_in_pdf.py:803_deal_source_keys = {"etazhi", "domklik", "rosreestr"}
  • schemas/trade_in.py:51,446 — docstring-enum 'domklik'
  • scrapers/base.py:66 — comment 'domklik'

С "domclick" в prod листинги ДомКлик не получат зелёную плашку «Домклик · Сбер» — экспортёр не найдёт source в маппинге (нейтральный цвет/имя вместо бренда).
Fix: переименовать source = "domklik" + ScrapedLot(source="domklik", ...) (domclick.py строки ~128, 134, 302, 308). Минимальный, upstream-consistent. + поправить соответствующий ассерт в test_domclick.py.

🟠 Follow-up (можно отдельным PR/issue, но желательно сразу)

1. Нет get_scraper_delay() — задержка hardcoded. Все другие scrapers (avito/cian/n1/yandex) берут get_scraper_delay(self.name) в __init__ из таблицы scraper_settings (runtime-configurable через admin API). DomClick хардкодит 8.0 → нельзя замедлить/ускорить без деплоя. Добавить вызов в __init__ + "domklik": 8.0 в _DEFAULT_DELAY_BY_SOURCE (scraper_settings.py).

2. Нет детекции DataDome block-страницы — silent data loss. BrowserFetcher.fetch() может вернуть HTTP 200 + DataDome block-HTML (протухший fingerprint / плохой proxy IP). Тогда _parse_html() вернёт [] → sweep тихо завершится с нулём лотов, parse_failures=0 — мониторинг не поймает блокировку. Другие scrapers это ловят явно (avito.py AvitoBlockedError, yandex_realty.py _CAPTCHA_MARKERS/_is_captcha). Минимум — проверять DataDome-маркеры в начале _parse_html и логировать WARNING / raise DomClickBlockedError.

Заметка (не блокер)

admin.py:74 sources: Literal["avito","cian","yandex","n1"] — DomClick там не зарегистрирован, т.е. через общий /admin/scrape endpoint он пока недоступен (вызывается напрямую из Celery/admin). OK для этого PR, но стоит зафиксировать в issue, чтобы следующий не удивился.


После фикса блокера #1 — повторный review + merge. 🟠-пункты можно вынести в follow-up issue, если не хочешь раздувать PR.

## Code review — ⚠️ запрос изменений (1 блокер + 2 follow-up) Хорошая реализация: `BrowserFetcher` через `async with` (нет утечки ресурсов), двухуровневая дедупликация (`seen_ids` в памяти + `ON CONFLICT` в `save_listings`), proxy через `BrowserFetcher._proxy_dict()`, нет hardcoded secrets/SQLi, `request_delay_sec=8.0` > Cian консервативно, ruff ≤100. 37 тестов на парсинг проходят. ### 🔴 Блокер (нужно до merge) — source name mismatch `domclick.py` пишет `source="domclick"`, но весь остальной кодебейс использует `"domklik"`: - `trade_in_pdf.py:182/192` — PDF-экспортёр ищет `"domklik"` для цвета плашки + display-name «Домклик · Сбер» - `trade_in_pdf.py:803` — `_deal_source_keys = {"etazhi", "domklik", "rosreestr"}` - `schemas/trade_in.py:51,446` — docstring-enum `'domklik'` - `scrapers/base.py:66` — comment `'domklik'` С `"domclick"` в prod листинги ДомКлик не получат зелёную плашку «Домклик · Сбер» — экспортёр не найдёт source в маппинге (нейтральный цвет/имя вместо бренда). **Fix:** переименовать `source = "domklik"` + `ScrapedLot(source="domklik", ...)` (domclick.py строки ~128, 134, 302, 308). Минимальный, upstream-consistent. + поправить соответствующий ассерт в `test_domclick.py`. ### 🟠 Follow-up (можно отдельным PR/issue, но желательно сразу) **1. Нет `get_scraper_delay()` — задержка hardcoded.** Все другие scrapers (avito/cian/n1/yandex) берут `get_scraper_delay(self.name)` в `__init__` из таблицы `scraper_settings` (runtime-configurable через admin API). DomClick хардкодит 8.0 → нельзя замедлить/ускорить без деплоя. Добавить вызов в `__init__` + `"domklik": 8.0` в `_DEFAULT_DELAY_BY_SOURCE` (`scraper_settings.py`). **2. Нет детекции DataDome block-страницы — silent data loss.** `BrowserFetcher.fetch()` может вернуть HTTP 200 + DataDome block-HTML (протухший fingerprint / плохой proxy IP). Тогда `_parse_html()` вернёт `[]` → sweep тихо завершится с нулём лотов, `parse_failures=0` — мониторинг не поймает блокировку. Другие scrapers это ловят явно (`avito.py` `AvitoBlockedError`, `yandex_realty.py` `_CAPTCHA_MARKERS`/`_is_captcha`). Минимум — проверять DataDome-маркеры в начале `_parse_html` и логировать WARNING / raise `DomClickBlockedError`. ### Заметка (не блокер) `admin.py:74` `sources: Literal["avito","cian","yandex","n1"]` — DomClick там не зарегистрирован, т.е. через общий `/admin/scrape` endpoint он пока недоступен (вызывается напрямую из Celery/admin). OK для этого PR, но стоит зафиксировать в issue, чтобы следующий не удивился. --- После фикса блокера #1 — повторный review + merge. 🟠-пункты можно вынести в follow-up issue, если не хочешь раздувать PR.
bot-backend added
status/needs-fix
and removed
status/review
labels 2026-05-31 14:17:03 +00:00
bot-backend added 1 commit 2026-05-31 14:21:41 +00:00
bot-backend added
status/review
and removed
status/needs-fix
labels 2026-05-31 14:21:58 +00:00
lekss361 reviewed 2026-05-31 14:42:32 +00:00
lekss361 left a comment
Owner

Manual review (code-reviewer subagent) — ⚠️ MINOR

Корректность/безопасность — без блокеров. Mergeable после 1 пункта ниже.

👍 Сильно

  • Дедуп 2-слойный: in-memory seen_ids в fetch_city + DB ON CONFLICT (dedup_hash) в save_listings — верный паттерн.
  • Price-extract из дочерних DOM-узлов — защита от склейки «адрес+цена» (хорошо задокументировано).
  • BrowserFetcher lifecycle чистый (async with, нет утечек сессий); DataDome-детект через _is_blocked_page → clean break.
  • Proxy из settings.scraper_proxy_url читается в __aenter__ (не на импорте), не логируется. scrape_allowed_hosts расширен корректно. SQL-инъекций нет (вся персистенция через save_listings). ruff line-100 ок (длинные строки — кириллица, счёт по символам).

🔧 Перед merge (actionable)

  • Нет SQL-миграции, сидящей строку domklik в scraper_settings. У всех остальных скраперов (avito/cian/n1/yandex/domrf) есть seeded-row через migr 053/054; без неё admin UI не покажет/не отредактирует delay для domklik (только ручной INSERT). Добавить 055_scraper_settings_domklik.sql (или дописать в 054) с INSERT ... ON CONFLICT DO NOTHING.

Post-merge polish (не блокеры)

  • domclick.py:224 — dead-fallback lot.source_id or lot.source_url (внутри fetch_city source_id никогда None из-за guard sale__flat__); сделать assert или убрать.
  • domclick.py:261 — добавить явный except Exception: logger.error(...); break после DomClickBlockedError, иначе ошибка _parse_html крутит остаток страниц с 0 результатов.
  • Тесты: нет покрытия _extract_price_from_children (прод-путь), _extract_address → None, малформед HTML, source_id=None defensive-ветки.
  • DomClickScraper.source = "domklik" — class-attr нигде не используется (другие скраперы определяют только name); убрать или задокументировать.

Это ручной advisory-review (main-сессия). Финальный verdict + merge — за auto-code-reviewer.

## Manual review (code-reviewer subagent) — ⚠️ MINOR Корректность/безопасность — без блокеров. Mergeable после 1 пункта ниже. **👍 Сильно** - Дедуп 2-слойный: in-memory `seen_ids` в `fetch_city` + DB `ON CONFLICT (dedup_hash)` в `save_listings` — верный паттерн. - Price-extract из дочерних DOM-узлов — защита от склейки «адрес+цена» (хорошо задокументировано). - BrowserFetcher lifecycle чистый (`async with`, нет утечек сессий); DataDome-детект через `_is_blocked_page` → clean break. - Proxy из `settings.scraper_proxy_url` читается в `__aenter__` (не на импорте), не логируется. `scrape_allowed_hosts` расширен корректно. SQL-инъекций нет (вся персистенция через `save_listings`). ruff line-100 ок (длинные строки — кириллица, счёт по символам). **🔧 Перед merge (actionable)** - **Нет SQL-миграции, сидящей строку `domklik` в `scraper_settings`.** У всех остальных скраперов (avito/cian/n1/yandex/domrf) есть seeded-row через migr 053/054; без неё admin UI не покажет/не отредактирует delay для domklik (только ручной INSERT). Добавить `055_scraper_settings_domklik.sql` (или дописать в 054) с `INSERT ... ON CONFLICT DO NOTHING`. **Post-merge polish (не блокеры)** - `domclick.py:224` — dead-fallback `lot.source_id or lot.source_url` (внутри `fetch_city` source_id никогда None из-за guard `sale__flat__`); сделать `assert` или убрать. - `domclick.py:261` — добавить явный `except Exception: logger.error(...); break` после `DomClickBlockedError`, иначе ошибка `_parse_html` крутит остаток страниц с 0 результатов. - Тесты: нет покрытия `_extract_price_from_children` (прод-путь), `_extract_address → None`, малформед HTML, `source_id=None` defensive-ветки. - `DomClickScraper.source = "domklik"` — class-attr нигде не используется (другие скраперы определяют только `name`); убрать или задокументировать. _Это ручной advisory-review (main-сессия). Финальный verdict + merge — за auto-code-reviewer._
lekss361 merged commit 98b0632dae into main 2026-05-31 14:43:40 +00:00
lekss361 deleted branch feat/796-domclick-scraper 2026-05-31 14:43:40 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
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#904
No description provided.