Merge pull request 'fix(avito): de-glue house number from district in _clean_address (#1773 part)' (#1775) from fix/1773-avito-address-deglue into main
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 7s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m19s
Deploy Trade-In / build-backend (push) Has been cancelled
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 7s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m19s
Deploy Trade-In / build-backend (push) Has been cancelled
Reviewed-on: #1775
This commit is contained in:
commit
19f740f2e9
3 changed files with 149 additions and 0 deletions
|
|
@ -1886,6 +1886,50 @@ _NOT_ADDRESS_TAIL_RE = re.compile(
|
||||||
flags=re.I,
|
flags=re.I,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# #1773: Avito DOM иногда выдаёт номер дома СЛИПШИМСЯ с маркером района/нас.пункта
|
||||||
|
# без разделителя — «…Савкова, 29р-н Академический», «17Ар-н», «35/2р-н», «33с1р-н».
|
||||||
|
# Это ломает извлечение номера дома и матчинг «того же дома» (см. issue #1773).
|
||||||
|
# Де-слип вставляет «, » между токеном номера дома и следующим маркером.
|
||||||
|
#
|
||||||
|
# Группа 1 — токен номера дома: цифры + опц. кириллическая литера (А/Б/...) +
|
||||||
|
# опц. корпус/строение (/N, сN, кN, корпус N, стр N) — напр. 29, 29А, 35/2, 33с1, 44Б.
|
||||||
|
# Группа 2 — маркер района/нас.пункта, начинающийся СРАЗУ за номером без пробела.
|
||||||
|
#
|
||||||
|
# Тонкость со слипшейся литерой: в «17Ар-н» литера «А» и маркер «р-н» — соседние
|
||||||
|
# кириллические буквы без границы. Поэтому литеру дома матчим ТОЛЬКО когда сразу
|
||||||
|
# за ней идёт известный маркер (lookahead на _GLUE_MARKER) — иначе риск съесть
|
||||||
|
# первую букву обычного слова. Корпус «сN/кN» требует цифры, поэтому не путается
|
||||||
|
# с маркерами «с.» (село) / «к» — у тех нет цифры вплотную.
|
||||||
|
_GLUE_MARKER = (
|
||||||
|
r"(?:р-?н|мкр\.?|микрорайон|район|г\.|пос\.?|посёлок|поселок"
|
||||||
|
r"|снт|кв-л|квартал|д\.|с\.|пгт)"
|
||||||
|
)
|
||||||
|
_GLUE_HOUSE_TOKEN = (
|
||||||
|
r"\d+" # дом: цифры
|
||||||
|
# опц. одиночная литера (29А) — только если за ней сразу маркер
|
||||||
|
rf"(?:\s?[А-Яа-я](?={_GLUE_MARKER}))?"
|
||||||
|
r"(?:\s?(?:/\d+|с\d+|к\d+|корп\.?\s?\d+|стр\.?\s?\d+))?" # опц. корпус/строение
|
||||||
|
)
|
||||||
|
# Хвост `(?![а-яёa-z])` гарантирует, что маркер — отдельный токен, а не начало слова.
|
||||||
|
_DEGLUE_RE = re.compile(
|
||||||
|
rf"({_GLUE_HOUSE_TOKEN})({_GLUE_MARKER}(?![а-яёa-z]))",
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _deglue_house_marker(addr: str) -> str:
|
||||||
|
"""Вставить «, » между слипшимся номером дома и маркером района/нас.пункта.
|
||||||
|
|
||||||
|
«…Савкова, 29р-н Академический» -> «…Савкова, 29, р-н Академический»
|
||||||
|
«…, 17Ар-н Пионерский» -> «…, 17А, р-н Пионерский»
|
||||||
|
«…, 35/2р-н …» -> «…, 35/2, р-н …»
|
||||||
|
«…, 33с1р-н …» -> «…, 33с1, р-н …»
|
||||||
|
|
||||||
|
Идемпотентно: на уже-расклеенной строке («29, р-н») паттерн не срабатывает,
|
||||||
|
т.к. между номером и маркером уже есть запятая/пробел.
|
||||||
|
"""
|
||||||
|
return _DEGLUE_RE.sub(r"\1, \2", addr)
|
||||||
|
|
||||||
|
|
||||||
def _clean_address(raw: str | None) -> str | None:
|
def _clean_address(raw: str | None) -> str | None:
|
||||||
"""Strip Emotion CSS and post-address noise that Avito DOM leaks via .text().
|
"""Strip Emotion CSS and post-address noise that Avito DOM leaks via .text().
|
||||||
|
|
@ -1895,11 +1939,14 @@ def _clean_address(raw: str | None) -> str | None:
|
||||||
-> "ул. Токарей, 56к1"
|
-> "ул. Токарей, 56к1"
|
||||||
"ул. Малышева, 1.css-xxx{...}"
|
"ул. Малышева, 1.css-xxx{...}"
|
||||||
-> "ул. Малышева, 1"
|
-> "ул. Малышева, 1"
|
||||||
|
"ул. Евгения Савкова, 29р-н Академический"
|
||||||
|
-> "ул. Евгения Савкова, 29, р-н Академический"
|
||||||
"""
|
"""
|
||||||
if not raw:
|
if not raw:
|
||||||
return None
|
return None
|
||||||
cleaned = _CSS_NOISE_RE.sub("", raw)
|
cleaned = _CSS_NOISE_RE.sub("", raw)
|
||||||
cleaned = _NOT_ADDRESS_TAIL_RE.split(cleaned, maxsplit=1)[0]
|
cleaned = _NOT_ADDRESS_TAIL_RE.split(cleaned, maxsplit=1)[0]
|
||||||
|
cleaned = _deglue_house_marker(cleaned)
|
||||||
cleaned = cleaned.strip(" ,.\n\t")
|
cleaned = cleaned.strip(" ,.\n\t")
|
||||||
return cleaned or None
|
return cleaned or None
|
||||||
|
|
||||||
|
|
|
||||||
44
tradein-mvp/backend/data/sql/124_deglue_avito_addresses.sql
Normal file
44
tradein-mvp/backend/data/sql/124_deglue_avito_addresses.sql
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
-- 124_deglue_avito_addresses.sql
|
||||||
|
-- #1773 — де-слип номера дома и маркера района/нас.пункта в listings.address (Avito).
|
||||||
|
--
|
||||||
|
-- ПРОБЛЕМА (прод). Avito-адреса хранят номер дома СЛИПШИМСЯ с маркером района без
|
||||||
|
-- разделителя: «…ул. Евгения Савкова, 29р-н Академический» (номер «29» приклеен к
|
||||||
|
-- «р-н Академический»). Паттерны из данных: «29р-н», «17Ар-н», «35/2р-н», «33с1р-н»,
|
||||||
|
-- «44Бр-н». Из-за склейки ломается извлечение номера дома и матчинг «того же дома»
|
||||||
|
-- (41 несматченный avito-листинг по одной улице).
|
||||||
|
--
|
||||||
|
-- ЧТО ДЕЛАЕТ. Вставляет «, » между токеном номера дома и следующим за ним маркером
|
||||||
|
-- района/нас.пункта без пробела. На ingest то же чинит avito.py::_clean_address;
|
||||||
|
-- этот backfill чистит УЖЕ сохранённые строки на деплое.
|
||||||
|
--
|
||||||
|
-- ПОЧЕМУ POSIX-ERE без lookahead. Postgres regexp_replace не поддерживает (?=…),
|
||||||
|
-- поэтому маркер фиксируется хвостовой группой \3 = «не кириллица/не цифра или конец»
|
||||||
|
-- (гарантирует, что маркер — целый токен, а не начало слова). Литера дома [А-Яа-яЁё]?
|
||||||
|
-- жадная, но движок обязан затем сматчить целый маркер вплотную — иначе совпадения нет.
|
||||||
|
-- Корпус «сN/кN» требует цифру, поэтому не путается с маркерами «с.» (село) / «к».
|
||||||
|
--
|
||||||
|
-- Idempotent: regexp_replace на уже-расклеенной строке («29, р-н») — no-op
|
||||||
|
-- (между номером и маркером уже стоит «, »); WHERE отбирает только ещё-слипшиеся
|
||||||
|
-- строки. Под gate _schema_migrations (ровно один раз).
|
||||||
|
-- Apply after: 123_avito_newbuilding_sweep_schedule.sql
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE listings
|
||||||
|
SET address = regexp_replace(
|
||||||
|
address,
|
||||||
|
-- \1 = токен номера дома (цифры + опц. литера + опц. корпус/строение)
|
||||||
|
-- \2 = маркер района/нас.пункта
|
||||||
|
-- \3 = хвостовая граница (не буква/цифра или конец строки)
|
||||||
|
'([0-9]+[А-Яа-яЁё]?(/[0-9]+|с[0-9]+|к[0-9]+|корп\.? ?[0-9]+|стр\.? ?[0-9]+)?)'
|
||||||
|
|| '(р-?н|мкр\.?|микрорайон|район|г\.|пос\.?|посёлок|поселок|снт|кв-л|квартал|д\.|с\.|пгт)'
|
||||||
|
|| '([^А-Яа-яЁё0-9]|$)',
|
||||||
|
'\1, \3\4',
|
||||||
|
'g'
|
||||||
|
)
|
||||||
|
WHERE source = 'avito'
|
||||||
|
AND address ~ ('([0-9]+[А-Яа-яЁё]?(/[0-9]+|с[0-9]+|к[0-9]+|корп\.? ?[0-9]+|стр\.? ?[0-9]+)?)'
|
||||||
|
|| '(р-?н|мкр\.?|микрорайон|район|г\.|пос\.?|посёлок|поселок|снт|кв-л|квартал|д\.|с\.|пгт)'
|
||||||
|
|| '([^А-Яа-яЁё0-9]|$)');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from app.services.scrapers.avito import _clean_address
|
from app.services.scrapers.avito import _clean_address
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -22,3 +24,59 @@ def test_clean_handles_none():
|
||||||
|
|
||||||
def test_clean_strips_trailing_punctuation():
|
def test_clean_strips_trailing_punctuation():
|
||||||
assert _clean_address("ул. Ленина, 5,") == "ул. Ленина, 5"
|
assert _clean_address("ул. Ленина, 5,") == "ул. Ленина, 5"
|
||||||
|
|
||||||
|
|
||||||
|
# ── #1773: де-слип номера дома и маркера района/нас.пункта ────────────────────
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"raw,expected",
|
||||||
|
[
|
||||||
|
# реальный кейс с прода
|
||||||
|
(
|
||||||
|
"Свердловская обл., Екатеринбург, ул. Евгения Савкова, 29р-н Академический",
|
||||||
|
"Свердловская обл., Екатеринбург, ул. Евгения Савкова, 29, р-н Академический",
|
||||||
|
),
|
||||||
|
# литера дома слиплась с маркером
|
||||||
|
("ул. X, 17Ар-н Пионерский", "ул. X, 17А, р-н Пионерский"),
|
||||||
|
("ул. X, 44Бр-н Запад", "ул. X, 44Б, р-н Запад"),
|
||||||
|
# корпус через дробь
|
||||||
|
("ул. X, 35/2р-н Юг", "ул. X, 35/2, р-н Юг"),
|
||||||
|
# корпус через «сN»
|
||||||
|
("ул. X, 33с1р-н Центр", "ул. X, 33с1, р-н Центр"),
|
||||||
|
# прочие маркеры
|
||||||
|
("ул. X, 12мкр Солнечный", "ул. X, 12, мкр Солнечный"),
|
||||||
|
("ул. X, 5район Кировский", "ул. X, 5, район Кировский"),
|
||||||
|
("ул. X, 100к2р-н Чкаловский", "ул. X, 100к2, р-н Чкаловский"),
|
||||||
|
# маркеры населённых пунктов
|
||||||
|
("ул. X, 12с. Кашино", "ул. X, 12, с. Кашино"),
|
||||||
|
("ул. X, 10д. Малое", "ул. X, 10, д. Малое"),
|
||||||
|
("ул. X, 25г. Берёзовский", "ул. X, 25, г. Берёзовский"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_deglue_house_marker(raw, expected):
|
||||||
|
assert _clean_address(raw) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"raw",
|
||||||
|
[
|
||||||
|
# уже расклеено — идемпотентность
|
||||||
|
"ул. Евгения Савкова, 29, р-н Академический",
|
||||||
|
# нормальные адреса без склейки — разделять НЕ надо
|
||||||
|
"ул. Малышева, 1",
|
||||||
|
"ул. Токарей, 56к1",
|
||||||
|
"ул. Ленина, 5",
|
||||||
|
"ул. Серова, 45",
|
||||||
|
"г. Екатеринбург, ул. Ленина, 10",
|
||||||
|
"снт Дружба, уч. 12",
|
||||||
|
"ул. Декабристов, 16",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_deglue_negative_no_change(raw):
|
||||||
|
# для этих строк де-слип не должен ничего менять (кроме штатного strip пунктуации)
|
||||||
|
assert _clean_address(raw) == raw.strip(" ,.\n\t")
|
||||||
|
|
||||||
|
|
||||||
|
def test_deglue_idempotent():
|
||||||
|
once = _clean_address("ул. Евгения Савкова, 29р-н Академический")
|
||||||
|
twice = _clean_address(once)
|
||||||
|
assert once == twice == "ул. Евгения Савкова, 29, р-н Академический"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue