diff --git a/tradein-mvp/backend/app/services/scrapers/avito_detail.py b/tradein-mvp/backend/app/services/scrapers/avito_detail.py index b6fcffa6..19172519 100644 --- a/tradein-mvp/backend/app/services/scrapers/avito_detail.py +++ b/tradein-mvp/backend/app/services/scrapers/avito_detail.py @@ -6,7 +6,9 @@ bathroom_type, windows_view, repair_state, sale_type, mortgage_available) - Location (lat, lon, avito_location_id, metro_stations[], address_full) - House params (house_type, total_floors_house, lifts, concierge, closed_yard, - house_catalog_url) — собираются но НЕ сохраняются в БД (Stage 2c) + house_catalog_url) — house_type + house_catalog_url сохраняются в listings + (#2009); total_floors_house/lifts/concierge/closed_yard остаются canonical + из Houses Catalog (Stage 2c, avito_houses.py → houses table) - Description - Domoteka 5 полей (owners_count, owners_at_least, last_owner_change_date, encumbrances_clean, registry_match) @@ -42,6 +44,7 @@ from app.services.scrapers.avito_exceptions import ( AvitoRateLimitedError, ) from app.services.scrapers.avito_shared import RUS_MONTHS +from app.services.scrapers.house_type_normalizer import normalize_house_type from app.services.scrapers.repair_state_normalizer import infer_repair_state_from_text if TYPE_CHECKING: @@ -173,14 +176,10 @@ _SALE_TYPE_MAP: dict[str, str] = { "переуступка": "assignment", } -_HOUSE_TYPE_MAP: dict[str, str] = { - "монолитный": "monolith", - "панельный": "panel", - "кирпичный": "brick", - "монолитно-кирпичный": "monolith_brick", - "блочный": "block", - "деревянный": "wood", -} +# house_type: канон panel/brick/monolith/monolith_brick/block/wood — через shared +# normalize_house_type() (#2009, консолидация с #2007/#2008; см. +# house_type_normalizer.py — русские лейблы avito добавлены туда же вокабуляром, +# НЕ дублируются локально). # ── DetailEnrichment dataclass ──────────────────────────────────────────────── @@ -189,8 +188,9 @@ class DetailEnrichment: """Результат парсинга detail page Avito. Содержит 30+ полей из страницы объявления. - save_detail_enrichment() пишет подмножество в listings (house-level поля пропускаются - — они canonical из Houses Catalog Stage 2c). + save_detail_enrichment() пишет подмножество в listings; house_type + house_catalog_url + сохраняются (#2009), остальные house-level поля (total_floors_house, elevators, + concierge, closed_yard) пропускаются — они canonical из Houses Catalog Stage 2c. """ # Identity @@ -219,7 +219,8 @@ class DetailEnrichment: sale_type: str | None = None mortgage_available: bool | None = None - # House params (собираем, но НЕ сохраняем в listings — Stage 2c) + # House params — house_type/house_catalog_url сохраняются в listings (#2009); + # остальные пропускаются, canonical источник — Houses Catalog (Stage 2c) house_type: str | None = None total_floors_house: int | None = None passenger_elevators: int | None = None @@ -683,7 +684,7 @@ def parse_detail_html(html: str, source_url: str) -> DetailEnrichment: if "возможна ипотека" in val.lower(): mortgage_available = True elif field_name == "house_type": - house_type = _map_lower(val, _HOUSE_TYPE_MAP) + house_type = normalize_house_type(val) elif field_name == "total_floors_house": total_floors_house = _try_int(val) elif field_name == "passenger_elevators": diff --git a/tradein-mvp/backend/app/services/scrapers/house_type_normalizer.py b/tradein-mvp/backend/app/services/scrapers/house_type_normalizer.py index f9ad280d..9ac346dc 100644 --- a/tradein-mvp/backend/app/services/scrapers/house_type_normalizer.py +++ b/tradein-mvp/backend/app/services/scrapers/house_type_normalizer.py @@ -17,6 +17,8 @@ house_type != target штрафуется. Yandex SERP отдаёт SCREAMING-з - yandex SERP: building.buildingType — SCREAMING_SNAKE (MONOLIT, MONOLIT_BRICK, ...) - cian SERP: building.materialType — camelCase (monolith, monolithBrick, gasSilicateBlock, ...) — переиспользуется в #2008 (cian.py:815). +- avito detail page: «Тип дома» — русские лейблы (монолитный, панельный, ...) — + переиспользуется в #2009 (avito_detail.py) вместо локального дубля-мэппинга. """ from __future__ import annotations @@ -52,6 +54,13 @@ _RAW_TO_CANON: dict[str, str] = { "aerocreteBlock": "block", "foamConcreteBlock": "block", "stalin": "brick", # «сталинка» — кирпич + # ── avito detail page («Тип дома», русские лейблы) — reuse в #2009 ─────── + "монолитный": "monolith", + "панельный": "panel", + "кирпичный": "brick", + "монолитно-кирпичный": "monolith_brick", + "блочный": "block", + "деревянный": "wood", } # Регистронезависимый lookup. Лоуэркейс-ключи не коллизят между вокабулярами: diff --git a/tradein-mvp/backend/tests/test_house_type_normalizer.py b/tradein-mvp/backend/tests/test_house_type_normalizer.py index cb26f914..a52a2029 100644 --- a/tradein-mvp/backend/tests/test_house_type_normalizer.py +++ b/tradein-mvp/backend/tests/test_house_type_normalizer.py @@ -1,7 +1,8 @@ -"""Unit-тесты для house_type_normalizer (#2007). +"""Unit-тесты для house_type_normalizer (#2007, #2009). Покрывают: -- normalize_house_type(): yandex SCREAMING + cian camelCase вокабуляры → канон, +- normalize_house_type(): yandex SCREAMING + cian camelCase + avito русские + лейблы вокабуляры → канон, - идемпотентность (normalize(normalize(x)) == normalize(x)), - None / '' / whitespace / 'other' / unknown → None. """ @@ -53,6 +54,24 @@ def test_cian_camelcase_vocab(raw: str, expected: str) -> None: assert normalize_house_type(raw) == expected +# ── avito detail page («Тип дома», русские лейблы) — reuse #2009 ──────────── + + +@pytest.mark.parametrize( + ("raw", "expected"), + [ + ("монолитный", "monolith"), + ("панельный", "panel"), + ("кирпичный", "brick"), + ("монолитно-кирпичный", "monolith_brick"), + ("блочный", "block"), + ("деревянный", "wood"), + ], +) +def test_avito_russian_vocab(raw: str, expected: str) -> None: + assert normalize_house_type(raw) == expected + + # ── регистронезависимость ────────────────────────────────────────────────────