Merge pull request 'fix(scrapers): avito_houses — отзывы/история/тип ЖК долетают (виджет-payload вложен глубже) (#1789)' (#1797) from feat/avito-houses-mfe-state into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 6s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Failing after 1m18s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Has been skipped
Some checks failed
Deploy Trade-In / changes (push) Successful in 6s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Failing after 1m18s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Has been skipped
Reviewed-on: #1797
This commit is contained in:
commit
059fa83589
3 changed files with 302 additions and 148 deletions
|
|
@ -4,6 +4,11 @@
|
|||
и разбирает 10 виджетов: housePage, reviews, miniSerp, housePlacementHistory,
|
||||
recommendations.
|
||||
|
||||
Avito (≈2026, MFE) перенёс полезную нагрузку виджетов на уровень глубже —
|
||||
под ключ с именем самого виджета (props['reviews']['entries'],
|
||||
props['miniSerp']['items'], props['housePlacementHistory']['items'] и т.д.).
|
||||
Разворачивание делает _widget_payload с fallback на старую форму (props напрямую).
|
||||
|
||||
Flow:
|
||||
fetch_house_catalog(house_url)
|
||||
→ HTTP GET (curl_cffi chrome120)
|
||||
|
|
@ -316,6 +321,26 @@ def _get_widget(placeholders: list[dict[str, Any]], widget_type: str) -> dict[st
|
|||
return None
|
||||
|
||||
|
||||
def _widget_payload(widget: dict[str, Any], widget_type: str) -> dict[str, Any]:
|
||||
"""Возвращает полезную нагрузку виджета, разворачивая type-вложенность.
|
||||
|
||||
Avito (≈2026, MFE) переместил данные виджета на уровень глубже —
|
||||
под ключ с именем самого виджета:
|
||||
новый: widget['props'][widget_type] = {... entries / items ...}
|
||||
старый: widget['props'] = {... entries / items ...}
|
||||
|
||||
Хелпер пробует новую форму (props[widget_type]), fallback на старую
|
||||
(props напрямую) → поддержка обеих форм.
|
||||
"""
|
||||
props = widget.get("props", {})
|
||||
if not isinstance(props, dict):
|
||||
return {}
|
||||
nested = props.get(widget_type)
|
||||
if isinstance(nested, dict):
|
||||
return nested
|
||||
return props
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Widget parsers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -427,7 +452,8 @@ def _parse_reviews(widget: dict[str, Any]) -> tuple[list[HouseReview], str | Non
|
|||
reviews: list[HouseReview] = []
|
||||
next_page_url: str | None = None
|
||||
|
||||
entries = widget.get("props", {}).get("entries", [])
|
||||
payload = _widget_payload(widget, "reviews")
|
||||
entries = payload.get("entries", [])
|
||||
for entry in entries:
|
||||
entry_type = entry.get("type")
|
||||
value = entry.get("value", {})
|
||||
|
|
@ -471,7 +497,7 @@ def _parse_mini_serp(widget: dict[str, Any]) -> list[MiniSerpListing]:
|
|||
"""Парсит виджет miniSerp → список активных объявлений в доме."""
|
||||
listings: list[MiniSerpListing] = []
|
||||
|
||||
items = widget.get("props", {}).get("items", [])
|
||||
items = _widget_payload(widget, "miniSerp").get("items", [])
|
||||
for item in items:
|
||||
geo = item.get("geo", {})
|
||||
colors: list[str] = geo.get("colors", [])
|
||||
|
|
@ -506,7 +532,7 @@ def _parse_placement_history(widget: dict[str, Any]) -> list[PlacementHistoryIte
|
|||
"""
|
||||
result: list[PlacementHistoryItem] = []
|
||||
|
||||
items = widget.get("props", {}).get("items", [])
|
||||
items = _widget_payload(widget, "housePlacementHistory").get("items", [])
|
||||
for item in items:
|
||||
item_copy = {k: v for k, v in item.items() if k != "itemImage"}
|
||||
result.append(
|
||||
|
|
@ -530,7 +556,7 @@ def _parse_recommendations(widget: dict[str, Any]) -> list[RecommendationItem]:
|
|||
"""Парсит виджет recommendations → lightweight список похожих объявлений."""
|
||||
result: list[RecommendationItem] = []
|
||||
|
||||
items = widget.get("props", {}).get("items", [])
|
||||
items = _widget_payload(widget, "recommendations").get("items", [])
|
||||
for item in items:
|
||||
additional_info: list[str] = item.get("additionalInfo", [])
|
||||
address = additional_info[0] if additional_info else None
|
||||
|
|
@ -636,6 +662,8 @@ def parse_houses_state(state: dict[str, Any], house_url: str) -> HouseCatalogEnr
|
|||
reviews_widget = _get_widget(placeholders, "reviews")
|
||||
if reviews_widget is not None:
|
||||
reviews, next_page_url = _parse_reviews(reviews_widget)
|
||||
if not reviews:
|
||||
logger.warning("Виджет 'reviews' найден, но 0 отзывов распарсено — проверь шейп props")
|
||||
else:
|
||||
logger.debug("Виджет 'reviews' не найден — пропускаем")
|
||||
|
||||
|
|
@ -652,6 +680,10 @@ def parse_houses_state(state: dict[str, Any], house_url: str) -> HouseCatalogEnr
|
|||
history_widget = _get_widget(placeholders, "housePlacementHistory")
|
||||
if history_widget is not None:
|
||||
placement_history = _parse_placement_history(history_widget)
|
||||
if not placement_history:
|
||||
logger.warning(
|
||||
"Виджет 'housePlacementHistory' найден, но 0 items распарсено — проверь шейп props"
|
||||
)
|
||||
else:
|
||||
logger.debug("Виджет 'housePlacementHistory' не найден — пропускаем")
|
||||
|
||||
|
|
@ -842,7 +874,8 @@ def _persist_house(db: Session, h: HouseInfo, house_url: str) -> int:
|
|||
lat = COALESCE(houses.lat, CAST(:lat AS double precision)),
|
||||
lon = COALESCE(houses.lon, CAST(:lon AS double precision)),
|
||||
year_built = COALESCE(houses.year_built, CAST(:year_built AS int)),
|
||||
last_scraped_at = NOW()
|
||||
last_scraped_at = NOW(),
|
||||
avito_validated_at = NOW()
|
||||
WHERE id = CAST(:house_id AS bigint)
|
||||
"""),
|
||||
{
|
||||
|
|
|
|||
8
tradein-mvp/backend/tests/fixtures/avito_house_572538.html
vendored
Normal file
8
tradein-mvp/backend/tests/fixtures/avito_house_572538.html
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -30,112 +30,143 @@ FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
MINIMAL_STATE: dict = {
|
||||
"data": {"data": {"page": {"placeholders": [
|
||||
{"type": "breadcrumbs"},
|
||||
{"type": "title"},
|
||||
{"type": "gallery", "props": {}},
|
||||
{
|
||||
"type": "housePage",
|
||||
"props": {"developmentData": {
|
||||
"avitoId": 3171365,
|
||||
"id": "MTE3LjM3MDg5MA",
|
||||
"title": "Akademika Postovskogo 17a",
|
||||
"address": "ул. Постовского, 17а",
|
||||
"fullAddress": "Свердловская обл., Екатеринбург, ул. Постовского, 17а",
|
||||
"coords": {"lat": 56.790699, "lng": 60.580191},
|
||||
"aboutDevelopment": {"expandParams": {"items": [
|
||||
{"title": "Параметры дома", "params": [
|
||||
{"type": "Год постройки", "value": "2019"},
|
||||
{"type": "Этажей", "value": "25"},
|
||||
{"type": "Тип дома", "value": "Монолитный"},
|
||||
{"type": "Пассажирский лифт", "value": "2"},
|
||||
{"type": "Консьерж", "value": "Да"},
|
||||
{"type": "Закрытый двор", "value": "Да"},
|
||||
{"type": "Детская площадка", "value": "Да"},
|
||||
]},
|
||||
]}},
|
||||
"developer": {"name": "Атомстройкомплекс", "key": "atomstroy"},
|
||||
"mapPreview": {
|
||||
"distance": "в 5 минутах ходьбы",
|
||||
"objects": "школа, парк, аптека",
|
||||
"pins": [{"lat": 56.79, "lng": 60.58, "type": "school"}],
|
||||
},
|
||||
"ratingBadge": {"info": {"score": 4.666, "scoreString": "4,7"}},
|
||||
"ratingSummaryStat": {
|
||||
"ratingStat": [{"score": 5, "count": 4}, {"score": 4, "count": 2}],
|
||||
"reviewCount": 6,
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
"type": "reviews",
|
||||
"props": {"entries": [
|
||||
{"type": "rating", "value": {
|
||||
"id": 12345,
|
||||
"author": {"title": "Иван"},
|
||||
"reviewTitle": "Хорошо",
|
||||
"score": 5,
|
||||
"modelExperience": "Живу в своей квартире",
|
||||
"rated": "9 октября 2025",
|
||||
"textSections": [
|
||||
{"title": "", "text": "Общий отзыв"},
|
||||
{"title": "Преимущества", "text": "Парк рядом"},
|
||||
{"title": "Недостатки", "text": "Парковки мало"},
|
||||
],
|
||||
}},
|
||||
{"type": "pages", "value": {"nextPageUrl": "/reviews?page=2"}},
|
||||
]},
|
||||
},
|
||||
{
|
||||
"type": "miniSerp",
|
||||
"props": {"items": [
|
||||
{
|
||||
"id": 7986882804,
|
||||
"title": "3-к. квартира",
|
||||
"description": "Описание",
|
||||
"url": "/ekaterinburg/kvartiry/test_7986882804",
|
||||
"price": "11 990 000 ₽",
|
||||
"geo": {"content": "Чкаловская, 2,7 км", "colors": ["#cf2734"]},
|
||||
"seller": {
|
||||
"name": "DOMRF66",
|
||||
"type": "Компания",
|
||||
"from": "На Авито с июля 2012",
|
||||
"url": "/brands/domrf66",
|
||||
"logos": [],
|
||||
"data": {
|
||||
"data": {
|
||||
"page": {
|
||||
"placeholders": [
|
||||
{"type": "breadcrumbs"},
|
||||
{"type": "title"},
|
||||
{"type": "gallery", "props": {}},
|
||||
{
|
||||
"type": "housePage",
|
||||
"props": {
|
||||
"developmentData": {
|
||||
"avitoId": 3171365,
|
||||
"id": "MTE3LjM3MDg5MA",
|
||||
"title": "Akademika Postovskogo 17a",
|
||||
"address": "ул. Постовского, 17а",
|
||||
"fullAddress": (
|
||||
"Свердловская обл., Екатеринбург, ул. Постовского, 17а"
|
||||
),
|
||||
"coords": {"lat": 56.790699, "lng": 60.580191},
|
||||
"aboutDevelopment": {
|
||||
"expandParams": {
|
||||
"items": [
|
||||
{
|
||||
"title": "Параметры дома",
|
||||
"params": [
|
||||
{"type": "Год постройки", "value": "2019"},
|
||||
{"type": "Этажей", "value": "25"},
|
||||
{"type": "Тип дома", "value": "Монолитный"},
|
||||
{"type": "Пассажирский лифт", "value": "2"},
|
||||
{"type": "Консьерж", "value": "Да"},
|
||||
{"type": "Закрытый двор", "value": "Да"},
|
||||
{"type": "Детская площадка", "value": "Да"},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
"developer": {"name": "Атомстройкомплекс", "key": "atomstroy"},
|
||||
"mapPreview": {
|
||||
"distance": "в 5 минутах ходьбы",
|
||||
"objects": "школа, парк, аптека",
|
||||
"pins": [{"lat": 56.79, "lng": 60.58, "type": "school"}],
|
||||
},
|
||||
"ratingBadge": {"info": {"score": 4.666, "scoreString": "4,7"}},
|
||||
"ratingSummaryStat": {
|
||||
"ratingStat": [
|
||||
{"score": 5, "count": 4},
|
||||
{"score": 4, "count": 2},
|
||||
],
|
||||
"reviewCount": 6,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]},
|
||||
},
|
||||
{
|
||||
"type": "housePlacementHistory",
|
||||
"props": {"items": [
|
||||
{
|
||||
"id": 7000000001,
|
||||
"title": "1-к. квартира",
|
||||
"startPrice": 5000000,
|
||||
"startPriceDate": 1697000000,
|
||||
"lastPrice": 4900000,
|
||||
"lastPriceDate": 1715000000,
|
||||
"exposure": 210,
|
||||
"itemImage": {},
|
||||
},
|
||||
]},
|
||||
},
|
||||
{"type": "uxFeedback"},
|
||||
{
|
||||
"type": "recommendations",
|
||||
"props": {"items": [
|
||||
{
|
||||
"title": "Похожая",
|
||||
"priceRange": "от 6 млн",
|
||||
"additionalInfo": ["Адрес 1"],
|
||||
"url": "/rec1",
|
||||
"images": [{"208x156": "https://avito.ru/img1.jpg"}],
|
||||
},
|
||||
]},
|
||||
},
|
||||
{"type": "padding"},
|
||||
]}}}
|
||||
{
|
||||
"type": "reviews",
|
||||
"props": {
|
||||
"entries": [
|
||||
{
|
||||
"type": "rating",
|
||||
"value": {
|
||||
"id": 12345,
|
||||
"author": {"title": "Иван"},
|
||||
"reviewTitle": "Хорошо",
|
||||
"score": 5,
|
||||
"modelExperience": "Живу в своей квартире",
|
||||
"rated": "9 октября 2025",
|
||||
"textSections": [
|
||||
{"title": "", "text": "Общий отзыв"},
|
||||
{"title": "Преимущества", "text": "Парк рядом"},
|
||||
{"title": "Недостатки", "text": "Парковки мало"},
|
||||
],
|
||||
},
|
||||
},
|
||||
{"type": "pages", "value": {"nextPageUrl": "/reviews?page=2"}},
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "miniSerp",
|
||||
"props": {
|
||||
"items": [
|
||||
{
|
||||
"id": 7986882804,
|
||||
"title": "3-к. квартира",
|
||||
"description": "Описание",
|
||||
"url": "/ekaterinburg/kvartiry/test_7986882804",
|
||||
"price": "11 990 000 ₽",
|
||||
"geo": {"content": "Чкаловская, 2,7 км", "colors": ["#cf2734"]},
|
||||
"seller": {
|
||||
"name": "DOMRF66",
|
||||
"type": "Компания",
|
||||
"from": "На Авито с июля 2012",
|
||||
"url": "/brands/domrf66",
|
||||
"logos": [],
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "housePlacementHistory",
|
||||
"props": {
|
||||
"items": [
|
||||
{
|
||||
"id": 7000000001,
|
||||
"title": "1-к. квартира",
|
||||
"startPrice": 5000000,
|
||||
"startPriceDate": 1697000000,
|
||||
"lastPrice": 4900000,
|
||||
"lastPriceDate": 1715000000,
|
||||
"exposure": 210,
|
||||
"itemImage": {},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
{"type": "uxFeedback"},
|
||||
{
|
||||
"type": "recommendations",
|
||||
"props": {
|
||||
"items": [
|
||||
{
|
||||
"title": "Похожая",
|
||||
"priceRange": "от 6 млн",
|
||||
"additionalInfo": ["Адрес 1"],
|
||||
"url": "/rec1",
|
||||
"images": [{"208x156": "https://avito.ru/img1.jpg"}],
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
{"type": "padding"},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -305,20 +336,28 @@ def test_recommendations() -> None:
|
|||
def test_missing_optional_widgets() -> None:
|
||||
"""Парсер не падает если опциональные виджеты отсутствуют."""
|
||||
state_minimal: dict = {
|
||||
"data": {"data": {"page": {"placeholders": [
|
||||
{
|
||||
"type": "housePage",
|
||||
"props": {"developmentData": {
|
||||
"avitoId": 9999,
|
||||
"coords": {"lat": 56.0, "lng": 60.0},
|
||||
"aboutDevelopment": {"expandParams": {"items": []}},
|
||||
"developer": {},
|
||||
"mapPreview": {},
|
||||
"ratingBadge": {"info": {}},
|
||||
"ratingSummaryStat": {},
|
||||
}},
|
||||
},
|
||||
]}}}
|
||||
"data": {
|
||||
"data": {
|
||||
"page": {
|
||||
"placeholders": [
|
||||
{
|
||||
"type": "housePage",
|
||||
"props": {
|
||||
"developmentData": {
|
||||
"avitoId": 9999,
|
||||
"coords": {"lat": 56.0, "lng": 60.0},
|
||||
"aboutDevelopment": {"expandParams": {"items": []}},
|
||||
"developer": {},
|
||||
"mapPreview": {},
|
||||
"ratingBadge": {"info": {}},
|
||||
"ratingSummaryStat": {},
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
e = parse_houses_state(state_minimal, "/catalog/houses/test/9999")
|
||||
assert e.house.ext_id == 9999
|
||||
|
|
@ -338,9 +377,15 @@ def test_invalid_state_raises() -> None:
|
|||
def test_missing_house_page_raises() -> None:
|
||||
"""ValueError если housePage виджет отсутствует."""
|
||||
state: dict = {
|
||||
"data": {"data": {"page": {"placeholders": [
|
||||
{"type": "breadcrumbs"},
|
||||
]}}}
|
||||
"data": {
|
||||
"data": {
|
||||
"page": {
|
||||
"placeholders": [
|
||||
{"type": "breadcrumbs"},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
with pytest.raises(ValueError, match="housePage"):
|
||||
parse_houses_state(state, "/catalog/houses/test/1")
|
||||
|
|
@ -373,27 +418,42 @@ def test_strip_price() -> None:
|
|||
def test_mini_serp_no_seller() -> None:
|
||||
"""Объявление без продавца не вызывает ошибку."""
|
||||
state: dict = {
|
||||
"data": {"data": {"page": {"placeholders": [
|
||||
{
|
||||
"type": "housePage",
|
||||
"props": {"developmentData": {
|
||||
"avitoId": 111,
|
||||
"coords": {"lat": 56.0, "lng": 60.0},
|
||||
"aboutDevelopment": {"expandParams": {"items": []}},
|
||||
"developer": {},
|
||||
"mapPreview": {},
|
||||
"ratingBadge": {"info": {}},
|
||||
"ratingSummaryStat": {},
|
||||
}},
|
||||
},
|
||||
{
|
||||
"type": "miniSerp",
|
||||
"props": {"items": [
|
||||
{"id": 12345, "title": "1-к.", "url": "/test", "price": "3 000 000 ₽",
|
||||
"geo": {"content": "Центр", "colors": []}},
|
||||
]},
|
||||
},
|
||||
]}}}
|
||||
"data": {
|
||||
"data": {
|
||||
"page": {
|
||||
"placeholders": [
|
||||
{
|
||||
"type": "housePage",
|
||||
"props": {
|
||||
"developmentData": {
|
||||
"avitoId": 111,
|
||||
"coords": {"lat": 56.0, "lng": 60.0},
|
||||
"aboutDevelopment": {"expandParams": {"items": []}},
|
||||
"developer": {},
|
||||
"mapPreview": {},
|
||||
"ratingBadge": {"info": {}},
|
||||
"ratingSummaryStat": {},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "miniSerp",
|
||||
"props": {
|
||||
"items": [
|
||||
{
|
||||
"id": 12345,
|
||||
"title": "1-к.",
|
||||
"url": "/test",
|
||||
"price": "3 000 000 ₽",
|
||||
"geo": {"content": "Центр", "colors": []},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
e = parse_houses_state(state, "/catalog/houses/test/111")
|
||||
assert len(e.mini_serp) == 1
|
||||
|
|
@ -458,3 +518,56 @@ def test_extract_preloaded_state_jsparse_fallback() -> None:
|
|||
def test_extract_preloaded_state_missing_returns_none() -> None:
|
||||
"""Нет маркера → None (а не исключение)."""
|
||||
assert _extract_preloaded_state("<html><body>no state here</body></html>") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Real-HTML fixture (regression for issue #1789 — MFE props nesting)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Fixture: реальная страница /catalog/houses/ekaterinburg/ul_soni_morozovoy_190/572538,
|
||||
# захвачена браузер-сервисом (2026-06-19). Avito перенёс данные виджетов на уровень
|
||||
# глубже — под ключ с именем виджета:
|
||||
# reviews: props['reviews']['entries'] (было props['entries'])
|
||||
# miniSerp: props['miniSerp']['items'] (было props['items'])
|
||||
# housePlacementHistory: props['housePlacementHistory']['items'] (было props['items'])
|
||||
# recommendations: props['recommendations']['items'] (было props['items'])
|
||||
# Из-за этого reviews/placement/seller-обогащение НЕ долетало в БД (0 строк).
|
||||
# Этот тест ловит регрессию шейпа props.
|
||||
|
||||
|
||||
def test_parse_mfe_fixture_widgets_reach_db() -> None:
|
||||
"""Фикстура MFE: reviews/placement/miniSerp/house_type/year_built распарсены непусто."""
|
||||
html = (FIXTURES_DIR / "avito_house_572538.html").read_text(encoding="utf-8")
|
||||
state = _extract_preloaded_state(html)
|
||||
assert state is not None, "URL-encoded __preloadedState__ должен распарситься"
|
||||
|
||||
e = parse_houses_state(state, "/catalog/houses/ekaterinburg/ul_soni_morozovoy_190/572538")
|
||||
|
||||
# housePage — тип дома и год постройки долетают
|
||||
assert e.house.ext_id == 572538
|
||||
assert e.house.house_type is not None, "house_type должен парситься из expandParams"
|
||||
assert e.house.house_type == "brick"
|
||||
assert e.house.year_built == 1994
|
||||
assert e.house.reviews_count == 3
|
||||
|
||||
# reviews — >=1 отзыв с текстом, автором, оценкой
|
||||
assert len(e.reviews) >= 1, "reviews должны распарситься из props['reviews']['entries']"
|
||||
r = e.reviews[0]
|
||||
assert r.ext_review_id, "у отзыва должен быть id"
|
||||
assert r.author_name, "у отзыва должен быть автор"
|
||||
assert r.score is not None, "у отзыва должна быть оценка"
|
||||
assert r.text_main, "у отзыва должен быть основной текст (textSections)"
|
||||
|
||||
# placement — >=1 item из props['housePlacementHistory']['items']
|
||||
assert len(e.placement_history) >= 1, "placement должен распарситься"
|
||||
p = e.placement_history[0]
|
||||
assert p.ext_item_id, "у placement item должен быть id"
|
||||
assert p.start_price is not None
|
||||
assert p.last_price is not None
|
||||
assert isinstance(p.last_price_date, date)
|
||||
|
||||
# miniSerp — >=1 объявление из props['miniSerp']['items']
|
||||
assert len(e.mini_serp) >= 1, "miniSerp должен распарситься"
|
||||
m = e.mini_serp[0]
|
||||
assert m.ext_item_id
|
||||
assert m.price_rub is not None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue