fix(tradein): yandex address-enrich — city-agnostic title regex (спутники ЕКБ) (#875)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 29s
Deploy Trade-In / build-backend (push) Successful in 41s
Deploy Trade-In / deploy (push) Successful in 36s
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 29s
Deploy Trade-In / build-backend (push) Successful in 41s
Deploy Trade-In / deploy (push) Successful in 36s
Co-authored-by: bot-backend <bot-backend@gendsgn.local> Co-committed-by: bot-backend <bot-backend@gendsgn.local>
This commit is contained in:
parent
960905d47f
commit
d9b8047a7d
2 changed files with 76 additions and 22 deletions
|
|
@ -26,15 +26,20 @@ from sqlalchemy.orm import Session
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Extract 'Екатеринбург, <full address>' from <title> — matches:
|
# Extract '<City>, <full address>' from <title> — matches any city, not just Ekaterinburg.
|
||||||
|
# Handles EKB metro-area satellite towns: Верхняя Пышма, Берёзовский, Среднеуральск, Арамиль, etc.
|
||||||
|
# Examples:
|
||||||
# «Продажа квартиры — Екатеринбург, улица Горького, 36 — id 7654321 на Яндекс.Недвижимости»
|
# «Продажа квартиры — Екатеринбург, улица Горького, 36 — id 7654321 на Яндекс.Недвижимости»
|
||||||
# Group 1 = «Екатеринбург, улица Горького, 36» (stripped of trailing punctuation/spaces)
|
# «Купить квартиру … — ЖК «Дуэт», Верхняя Пышма, улица Мальцева, 14 — id 75332…»
|
||||||
|
# «Купить квартиру … — ЖК «Уют-Сити», Берёзовский, Александровский проспект, 3 — id 3988…»
|
||||||
|
# Group 1 = «<City>, <street/house>» (stripped of trailing punctuation/spaces)
|
||||||
|
# The optional ЖК-prefix (?:[^—,]*?,\s*) consumes «ЖК «Дуэт», » before the city name.
|
||||||
|
# City capture starts with [А-ЯЁ] to ensure the prefix never swallows the city itself.
|
||||||
_RE_TITLE_ADDRESS = re.compile(
|
_RE_TITLE_ADDRESS = re.compile(
|
||||||
r"<title>[^<]*?—\s*"
|
r"<title>[^<]*?—\s*"
|
||||||
r"(?:[^—,]*?,\s*)?" # optional ЖК/жилой комплекс prefix
|
r"(?:(?![А-ЯЁ][а-яё])[^—,]*?,\s*)?" # optional ЖК-prefix — fires only when NOT a city name
|
||||||
r"(Екатеринбург,\s*[^<—]+?)"
|
r"([А-ЯЁ][^—<]+?,\s*[^<—]+?)" # <City>, <street/house> — any city (uppercase Cyrillic start)
|
||||||
r"\s*—\s*id\s*\d+[^<]*</title>", # allow trailing text after id NNN (e.g. 'на Яндекс...')
|
r"\s*—\s*id\s*\d+[^<]*</title>", # allow trailing text after id NNN (e.g. 'на Яндекс...')
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Listings without a house number: address contains no digit after comma-space
|
# Listings without a house number: address contains no digit after comma-space
|
||||||
|
|
@ -53,7 +58,11 @@ class YandexAddressBackfillResult:
|
||||||
|
|
||||||
|
|
||||||
def _extract_address_from_title(html: str) -> str | None:
|
def _extract_address_from_title(html: str) -> str | None:
|
||||||
"""Extract full address from Yandex offer detail page <title>."""
|
"""Extract full address from Yandex offer detail page <title>.
|
||||||
|
|
||||||
|
City-agnostic: works for EKB and satellite towns (Верхняя Пышма, Берёзовский, etc.).
|
||||||
|
Returns «<City>, <street>, <house>» or None if the title format is not recognised.
|
||||||
|
"""
|
||||||
html_clean = html.replace("\xa0", " ")
|
html_clean = html.replace("\xa0", " ")
|
||||||
m = _RE_TITLE_ADDRESS.search(html_clean)
|
m = _RE_TITLE_ADDRESS.search(html_clean)
|
||||||
if not m:
|
if not m:
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,67 @@ def test_extract_address_with_zhk_prefix() -> None:
|
||||||
assert "Екатеринбург" in addr
|
assert "Екатеринбург" in addr
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# City-agnostic tests — satellite towns (T10 live-bug fix)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_address_verkhnaya_pyshma_with_zhk() -> None:
|
||||||
|
"""Real title from sweep #28: ЖК «Дуэт», Верхняя Пышма — was returning None before fix."""
|
||||||
|
html = (
|
||||||
|
"<title>Купить квартиру 102.4 м²"
|
||||||
|
" — ЖК «Дуэт», Верхняя Пышма, улица Мальцева, 14"
|
||||||
|
" — id 7533242317391171630</title>"
|
||||||
|
)
|
||||||
|
addr = _extract_address_from_title(html)
|
||||||
|
assert addr is not None, "Верхняя Пышма title should produce an address"
|
||||||
|
assert _RE_HAS_HOUSE_NUMBER.search(addr), f"Should contain house number: {addr!r}"
|
||||||
|
assert "Верхняя Пышма" in addr
|
||||||
|
assert "Мальцева" in addr
|
||||||
|
assert "14" in addr
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_address_beryozovsky_with_zhk() -> None:
|
||||||
|
"""Real title from sweep #28: ЖК «Уют-Сити», Берёзовский — was returning None before fix."""
|
||||||
|
html = (
|
||||||
|
"<title>Купить квартиру 139.2 м²"
|
||||||
|
" — ЖК «Уют-Сити», Берёзовский, Александровский проспект, 3"
|
||||||
|
" — id 3988368535316036933</title>"
|
||||||
|
)
|
||||||
|
addr = _extract_address_from_title(html)
|
||||||
|
assert addr is not None, "Берёзовский title should produce an address"
|
||||||
|
assert _RE_HAS_HOUSE_NUMBER.search(addr), f"Should contain house number: {addr!r}"
|
||||||
|
assert "Берёзовский" in addr
|
||||||
|
assert "Александровский" in addr
|
||||||
|
assert "3" in addr
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_address_ekb_no_zhk_still_matches() -> None:
|
||||||
|
"""EKB variant from docstring (no ЖК prefix) still matches after city-agnostic change."""
|
||||||
|
html = "<title>Продажа квартиры — Екатеринбург, улица Горького, 36" " — id 7654321</title>"
|
||||||
|
addr = _extract_address_from_title(html)
|
||||||
|
assert addr is not None, "EKB no-ЖК title should still match"
|
||||||
|
assert _RE_HAS_HOUSE_NUMBER.search(addr), f"Should contain house number: {addr!r}"
|
||||||
|
assert "Екатеринбург" in addr
|
||||||
|
assert "Горького" in addr
|
||||||
|
assert "36" in addr
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_address_no_house_number_guard() -> None:
|
||||||
|
"""Street-only title (no house number): extractor may return value, but guard rejects it.
|
||||||
|
|
||||||
|
_RE_HAS_HOUSE_NUMBER must NOT match → backfill skips the listing to avoid overwriting
|
||||||
|
with another street-only address.
|
||||||
|
"""
|
||||||
|
html = "<title>Продажа квартиры — Екатеринбург, улица Мира — id 999888</title>"
|
||||||
|
addr = _extract_address_from_title(html)
|
||||||
|
# If the regex matches at all, the extracted value must not contain a house number.
|
||||||
|
if addr is not None:
|
||||||
|
assert not _RE_HAS_HOUSE_NUMBER.search(
|
||||||
|
addr
|
||||||
|
), f"Street-only addr must fail house-number guard: {addr!r}"
|
||||||
|
|
||||||
|
|
||||||
def test_extract_address_nbsp_replaced() -> None:
|
def test_extract_address_nbsp_replaced() -> None:
|
||||||
"""\\xa0 (non-breaking space) in title should be normalised before regex match."""
|
"""\\xa0 (non-breaking space) in title should be normalised before regex match."""
|
||||||
html = (
|
html = (
|
||||||
|
|
@ -87,22 +148,6 @@ def test_extract_address_title_without_id_returns_none() -> None:
|
||||||
assert addr is None
|
assert addr is None
|
||||||
|
|
||||||
|
|
||||||
def test_extract_address_street_only_still_extracted() -> None:
|
|
||||||
"""Title with street-only (no house number) is extracted but fails the house-number check.
|
|
||||||
|
|
||||||
The extractor itself does not gate on house number presence — the caller
|
|
||||||
(backfill_yandex_addresses) checks _RE_HAS_HOUSE_NUMBER separately and skips.
|
|
||||||
"""
|
|
||||||
html = "<title>Продажа квартиры — Екатеринбург, улица Горького — id 7654321</title>"
|
|
||||||
addr = _extract_address_from_title(html)
|
|
||||||
# May or may not match depending on whether «улица Горького» satisfies the regex —
|
|
||||||
# the key assertion is that IF it does, it does NOT contain a house number.
|
|
||||||
if addr is not None:
|
|
||||||
assert not _RE_HAS_HOUSE_NUMBER.search(
|
|
||||||
addr
|
|
||||||
), "Street-only extracted address should NOT contain a house number"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Task-level smoke tests (no real DB, no real HTTP)
|
# Task-level smoke tests (no real DB, no real HTTP)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue