fix(tradein/avito): консолидировать house_type normalizer + актуализировать docstring (#2009) (#2340)
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled

This commit is contained in:
lekss361 2026-07-03 22:47:05 +00:00
parent e3d26f0aae
commit e7f97f1355
3 changed files with 44 additions and 15 deletions

View file

@ -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":

View file

@ -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. Лоуэркейс-ключи не коллизят между вокабулярами:

View file

@ -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
# ── регистронезависимость ────────────────────────────────────────────────────