diff --git a/tradein-mvp/backend/app/services/scrapers/yandex_helpers.py b/tradein-mvp/backend/app/services/scrapers/yandex_helpers.py index 8b945943..573f6533 100644 --- a/tradein-mvp/backend/app/services/scrapers/yandex_helpers.py +++ b/tradein-mvp/backend/app/services/scrapers/yandex_helpers.py @@ -281,13 +281,17 @@ def parse_rub(text: str | None) -> int | None: m = RE_RUB_MLN.search(text) if m: try: - return int(float(m.group(1).replace(",", ".").replace(" ", "")) * 1_000_000) + # `\s` strips also NBSP (\xa0), thin space, и т.д. — Yandex SERP + # форматирует «9\xa0800\xa0000» через NBSP, не regular space. + cleaned = re.sub(r"\s", "", m.group(1)).replace(",", ".") + return int(float(cleaned) * 1_000_000) except (ValueError, TypeError): pass m = RE_RUB_RAW.search(text) if m: - cleaned = m.group(1).replace(" ", "") + # Same NBSP issue: `.replace(" ", "")` оставляет \xa0 → int() raises. + cleaned = re.sub(r"\s", "", m.group(1)) try: return int(cleaned) except ValueError: diff --git a/tradein-mvp/backend/app/services/scrapers/yandex_realty.py b/tradein-mvp/backend/app/services/scrapers/yandex_realty.py index f991dd81..203e2ce2 100644 --- a/tradein-mvp/backend/app/services/scrapers/yandex_realty.py +++ b/tradein-mvp/backend/app/services/scrapers/yandex_realty.py @@ -13,6 +13,7 @@ Typical: 23 [data-test="OffersSerpItem"] cards per page. from __future__ import annotations import logging +import re from datetime import date # noqa: F401 (used in type hints / future helpers) from typing import Any @@ -40,6 +41,11 @@ PHOTO_DOMAIN = "avatars.mds.yandex" PHOTO_SIZE_FROM = "app_snippet_small" PHOTO_SIZE_TO = "main" +# Address container wraps street link + house number text node. +# Class is hashed but always starts with "AddressWithGeoLinks__addressContainer". +ADDRESS_CONTAINER_SELECTOR = '[class*="AddressWithGeoLinks__addressContainer"]' +_RE_WS = re.compile(r"\s+") + class YandexRealtyScraper(BaseScraper): name = "yandex" @@ -158,9 +164,10 @@ class YandexRealtyScraper(BaseScraper): bargain = "торг" in text.lower() listing_date = parse_listing_date(text) - # Address — street aggregator link - street_link = card.css_first('a[href*="/kupit/kvartira/st-"]') - address = street_link.text(strip=True) if street_link else None + # Address — prefer the full address container (street + house number); + # fall back to the street aggregator link (street name only) if absent. + # See _extract_address for details. + address = self._extract_address(card) # Photos photo_urls = self._extract_photos(card) @@ -211,6 +218,38 @@ class YandexRealtyScraper(BaseScraper): logger.exception("yandex card parse failed (page=%d)", page) return None + @staticmethod + def _extract_address(card: Node) -> str | None: + """Extract full address (street + house number) from a SERP card. + + Yandex renders the address inside a container like: +