fix(tradein): Yandex listing_date — keyword + absolute-no-year #602

Merged
Light1YT merged 1 commit from fix/yandex-listing-date-keyword-formats into main 2026-05-27 13:11:23 +00:00
2 changed files with 95 additions and 3 deletions

View file

@ -188,17 +188,73 @@ def _parse_relative_date_yandex(s: str | None) -> date | None:
return None
RE_RU_DATE_NO_YEAR = re.compile(
r"(\d{1,2})\s+(январ\w*|феврал\w*|март\w*|апрел\w*|ма[яй]|"
r"июн\w*|июл\w*|август\w*|сентябр\w*|октябр\w*|ноябр\w*|декабр\w*)"
r"(?!\s+\d{4})",
flags=re.I,
)
_RU_MONTH_PREFIXES: dict[str, int] = {
"январ": 1, "феврал": 2, "март": 3, "апрел": 4,
"ма": 5, "июн": 6, "июл": 7, "август": 8,
"сентябр": 9, "октябр": 10, "ноябр": 11, "декабр": 12,
}
def parse_listing_date(text: str | None) -> date | None:
"""Parse listing date from Yandex card text.
Tries in order:
1. Absolute Russian date: '9 мая 2026' date(2026, 5, 9)
2. Relative date: 'N дней назад' date.today() - N days
Returns None if neither matches.
1. Keyword shortcut: 'сегодня' / 'вчера' / 'позавчера' relative day
2. Absolute Russian date with year: '9 мая 2026' date(2026, 5, 9)
3. Absolute without year: '18 мая' current year (rollback to prev
year if month >30 дней в будущем)
4. Relative date: 'N дней назад' date.today() - N days
Returns None if none match.
"""
if not text:
return None
text_lower = text.lower()
# 1. Keyword shortcuts (common on Yandex SERP for recent cards).
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()
# 2. Absolute with year (existing).
result = parse_ru_date(text)
if result is not None:
return result
# 3. Absolute without year — infer current, rollback to prev if future.
m = RE_RU_DATE_NO_YEAR.search(text)
if m:
day = int(m.group(1))
month_raw = m.group(2).lower()
month: int | None = None
for prefix, num in _RU_MONTH_PREFIXES.items():
if month_raw.startswith(prefix):
month = num
break
if month is not None and 1 <= day <= 31:
today = date.today()
try:
parsed = date(today.year, month, day)
except ValueError:
parsed = None
if parsed and (parsed - today).days > 30:
try:
parsed = date(today.year - 1, month, day)
except ValueError:
parsed = None
if parsed:
return parsed
# 4. Relative numeric.
return _parse_relative_date_yandex(text)

View file

@ -132,6 +132,42 @@ def test_parse_ru_date_empty():
assert parse_ru_date(None) is None
# --- parse_listing_date (extended formats) ---
def test_listing_date_today_keyword():
from datetime import timedelta
from app.services.scrapers.yandex_helpers import parse_listing_date
assert parse_listing_date("сегодня в 12:30") == date.today()
assert parse_listing_date("вчера в 18:45") == date.today() - timedelta(days=1)
assert parse_listing_date("позавчера") == date.today() - timedelta(days=2)
assert parse_listing_date("только что") == date.today()
def test_listing_date_absolute_no_year():
"""Yandex SERP формат «18 мая» без года — infer current year."""
from app.services.scrapers.yandex_helpers import parse_listing_date
today = date.today()
# Pick a date в прошлом этого года, чтобы тест стабильный
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]}"
assert parse_listing_date(text) == target
def test_listing_date_relative_still_works():
from datetime import timedelta
from app.services.scrapers.yandex_helpers import parse_listing_date
assert parse_listing_date("5 дней назад") == date.today() - timedelta(days=5)
# --- parse_dmy ---
def test_parse_dmy_basic():
assert parse_dmy("17.02.2026") == date(2026, 2, 17)