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"