feat(tradein): yandex_valuation.py — anonymous house-history scraper #468
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#468
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-yandex-valuation-api"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation
Stage 6 of 9 for YandexRealtyScraper v1 (Wave 4 / Worker C — parallel with
yandex_detail.pyandyandex_newbuilding.py).Yandex Valuation tool is the only anonymous house-history source in our stack — vs Cian Calculator (cookies required) and Avito IMV (POST + JWT geoHash). Pagination via
?page=Nyields 12+ historical offers per house — the widest history net we have.Module
tradein-mvp/backend/app/services/scrapers/yandex_valuation.py—YandexValuationScraper(BaseScraper)with asyncfetch_house_history(address, offer_category="APARTMENT", offer_type="SELL", page=1).URL:
GET /otsenka-kvartiry-po-adresu-onlayn/?address=...&offerCategory=...&offerType=...&page=N— NO cookies, NO auth.Models
ValuationHouseMeta:
year_built,total_floors,house_type,ceiling_height,has_lift,total_objects,has_panoramaValuationHistoryItem:
area_m2,rooms(0 for studio),floor,start_price,start_price_per_m2,last_price,last_price_per_m2,publish_date(DMY),exposure_days,status(В продаже / Снято)YandexValuationResult: house + history_items[] + pagination metadata + source_url + raw_payload
History extraction strategy
Two-tier fallback (Yandex history items have unstable selectors):
[data-test*="HistoryItem"]/[data-test*="ValuationItem"]containers (if Yandex adds them later)(publish_date, area_m2, floor). Capped at 30 items per page.Worker notes
_RE_PRICE_TOKENcarries a negative lookahead(?!\s*за\s*м²)to prevent per-m2 price tokens being captured as total-price tokens. Without it,last_pricewould mis-resolve to e.g. 117000 (the m² rate) instead of 3_100_000 (the actual price).Tests
Files
tradein-mvp/backend/app/services/scrapers/yandex_valuation.py(new, 331 lines)tradein-mvp/backend/tests/test_yandex_valuation.py(new, 242 lines)Reviewer note
Reviewer: deep-code-reviewer — only merge after APPROVE. Wave 4 sibling PRs: yandex_detail.py + yandex_newbuilding.py.
Stage 6 of YandexRealtyScraper v1 (Wave 4 / Worker C — parallel with yandex_detail.py and yandex_newbuilding.py). Module: app/services/scrapers/yandex_valuation.py - YandexValuationScraper(BaseScraper) — fetch_house_history(address, ...) URL: GET /otsenka-kvartiry-po-adresu-onlayn/?address=...&offerCategory=... &offerType=...&page=N ANONYMOUS — no cookies, no auth (vs Cian Calculator). - ValuationHouseMeta (year_built, total_floors, house_type, ceiling_height, has_lift, total_objects, has_panorama) - ValuationHistoryItem (area_m2, rooms, floor, start/last price + ppm2, publish_date DMY, exposure_days, status) - YandexValuationResult — house + history_items[] + pagination metadata - Two-strategy history extraction: data-test container (if present), else text-chunk-around-DMY-date fallback with dedup by (date, area, floor). - _RE_PRICE_TOKEN uses negative lookahead (?!\s*за\s*м²) to exclude ppm2 tokens from total-price extraction. Tests: 16 unit tests against fixture HTML. All pass. Ruff clean.Merged via deep-code-reviewer — verdict APPROVE (with MINOR nits).
Two-tier fallback assessment: graceful. Primary CSS selector returns empty (Yandex doesn't expose
data-test*="HistoryItem"currently), fallback iterates overDD.MM.YYYYmatches with ±200/+100 char windows, dedups by(publish_date, area_m2, floor), caps at 30/page. Empty pages returnYandexValuationResultwith emptyhistory_items— orchestrator can paginate untillen()==0. No crashes on no-date body.Price regex correctness (
_RE_PRICE_TOKEN): confirmed."3 200 000 ₽"→ matches, parse_rub → 3_200_000 ✓"117 000 ₽ за м²"→ excluded by negative lookahead ✓"4,4 млн ₽"→ matches via(?:[.,]\d+)?\s*(?:млн)?branch ✓Anonymous fetch reliability: plain httpx via BaseScraper UA rotation + 4.0s
request_delay_sec+ tenacity retry on 403/429/5xx. HTTP errors returnNone(logged warning, no crash). Sufficient for anonymous endpoint; if Yandex adds rate-limit/captcha later, graceful degradation already in place.Minor nits (non-blocking, can be follow-up PR):
_RE_PRICE_TOKENlookahead(?!\s*за\s*м²)doesn't cover slash variant"₽/м²"— if Yandex ever switches notation, would mis-capture. Suggest:(?!\s*(?:за\s*м²|/\s*м²)).statusenum regex(В\s+продаже|Снят[оа])misses"Продано"— add|Продан[оа]?if surfaces in real HTML.RE_ITEM_FLOOR = (\d+)\s*этажalso matches"9 этажей"from house meta block — if chunk window pulls in meta, spurious floor. Tighten to(\d+)\s*этаж\b(?!ей|и)or scope to item containers.test_parse_history_item_fullassertion"В" in item.statusis too loose (any Cyrillic capital В passes) — strict-matchitem.status.startswith("В продаже").raw_payloadkeeps onlybody_len+items_count— re-parse from cached HTML not possible. Trade-off vs OOM is fine; document if needed.fetch_house_history— synchronousparse()is fully covered (16/16), but integration path untested. Snapshot test against real Yandex HTML recommended for Stage 7+.Cross-check: no DB writes, no migration, no API contract change. Pure module addition. No conflict with PR #466/#467 (different files). Vault decision
YandexRealtyScraper_v1_Implementation_Plan.mdStage 6 scope matches. Worker should addcode/modules/tradein/Yandex_Valuation_Scraper.mdentry.Next: verify
deploy-tradein.ymltriggers + CI 16/16 green on main.