fix(tradein): Yandex listing_date extraction — keyword + absolute-no-year
Production: Yandex Realty имеет 60% listings с listing_date=NULL (2234/3704).
Audit показал parse_listing_date() ловил только absolute с годом
(«9 мая 2026») и relative numeric («5 дней назад»), но Yandex SERP
для свежих cards чаще использует:
- Keyword: «сегодня в 12:30», «вчера», «позавчера», «только что»
- Absolute без года: «18 мая» (год inferred)
Changes:
- yandex_helpers.py:
* RE_RU_DATE_NO_YEAR — regex для absolute без \d{4}
* _RU_MONTH_PREFIXES dict для day+month → date
* parse_listing_date(): 4 fallback tiers
1) keyword shortcuts
2) absolute с годом (existing)
3) absolute без года → infer current year, rollback на prev если future >30d
4) relative numeric (existing)
- tests: 3 new test groups (keyword/absolute-no-year/relative-still-works),
41/41 pass
Impact: linkage rate в /sales-vs-listings (PR K) для Yandex pairs
повысится с 40% → ~90% после next sweep. Existing 2234 NULL — будут
re-extracted при periodic scrape (URL остался).
Mirrors PR N (Avito same fix). Pattern теперь consistent между scrapers.
Refs #564.
This commit is contained in:
parent
f5c87201f1
commit
bdb9f3dd09
2 changed files with 95 additions and 3 deletions
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue