- avito.py: AVITO_PROXY_URL → proxies в curl_cffi AsyncSession; firewall-детект «Доступ ограничен: проблема с IP» на HTTP 200 → AvitoBlockedError (раньше бан проходил молча как 0-cards); ротация мобильного IP через changeip-ссылку (_rotate_ip) с retry до avito_proxy_max_rotations; transient-retry на сетевых обрывах мобильного канала. - config.py: avito_proxy_url / avito_proxy_rotate_url / avito_proxy_max_rotations. - #726: sortTimeStamp → дата в MSK (UTC+3 fixed offset), а не UTC — совпадает с датой на сайте; off-by-one для постов 00:00–03:00 MSK устранён. - tests: firewall-детект (HTTP 200), ротация+повтор, exhausted-rotation; serp_date переведён на MSK. Live-verified с прод-сервера: mobile proxy снимает firewall (0→48 карточек), ротация IP 188.162.65.168→94.25.229.234. Без env AVITO_PROXY_URL поведение не меняется (dev). Требует deploy (#659) + env AVITO_PROXY_URL/ROTATE_URL на VPS.
62 lines
2.8 KiB
Python
62 lines
2.8 KiB
Python
"""#726 — avito SERP listing_date extraction (sortTimeStamp JSON + DOM fallback).
|
||
|
||
Offline: парсит фикстуру через AvitoScraper._parse_html, без сети/curl_cffi/БД.
|
||
Регрессия на дрейф разметки: дата у lazy-карточек (без DOM item-date) берётся из
|
||
per-item JSON sortTimeStamp; у верхних карточек — из DOM data-marker="item-date".
|
||
"""
|
||
|
||
import os
|
||
from datetime import date, datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||
|
||
from app.services.scrapers.avito import (
|
||
AvitoScraper,
|
||
_build_sort_timestamp_map,
|
||
)
|
||
|
||
FIXTURE = Path(__file__).parent / "fixtures" / "avito_serp_sample.html"
|
||
|
||
# Из фикстуры (sortTimeStamp, epoch-ms).
|
||
_TS_LAZY = 1700000000000
|
||
_TS_TOP = 1700086400000
|
||
|
||
# #726: дата берётся в MSK (UTC+3), чтобы совпадать с тем, что Avito показывает на
|
||
# сайте (sortTimeStamp — UTC epoch, но отображается по Москве).
|
||
_MSK = timezone(timedelta(hours=3))
|
||
|
||
|
||
def _expected(ms: int) -> date:
|
||
return datetime.fromtimestamp(ms / 1000, tz=_MSK).date()
|
||
|
||
|
||
def test_sort_timestamp_map_built_from_json() -> None:
|
||
html = FIXTURE.read_text(encoding="utf-8")
|
||
ts_map = _build_sort_timestamp_map(html)
|
||
assert ts_map["8043936560"] == _expected(_TS_LAZY)
|
||
assert ts_map["8163084615"] == _expected(_TS_TOP)
|
||
|
||
|
||
def test_parse_html_sets_listing_date_for_all_cards() -> None:
|
||
"""Обе карточки получают listing_date != None (DoD #726)."""
|
||
html = FIXTURE.read_text(encoding="utf-8")
|
||
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
|
||
assert len(lots) == 2
|
||
by_url = {lot.source_url: lot for lot in lots}
|
||
assert all(lot.listing_date is not None for lot in lots), "every card must carry a date"
|
||
|
||
# Lazy card (нет DOM item-date) → дата из JSON sortTimeStamp.
|
||
lazy = next(lot for lot in lots if "8043936560" in (lot.source_url or ""))
|
||
assert lazy.listing_date == _expected(_TS_LAZY)
|
||
assert by_url # touch to keep linter calm if structure changes
|
||
|
||
|
||
def test_lazy_card_date_comes_from_json_not_dom() -> None:
|
||
"""Карточка без data-marker='item-date' всё равно датируется (JSON primary)."""
|
||
html = FIXTURE.read_text(encoding="utf-8")
|
||
assert 'data-item-id="8043936560"' in html
|
||
# у этой карточки в фикстуре НЕТ DOM item-date — проверяем, что дата не потерялась
|
||
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/x")
|
||
lazy = next(lot for lot in lots if "8043936560" in (lot.source_url or ""))
|
||
assert lazy.listing_date == _expected(_TS_LAZY)
|