feat(scrapers): yandex detail-enrichment — save + ночной backfill через прокси (part of #1553) #1554

Merged
bot-backend merged 3 commits from feat/yandex-detail-backfill into main 2026-06-16 11:18:19 +00:00
Owner

Проблема (#1553)

Yandex detail-данные не сохранялись НИКУДА: YandexDetailScraper.fetch_detail парсит 17 полей, но save_detail_enrichment отсутствовал, и detail не вызывался в свипе. На проде (verified): yandex area_m2 23%, living/kitchen 0%, repair_state 1%.

Фикс

Реализован yandex detail save + ночной backfill для 3952 существующих (аналог avito avito_detail_backfill #1552):

  • yandex_detail.py — новый save_detail_enrichment(db, listing_id, e): UPDATE listings (COALESCE для базовых rooms/area/floor/repair/description/address — не затирает непустое; metro_stations/photo_urls → jsonb; agency_*/views_total_yandex/publish_date_relative). psycopg v3 CAST. Возвращает bool.
  • app/tasks/yandex_detail_backfill.py (new) — run_yandex_detail_backfill: snapshot source='yandex' AND detail_enriched_at IS NULL (приоритет active+свежие), budget guard, run-lifecycle.
    • ⚠️ Прокси-путь (ключевое): YandexDetailScraper.fetch_detail = plain httpx БЕЗ прокси → captcha/shell на datacenter-IP. Поэтому fetch идёт через curl_cffi AsyncSession(impersonate=chrome120, proxies=scraper_proxy_url) (зеркало рабочего yandex_address_backfill), затем YandexDetailScraper().parse(html, offer_url) (чистая функция). Captcha→parse None.
    • Блок-семантика: parse→None / non-200 / fetch-exc → consecutive_none++; после max_consecutive_blocks → abort + mark_done (не failed; объявление остаётся NULL → повтор след. прогон). reset на успехе. db.rollback() на ошибке.
  • scheduler.pytrigger_yandex_detail_backfill_run + dispatch elif.
  • data/sql/113_yandex_detail_backfill.sqlADD COLUMN IF NOT EXISTS photo_urls/newbuilding_url/newbuilding_id (остальные 14 полей уже имеют колонки по 002/011/033); seed scrape_schedules enabled=true окно 12-15 UTC (не пересекается с avito_detail_backfill 9-12), batch_size=800 budget_sec=3600 delay=5 max_consecutive_blocks=5, idempotent.
  • tests — 9 кейсов task + 3 save.

Проверки

  • uv run pytest (полный suite, оба CI-deselect): 1821 passed, 2 deselected, 0 failed.
  • ruff check + ruff format --check: clean.
  • code-reviewer: (после применения 2 minor: stale-комментарий + observability save-ошибки). Прокси-путь подтверждён (нет остаточного plain-httpx), завершение run'а гарантировано.

Scope / остаётся в #1553

Этот PR = yandex save + backfill (даёт обогащение 3952 сразу, через yandex_detail_backfill крон, независимо от dormant yandex_city_sweep). НЕ входит (follow-up #1553): интеграция detail-стадии в активный свип + cian kitchen_area_m2 (minor).

Миграция 113 (110-112 уже в main).

## Проблема (#1553) Yandex detail-данные не сохранялись НИКУДА: `YandexDetailScraper.fetch_detail` парсит 17 полей, но `save_detail_enrichment` отсутствовал, и detail не вызывался в свипе. На проде (verified): yandex `area_m2` 23%, `living/kitchen` 0%, `repair_state` 1%. ## Фикс Реализован yandex detail save + ночной backfill для 3952 существующих (аналог avito `avito_detail_backfill` #1552): - **`yandex_detail.py`** — новый `save_detail_enrichment(db, listing_id, e)`: UPDATE listings (COALESCE для базовых rooms/area/floor/repair/description/address — не затирает непустое; metro_stations/photo_urls → jsonb; agency_*/views_total_yandex/publish_date_relative). psycopg v3 CAST. Возвращает bool. - **`app/tasks/yandex_detail_backfill.py`** (new) — `run_yandex_detail_backfill`: snapshot `source='yandex' AND detail_enriched_at IS NULL` (приоритет active+свежие), budget guard, run-lifecycle. - ⚠️ **Прокси-путь (ключевое):** `YandexDetailScraper.fetch_detail` = plain httpx БЕЗ прокси → captcha/shell на datacenter-IP. Поэтому fetch идёт через **`curl_cffi AsyncSession(impersonate=chrome120, proxies=scraper_proxy_url)`** (зеркало рабочего `yandex_address_backfill`), затем `YandexDetailScraper().parse(html, offer_url)` (чистая функция). Captcha→parse None. - Блок-семантика: parse→None / non-200 / fetch-exc → `consecutive_none++`; после `max_consecutive_blocks` → abort + `mark_done` (не failed; объявление остаётся NULL → повтор след. прогон). reset на успехе. db.rollback() на ошибке. - **`scheduler.py`** — `trigger_yandex_detail_backfill_run` + dispatch elif. - **`data/sql/113_yandex_detail_backfill.sql`** — `ADD COLUMN IF NOT EXISTS` photo_urls/newbuilding_url/newbuilding_id (остальные 14 полей уже имеют колонки по 002/011/033); seed scrape_schedules enabled=true окно **12-15 UTC** (не пересекается с avito_detail_backfill 9-12), `batch_size=800 budget_sec=3600 delay=5 max_consecutive_blocks=5`, idempotent. - **tests** — 9 кейсов task + 3 save. ## Проверки - `uv run pytest` (полный suite, оба CI-deselect): **1821 passed, 2 deselected, 0 failed**. - `ruff check` + `ruff format --check`: clean. - code-reviewer: ✅ (после применения 2 minor: stale-комментарий + observability save-ошибки). Прокси-путь подтверждён (нет остаточного plain-httpx), завершение run'а гарантировано. ## Scope / остаётся в #1553 Этот PR = yandex save + backfill (даёт обогащение 3952 сразу, через `yandex_detail_backfill` крон, независимо от dormant `yandex_city_sweep`). НЕ входит (follow-up #1553): интеграция detail-стадии в активный свип + cian `kitchen_area_m2` (minor). Миграция **113** (110-112 уже в main).
lekss361 added 3 commits 2026-06-16 11:14:52 +00:00
feat(tradein): yandex detail backfill — save_detail_enrichment + nightly task (#1553)
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
03e43f12b5
- yandex_detail.py: добавить save_detail_enrichment(db, listing_id, enrichment) → bool;
  UPDATE listings COALESCE по 17 полям (rooms/area_m2/floor/total_floors/address/
  description/repair_state/publish_date/views_total_yandex/publish_date_relative/
  agency_name/agency_founded_year/agency_objects_count/metro_stations/photo_urls/
  newbuilding_url/newbuilding_id); metro/photos → jsonb; detail_enriched_at = NOW()
- app/tasks/yandex_detail_backfill.py: новый task run_yandex_detail_backfill;
  snapshot SELECT source='yandex' AND detail_enriched_at IS NULL; YandexDetailScraper
  async context manager; consecutive None abort (max_consecutive_blocks=5); budget guard;
  run lifecycle update_heartbeat/mark_done/mark_failed
- scheduler.py: trigger_yandex_detail_backfill_run + elif source=='yandex_detail_backfill'
  в scheduler_loop; window 12-15 UTC (после avito_detail 09-12 UTC)
- migration 113: ADD COLUMN IF NOT EXISTS photo_urls jsonb, newbuilding_url text,
  newbuilding_id text; seed scrape_schedules yandex_detail_backfill enabled=true
- tests: 10 тестов (8 backfill + 2 save_detail_enrichment); 1819 passed 0 failed

Closes #1553
fix(tradein): yandex detail backfill — заменить fetch_detail на curl_cffi+proxy
All checks were successful
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
e2e0291a0a
YandexDetailScraper.fetch_detail использует plain httpx (BaseScraper._http_get)
без прокси и без TLS-fingerprinting → на датацентровом IP Yandex отдаёт капчу /
shell-HTML → parse всегда None → backfill 0% эффективен.

Замена: curl_cffi AsyncSession(chrome120 + scraper_proxy_url), идентично
yandex_address_backfill.py (доказано работающий путь на проде). HTTP 200 →
YandexDetailScraper.parse(html) (чистая функция, без сети). parse→None
считается fail (captcha wall); consecutive None → abort после max_consecutive_blocks.

Тесты обновлены: мокается AsyncSession.get + YandexDetailScraper.parse
(не fetch_detail). Добавлены кейсы: non-200 → abort, proxy=None → proxies=None.
fix(tradein): review minors — scheduler comment + save-error log label
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / changes (pull_request) Successful in 6s
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
fd4a0d643d
- scheduler.py: исправлен stale-комментарий yandex_detail_backfill
  (был: «YandexDetailScraper httpx, no proxy layer»;
   теперь: fetch via curl_cffi chrome120 + scraper_proxy_url,
           parse via YandexDetailScraper.parse)
- yandex_detail_backfill.py: outer except — сообщение лога изменено на
  «save/iteration error for listing_id=%d» чтобы отличать save-ошибку
  от fetch/parse-None путей (run_id убран — он менее важен в этом scope)
lekss361 added the
enhancement
scope/backend
scrapers
labels 2026-06-16 11:14:57 +00:00
bot-backend merged commit d76df54ece into main 2026-06-16 11:18:19 +00:00
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#1554
No description provided.