fix(tradein): Avito listing_date extraction (3 формата) #601

Merged
Light1YT merged 1 commit from fix/avito-listing-date-extraction into main 2026-05-27 12:55:28 +00:00
2 changed files with 117 additions and 8 deletions

View file

@ -51,19 +51,80 @@ _UNIT_DAYS: dict[str, int] = {
"год": 365,
}
# Russian month names → number. Avito показывает абсолютные даты как «18 мая».
_RU_MONTHS: dict[str, int] = {
"январ": 1, "феврал": 2, "март": 3, "апрел": 4,
"ма": 5, "июн": 6, "июл": 7, "август": 8,
"сентябр": 9, "октябр": 10, "ноябр": 11, "декабр": 12,
}
# «18 мая», «5 июня в 12:30» — day + month (year inferred current).
_ABS_DATE_RE = re.compile(
r"(?P<day>\d{1,2})\s+(?P<month>январ\w*|феврал\w*|март\w*|апрел\w*|"
r"ма[яй]|июн\w*|июл\w*|август\w*|сентябр\w*|октябр\w*|ноябр\w*|декабр\w*)",
flags=re.I,
)
def _parse_relative_date(s: str | None) -> date | None:
"""Avito показывает '5 дней назад' / 'час назад'. Конвертирует в date."""
"""Avito показывает дату публикации в трёх форматах:
* '5 дней назад' / 'час назад' / 'неделю назад' relative numeric
* 'сегодня в 12:30' / 'вчера в 18:45' / 'позавчера' relative keyword
* '18 мая' / '5 июня в 12:30' absolute day + month (year inferred)
PR M-followup 2026-05-27: расширено покрытие раньше только relative numeric
matched (regex `\\d+ unit назад`), 100% Avito listings заходило с listing_date=None
т.к. Avito SERP для свежих cards использует keyword/absolute форматы. См.
тесты для каждого case.
"""
if not s:
return None
text_lower = s.lower()
# Keyword shortcuts (no \d+).
if "позавчера" in text_lower:
return date.today() - timedelta(days=2)
if "вчера" in text_lower:
return date.today() - timedelta(days=1)
if "сегодня" in text_lower or "только что" in text_lower:
return date.today()
# Relative numeric («5 дней назад»).
m = _REL_DATE_RE.search(s)
if not m:
return None
n = int(m["n"])
unit_raw = m["unit"].lower()
for prefix, days in _UNIT_DAYS.items():
if unit_raw.startswith(prefix):
return date.today() - timedelta(days=n * days)
if m:
n = int(m["n"])
unit_raw = m["unit"].lower()
for prefix, days in _UNIT_DAYS.items():
if unit_raw.startswith(prefix):
return date.today() - timedelta(days=n * days)
# Absolute («18 мая», «5 июня в 12:30»). Year — current; если месяц
# в будущем относительно сегодня (например сейчас декабрь, увидели
# «5 января») — откатываем к прошлому году.
m = _ABS_DATE_RE.search(s)
if m:
day = int(m["day"])
month_raw = m["month"].lower()
month: int | None = None
for prefix, num in _RU_MONTHS.items():
if month_raw.startswith(prefix):
month = num
break
if month is not None and 1 <= day <= 31:
today = date.today()
year = today.year
try:
parsed = date(year, month, day)
except ValueError:
return None
# Если parsed в будущем больше чем на 30 дней — Avito показывает
# прошлогоднюю дату («декабрь» в феврале значит прошлый декабрь).
if (parsed - today).days > 30:
try:
parsed = date(year - 1, month, day)
except ValueError:
return None
return parsed
return None

View file

@ -19,3 +19,51 @@ def test_none_passthrough():
assert _parse_relative_date(None) is None
assert _parse_relative_date("") is None
assert _parse_relative_date("random text") is None
# Keyword shortcuts — без \d+ (Avito SERP формат для свежих cards)
def test_today_keyword():
assert _parse_relative_date("сегодня в 12:30") == date.today()
def test_yesterday_keyword():
assert _parse_relative_date("вчера в 18:45") == date.today() - timedelta(days=1)
def test_day_before_yesterday():
assert _parse_relative_date("позавчера") == date.today() - timedelta(days=2)
def test_just_now():
assert _parse_relative_date("только что") == date.today()
# Absolute dates — «18 мая», «5 июня в 12:30»
def test_absolute_date_recent():
today = date.today()
# Take a day-month combo near current (last month) — gradening для CI stability
if today.month >= 2:
target = date(today.year, today.month - 1, 15)
else:
target = date(today.year - 1, 12, 15)
ru_months = {
1: "января", 2: "февраля", 3: "марта", 4: "апреля",
5: "мая", 6: "июня", 7: "июля", 8: "августа",
9: "сентября", 10: "октября", 11: "ноября", 12: "декабря",
}
text = f"{target.day} {ru_months[target.month]} в 12:30"
assert _parse_relative_date(text) == target
def test_absolute_date_future_month_rolls_back():
"""Если today январь, а текст «5 декабря» — это прошлый год."""
today = date.today()
if today.month == 1:
# Today already January, «декабря» → previous year December
result = _parse_relative_date("5 декабря")
assert result == date(today.year - 1, 12, 5)
else:
# Skip when not January — would change semantics
result = _parse_relative_date("5 января")
# Either matches current year January (если уже прошло) или previous
assert result is not None and result.month == 1 and result.day == 5