gendesign/tradein-mvp/backend/app/services/scrapers/avito_shared.py
lekss361 32d1818fd4
Some checks failed
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 1m32s
Deploy Trade-In / deploy (push) Has been cancelled
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-backend (push) Successful in 59s
fix(scrapers/avito): unify _unix_to_date timezone across serp/houses/imv (#2129)
2026-07-02 14:03:11 +00:00

79 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""avito_shared.py — общие константы/хелперы для avito*.py модулей.
Единая точка правды для:
- `RUS_MONTHS` — рус. месяц (родительный падеж) → номер, для парсинга дат вида
"9 октября 2025" (было продублировано в avito.py/avito_detail.py/avito_houses.py).
- `_unix_to_date` — unix-epoch (в СЕКУНДАХ) → `date`, в таймзоне Europe/Moscow.
TZ-обоснование (issue #2129): было 3 независимые копии конвертации unix→date с
РАЗНЫМИ timezone — MSK (avito.py, документировано issue #726), UTC (avito_houses.py),
naive/system-local (avito_imv.py). Один и тот же epoch давал разные `date` в
SERP / houses / IMV путях для одного и того же объявления.
Почему MSK — канонический выбор (не UTC, не local):
- Avito — российская площадка, отображает даты объявлений (`item-date` на SERP,
`publishDate`, история цен `startPriceDate`/`lastPriceDate`/`removedDate`) в
Europe/Moscow. Объявление, поднятое в 00:0003:00 MSK, при наивной UTC-конвертации
получило бы дату на день раньше, чем видно на самом Avito (обоснование #726).
- avito_houses.py и avito_imv.py используются на одних и тех же данных
(`housePlacementHistory` widget / IMV `placementHistory` — идентичные поля
startPriceDate/lastPriceDate/removedDate, см. fixtures/avito_imv_getdata.json) —
единственный смысл держать их согласованными с уже принятым MSK-решением из #726.
- Проверено на живой fixture: startPriceDate/lastPriceDate — всегда полночь UTC
(00:00:00), поэтому MSK vs UTC не меняет date для них. `removedDate`/`publishDate`
несут реальное время суток — там UTC/naive-local и MSK ДЕЙСТВИТЕЛЬНО расходятся
(пример из fixture: removedDate=1756077885 → UTC 2025-08-24, MSK 2025-08-25).
Это единственный реальный сдвиг данных от этого фикса: `avito_imv_evaluations` /
`house_placement_history`, поля `removed_date`/`publish_date`, у ~12.5% строк
(события в 21:0023:59 UTC/local) дата сдвинется на +1 день при следующем прогоне
скрейпера. Схему/уже сохранённые строки не трогаем — это единственно правильное
представление на будущее.
- Fixed-offset `timezone(timedelta(hours=3))`, а не `zoneinfo("Europe/Moscow")` —
не зависит от системной tzdata в контейнере; Москва не переходит на DST с 2014,
так что fixed offset корректен всегда.
"""
from __future__ import annotations
import logging
from datetime import date, datetime, timedelta, timezone
logger = logging.getLogger(__name__)
# Europe/Moscow, UTC+3, без перехода на летнее время (с 2014).
MSK = timezone(timedelta(hours=3))
# Русские месяцы (родительный падеж) для парсинга дат вида "9 октября 2025".
RUS_MONTHS: dict[str, int] = {
"января": 1,
"февраля": 2,
"марта": 3,
"апреля": 4,
"мая": 5,
"июня": 6,
"июля": 7,
"августа": 8,
"сентября": 9,
"октября": 10,
"ноября": 11,
"декабря": 12,
}
def _unix_to_date(ts: int | float | None) -> date | None:
"""Конвертирует unix-epoch (в СЕКУНДАХ) в `date`, в таймзоне Europe/Moscow (MSK).
Возвращает `None` для `None`/`0` (Avito использует 0 как sentinel «нет даты») и
для значений, которые не конвертируются в валидную дату.
ВАЖНО: ожидает epoch в СЕКУНДАХ. Avito SERP-поле `sortTimeStamp` отдаёт epoch в
МИЛЛИСЕКУНДАХ — вызывающий код обязан поделить на 1000 до вызова (см.
`avito.py::_build_sort_timestamp_map`).
"""
if not ts:
return None
try:
return datetime.fromtimestamp(float(ts), tz=MSK).date()
except (OSError, OverflowError, ValueError):
logger.warning("Не удалось конвертировать unix timestamp: %r", ts)
return None