fix(tradein): avito house-link из detail SSR — SERP-карточка JS-only (#871) (#873)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 30s
Deploy Trade-In / build-backend (push) Successful in 41s
Deploy Trade-In / deploy (push) Successful in 36s

Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
This commit is contained in:
lekss361 2026-05-31 08:27:47 +00:00 committed by bot-reviewer
parent 1684c6ed3e
commit 70838c7dfe
3 changed files with 65 additions and 2 deletions

View file

@ -268,6 +268,7 @@ async def run_avito_pipeline(
)
consecutive_blocks = 0
detail_house_paths: set[str] = set()
for idx, row in enumerate(priority_rows):
source_url: str = row["source_url"]
counters.detail_attempted += 1
@ -278,6 +279,12 @@ async def run_avito_pipeline(
enrichment_detail = await fetch_detail(item_url, cffi_session=session)
if save_detail_enrichment(db, enrichment_detail):
counters.detail_enriched += 1
# #871: house-link есть в detail SSR (в SERP-карточке — JS-only).
# Копим catalog-пути для house-enrich ниже (Step 5b).
if enrichment_detail.house_catalog_url:
hp = urlparse(enrichment_detail.house_catalog_url).path
if hp:
detail_house_paths.add(hp)
consecutive_blocks = 0
except (AvitoBlockedError, AvitoRateLimitedError) as e:
consecutive_blocks += 1
@ -310,6 +317,35 @@ async def run_avito_pipeline(
jitter = random.uniform(0.8, 1.2)
await asyncio.sleep(detail_delay * jitter)
# ── Step 5b: enrich houses, найденные через detail-страницы (#871) ──
# SERP-карточки больше не несут house-link (JS-render) → дома берём из
# detail SSR. Дедуп против обработанных в Step 4 (unique_house_paths).
new_house_paths = detail_house_paths - unique_house_paths
if enrich_houses and new_house_paths:
counters.unique_houses += len(new_house_paths)
nh_list = list(new_house_paths)
for h_idx, house_path in enumerate(nh_list):
try:
enrichment = await fetch_house_catalog(house_path, cffi_session=session)
house_counters = save_house_catalog_enrichment(db, enrichment)
counters.houses_enriched += 1
hid = house_counters.get("house_id")
if hid:
touched_house_ids.add(int(hid))
except (AvitoBlockedError, AvitoRateLimitedError):
logger.error(
"pipeline:house(detail) BLOCKED at %s — propagating", house_path
)
raise
except Exception as e:
logger.warning(
"pipeline:house(detail) enrich failed for %s: %s", house_path, e
)
counters.houses_failed += 1
counters.errors.append(f"house(detail) {house_path}: {e}")
if h_idx < len(nh_list) - 1:
await asyncio.sleep(detail_delay)
logger.info(
"pipeline:done anchor=(%.4f,%.4f) lots=%d (ins=%d/upd=%d) "
"houses=%d/%d detail=%d/%d touched_house_ids=%d errors=%d",

View file

@ -25,7 +25,7 @@ import re
from dataclasses import dataclass, field
from datetime import date
from typing import Any
from urllib.parse import urljoin
from urllib.parse import urljoin, urlparse
from curl_cffi.requests import AsyncSession
from selectolax.parser import HTMLParser, Node
@ -319,12 +319,19 @@ def parse_detail_html(html: str, source_url: str) -> DetailEnrichment:
metro_stations = _extract_metro(description)
# ── House catalog link ───────────────────────────────────────────────────
# Новостройки отдают кнопку ЖК (nd-jk-details-button); вторичка — ссылку
# «узнать больше о доме» под другим маркером. Fallback на любой /catalog/houses/
# якорь: verified 2026-05-31 (#871) — эта ссылка есть в SSR detail-страницы,
# даже когда отсутствует в JS-рендеренной SERP-карточке (откуда мы брали 0).
house_catalog_url: str | None = None
house_link = tree.css_first("[data-marker='nd-jk-details-button']")
if house_link is None:
house_link = tree.css_first("a[href*='/catalog/houses/']")
if house_link is not None:
href = house_link.attributes.get("href", "")
if href:
house_catalog_url = urljoin(AVITO_BASE, href)
# отбрасываем ?context-трекинг → чистый catalog-путь
house_catalog_url = urljoin(AVITO_BASE, urlparse(href).path)
# ── Photo gallery ────────────────────────────────────────────────────────
photo_urls: list[str] = []

View file

@ -4,6 +4,8 @@
без сетевых запросов.
"""
# ruff: noqa: E501 # HTML-фикстуры содержат длинные строки разметки
import pytest
from app.services.scrapers.avito_detail import DetailEnrichment, parse_detail_html
@ -136,6 +138,24 @@ def test_house_catalog_url_full() -> None:
assert result.house_catalog_url.startswith("https://www.avito.ru")
def test_house_catalog_url_secondary_marker_no_nd_jk() -> None:
"""#871: вторичка без nd-jk-details-button → fallback на /catalog/houses/ якорь;
?context-трекинг отброшен (verified live: house-link есть в detail SSR)."""
html = """
<html><body>
<div data-marker="item-view/item-id"> 7683141312</div>
<span itemprop="price" content="4000000">4 000 000 </span>
<a href="/catalog/houses/ekaterinburg/ul_gotvalda_6k1/142792?context=H4sIabc">
Узнать больше о доме</a>
</body></html>
"""
result = parse_detail_html(html, "https://www.avito.ru/test_7683141312")
assert (
result.house_catalog_url
== "https://www.avito.ru/catalog/houses/ekaterinburg/ul_gotvalda_6k1/142792"
)
def test_bathroom_type_combined() -> None:
html = """
<html><body>