From 278dafba7602e1014a9b7d75e6aa378fff6fe07a Mon Sep 17 00:00:00 2001 From: bot-backend Date: Mon, 15 Jun 2026 21:29:43 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20yandex=20newbuilding=20rating?= =?UTF-8?q?=20drift=20=E2=80=94=20"4.3=D0=B8=D0=B7=205"=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=BF=D1=80=D0=BE=D0=B1=D0=B5=D0=BB=D0=B0=20(#974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Валидационный yandex_newbuilding_sweep (run 80) заполнял ratings_count / text_reviews_count / developer_name / lat-lon, но rating оставался NULL для всех ЖК. Live-диагностика (прод-браузер, ЖК Татлин/Рио 2026-06-15): рейтинг рендерится как «4.3из 5» / «4,5из 5» — БЕЗ пробела перед «из» (nbsp внутри «из 5»), а RE_RATING требовал \s+ перед «из» → match=None. - RE_RATING: (\d[.,]\d)\s+из\s+5 → (\d[.,]\d)\s*из\s+5 (пробел перед «из» опционален). \s уже матчит nbsp, поэтому «из\xa05» обрабатывался; ломал именно отсутствующий разделитель перед «из». - test_parse_rating_no_space_before_iz: регрессия на обе дрейфовые формы. Существующие rating-тесты («4,3 из 5» с пробелом) остаются зелёными (\s* ⊇ \s+). Refs #974 --- .../services/scrapers/yandex_newbuilding.py | 5 ++- .../backend/tests/test_yandex_newbuilding.py | 39 ++++++++++++++----- 2 files changed, 33 insertions(+), 11 deletions(-) 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}
"""