From 22a7a3f0861aa9e38a004c5273a93d8a9cbfa252 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 17 Jun 2026 22:24:13 +0300 Subject: [PATCH] =?UTF-8?q?fix(avito):=20classify=20novostroyki=20by=20ite?= =?UTF-8?q?m-development-name=20marker;=20map=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=83=D1=81=D1=82=D1=83=D0=BF=D0=BA=D0=B0=20sale=5Ftype?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/app/services/scrapers/avito.py | 22 ++++-------- .../app/services/scrapers/avito_detail.py | 1 + .../backend/tests/test_avito_detail_parse.py | 19 +++++++++++ .../backend/tests/test_avito_serp_date.py | 34 +++++++++++++++++++ 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/tradein-mvp/backend/app/services/scrapers/avito.py b/tradein-mvp/backend/app/services/scrapers/avito.py index b500752f..38255dda 100644 --- a/tradein-mvp/backend/app/services/scrapers/avito.py +++ b/tradein-mvp/backend/app/services/scrapers/avito.py @@ -74,15 +74,7 @@ _UNIT_DAYS: dict[str, int] = { # поэтому отдельная ветка с неявным n=1. Юниты в винительном падеже («минуту», # «неделю») плюс именительный («час», «день»). _REL_DATE_SINGULAR_RE = re.compile( - r"(?P" - r"секунду|" - r"минуту|" - r"час|" - r"день|" - r"неделю|" - r"месяц|" - r"год" - r")\s+назад", + r"(?P" r"секунду|" r"минуту|" r"час|" r"день|" r"неделю|" r"месяц|" r"год" r")\s+назад", flags=re.I, ) @@ -751,12 +743,12 @@ class AvitoScraper(BaseScraper): house_ext_id = parts[-1] house_url = urljoin("https://www.avito.ru", h_href) - # listing_segment по item URL pattern - listing_segment: str | None = None - if "/kvartiry/" in href: - listing_segment = "vtorichka" - elif "/novostroyki/" in href: - listing_segment = "novostroyki" + # listing_segment по DOM-маркеру карточки: новостройки несут + # data-marker="item-development-name" (название ЖК/застройщика), + # вторичка — нет. URL-паттерн ненадёжен: каждый avito-URL квартиры + # содержит /kvartiry/ (вкл. новостройки) → ветка всегда vtorichka. + dev_name = card.css_first('[data-marker="item-development-name"]') + listing_segment = "novostroyki" if dev_name is not None else "vtorichka" return ScrapedLot( source="avito", diff --git a/tradein-mvp/backend/app/services/scrapers/avito_detail.py b/tradein-mvp/backend/app/services/scrapers/avito_detail.py index c0e246a3..f7f52ddd 100644 --- a/tradein-mvp/backend/app/services/scrapers/avito_detail.py +++ b/tradein-mvp/backend/app/services/scrapers/avito_detail.py @@ -152,6 +152,7 @@ _SALE_TYPE_MAP: dict[str, str] = { "свободная": "free", "альтернатива": "alternative", "аукцион": "auction", + "переуступка": "assignment", } _HOUSE_TYPE_MAP: dict[str, str] = { diff --git a/tradein-mvp/backend/tests/test_avito_detail_parse.py b/tradein-mvp/backend/tests/test_avito_detail_parse.py index e9e419fb..7a2f6b0e 100644 --- a/tradein-mvp/backend/tests/test_avito_detail_parse.py +++ b/tradein-mvp/backend/tests/test_avito_detail_parse.py @@ -78,6 +78,25 @@ def test_minimal_parse() -> None: assert result.views_today == 12 +def test_sale_type_assignment_pereustupka() -> None: + """«Способ продажи: переуступка» → sale_type='assignment' (ДДУ переуступка, + сигнал новостройки). До фикса map не содержал ключ → None.""" + html = """ + +
№ 8043003287
+ 9 500 000 ₽ +
+
    +
  • Количество комнат: 2
  • +
  • Способ продажи: переуступка
  • +
+
+ + """ + result = parse_detail_html(html, "https://www.avito.ru/test_8043003287") + assert result.sale_type == "assignment" + + def test_metro_extraction() -> None: result = parse_detail_html(MINIMAL_HTML, SOURCE_URL) # "Чкаловская 11-15 мин пешком" → metro_stations diff --git a/tradein-mvp/backend/tests/test_avito_serp_date.py b/tradein-mvp/backend/tests/test_avito_serp_date.py index 870541d4..f6fa868f 100644 --- a/tradein-mvp/backend/tests/test_avito_serp_date.py +++ b/tradein-mvp/backend/tests/test_avito_serp_date.py @@ -60,3 +60,37 @@ def test_lazy_card_date_comes_from_json_not_dom() -> None: lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/x") lazy = next(lot for lot in lots if "8043936560" in (lot.source_url or "")) assert lazy.listing_date == _expected(_TS_LAZY) + + +# ── listing_segment по DOM-маркеру item-development-name ─────────────────────── +# Каждый avito-URL квартиры содержит /kvartiry/ (вкл. новостройки), поэтому +# сегмент определяется не по URL, а по наличию data-marker="item-development-name" +# (название ЖК/застройщика рендерится только у карточек новостроек). + + +def _serp_card(item_id: str, *, with_dev_name: bool) -> str: + dev = '
ЖК «Федерация»
' if with_dev_name else "" + return f""" +
+ + 2-к. квартира, 50 м², 5/20 эт. + + {dev} +
+ """ + + +def test_segment_novostroyki_when_development_name_present() -> None: + """Карточка с data-marker='item-development-name' → listing_segment='novostroyki'.""" + html = f"{_serp_card('8043003287', with_dev_name=True)}" + lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry") + assert len(lots) == 1 + assert lots[0].listing_segment == "novostroyki" + + +def test_segment_vtorichka_when_no_development_name() -> None: + """Карточка без маркера ЖК → listing_segment='vtorichka' (даже с /kvartiry/ в URL).""" + html = f"{_serp_card('8043936560', with_dev_name=False)}" + lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry") + assert len(lots) == 1 + assert lots[0].listing_segment == "vtorichka" -- 2.45.3