96 lines
4.6 KiB
Python
96 lines
4.6 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)
|
||
|
||
|
||
# ── listing_segment по DOM-маркеру item-development-name ───────────────────────
|
||
# Каждый avito-URL квартиры содержит /kvartiry/ (вкл. новостройки), поэтому
|
||
# сегмент определяется не по URL, а по наличию data-marker="item-development-name"
|
||
# (название ЖК/застройщика рендерится только у карточек новостроек).
|
||
|
||
|
||
def _serp_card(item_id: str, *, with_dev_name: bool) -> str:
|
||
dev = '<div data-marker="item-development-name">ЖК «Федерация»</div>' if with_dev_name else ""
|
||
return f"""
|
||
<div data-marker="item" data-item-id="{item_id}">
|
||
<a data-marker="item-title" href="/ekaterinburg/kvartiry/2-k_kvartira_{item_id}">
|
||
2-к. квартира, 50 м², 5/20 эт.</a>
|
||
<meta itemprop="price" content="9500000"/>
|
||
{dev}
|
||
</div>
|
||
"""
|
||
|
||
|
||
def test_segment_novostroyki_when_development_name_present() -> None:
|
||
"""Карточка с data-marker='item-development-name' → listing_segment='novostroyki'."""
|
||
html = f"<html><body>{_serp_card('8043003287', with_dev_name=True)}</body></html>"
|
||
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
|
||
assert len(lots) == 1
|
||
assert lots[0].listing_segment == "novostroyki"
|
||
|
||
|
||
def test_segment_vtorichka_when_no_development_name() -> None:
|
||
"""Карточка без маркера ЖК → listing_segment='vtorichka' (даже с /kvartiry/ в URL)."""
|
||
html = f"<html><body>{_serp_card('8043936560', with_dev_name=False)}</body></html>"
|
||
lots = AvitoScraper()._parse_html(html, "https://www.avito.ru/ekaterinburg/kvartiry")
|
||
assert len(lots) == 1
|
||
assert lots[0].listing_segment == "vtorichka"
|