diff --git a/tradein-mvp/backend/app/services/scrapers/yandex_newbuilding.py b/tradein-mvp/backend/app/services/scrapers/yandex_newbuilding.py index 3baf1565..ff54c385 100644 --- a/tradein-mvp/backend/app/services/scrapers/yandex_newbuilding.py +++ b/tradein-mvp/backend/app/services/scrapers/yandex_newbuilding.py @@ -93,7 +93,10 @@ RE_AREA_HA = re.compile(r"участке\s+([\d,]+)\s*га", re.IGNORECASE) RE_COMMISSION = re.compile( r"(?:введ[её]н|сдан)\s+в\s+эксплуатацию\s+в\s+(\w+)\s+(\d{4})", re.IGNORECASE ) -RE_RATING = re.compile(r"(\d[.,]\d)\s+из\s+5") +# Yandex drift (#974 follow-up): рейтинг рендерится без пробела перед «из» +# ("4.3из 5" / "4,5из 5", nbsp внутри) → \s* (а не \s+) перед «из», иначе rating=NULL +# при заполненных ratings_count/text_reviews (verified live 2026-06-15, ЖК Татлин/Рио). +RE_RATING = re.compile(r"(\d[.,]\d)\s*из\s+5") RE_RATINGS_COUNT = re.compile(r"(\d+)\s+оценок", re.IGNORECASE) RE_TEXT_REVIEWS = re.compile(r"Смотреть\s+все\s+(\d+)\s+отзыв", re.IGNORECASE) RE_CORPUS_COUNT_WORD = re.compile( diff --git a/tradein-mvp/backend/tests/test_yandex_newbuilding.py b/tradein-mvp/backend/tests/test_yandex_newbuilding.py index fa17eed0..789f938d 100644 --- a/tradein-mvp/backend/tests/test_yandex_newbuilding.py +++ b/tradein-mvp/backend/tests/test_yandex_newbuilding.py @@ -246,6 +246,32 @@ def test_parse_rating_comma_separator(): assert info.ratings_count == 200 +def test_parse_rating_no_space_before_iz(): + """Yandex drift (#974): '4.3из 5' / '4,5из\xa05' — без пробела перед «из», nbsp внутри. + + Регрессия 2026-06-15: live-страницы рендерили рейтинг как «4.3из 5» (без + разделителя), из-за чего \\s+ перед «из» не матчил → rating=NULL при + заполненных ratings_count. \\s* должен извлечь рейтинг в обеих формах. + """ + scraper = YandexNewbuildingScraper() + info = scraper.parse( + "
Отзывы4.3из 5Всего 1508 оценок
", + jk_slug="x", + jk_id="1", + source_url="http://x", + ) + assert info.rating == pytest.approx(4.3, abs=0.01) + assert info.ratings_count == 1508 + # nbsp-вариант (как в реальном body_text) + info2 = scraper.parse( + "
4,5из\xa05Всего 850 оценок
", + jk_slug="y", + jk_id="2", + source_url="http://y", + ) + assert info2.rating == pytest.approx(4.5, abs=0.01) + + # ── Developer link test ─────────────────────────────────────────────────────── @@ -284,9 +310,7 @@ def test_metro_stations_parsed(): source_url="https://realty.yandex.ru/ekaterinburg/kupit/novostrojka/tatlin-1592987/", ) assert len(info.metro_stations) >= 1 - uralskaya = next( - (s for s in info.metro_stations if "Уральская" in s.name), None - ) + uralskaya = next((s for s in info.metro_stations if "Уральская" in s.name), None) assert uralskaya is not None assert uralskaya.walk_min == 11 @@ -302,10 +326,7 @@ def test_parse_metro_direct(): def test_parse_metro_caps_at_five(): - text = ( - "Альфа 5 мин. Бета 6 мин. Гамма 7 мин. " - "Дельта 8 мин. Эпсилон 9 мин. Зета 10 мин." - ) + text = "Альфа 5 мин. Бета 6 мин. Гамма 7 мин. " "Дельта 8 мин. Эпсилон 9 мин. Зета 10 мин." stations = _parse_metro(text) assert len(stations) == 5 @@ -387,9 +408,7 @@ def test_developer_other_jk_dedup(): def test_developer_other_jk_capped_at_10(): """Only first 10 other JK entries are kept.""" - links = "\n".join( - f'ЖК {i}' for i in range(15) - ) + links = "\n".join(f'ЖК {i}' for i in range(15)) html = f"""
{links}
"""