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 consecutive_blocks = 0
detail_house_paths: set[str] = set()
for idx, row in enumerate(priority_rows): for idx, row in enumerate(priority_rows):
source_url: str = row["source_url"] source_url: str = row["source_url"]
counters.detail_attempted += 1 counters.detail_attempted += 1
@ -278,6 +279,12 @@ async def run_avito_pipeline(
enrichment_detail = await fetch_detail(item_url, cffi_session=session) enrichment_detail = await fetch_detail(item_url, cffi_session=session)
if save_detail_enrichment(db, enrichment_detail): if save_detail_enrichment(db, enrichment_detail):
counters.detail_enriched += 1 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 consecutive_blocks = 0
except (AvitoBlockedError, AvitoRateLimitedError) as e: except (AvitoBlockedError, AvitoRateLimitedError) as e:
consecutive_blocks += 1 consecutive_blocks += 1
@ -310,6 +317,35 @@ async def run_avito_pipeline(
jitter = random.uniform(0.8, 1.2) jitter = random.uniform(0.8, 1.2)
await asyncio.sleep(detail_delay * jitter) 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( logger.info(
"pipeline:done anchor=(%.4f,%.4f) lots=%d (ins=%d/upd=%d) " "pipeline:done anchor=(%.4f,%.4f) lots=%d (ins=%d/upd=%d) "
"houses=%d/%d detail=%d/%d touched_house_ids=%d errors=%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 dataclasses import dataclass, field
from datetime import date from datetime import date
from typing import Any from typing import Any
from urllib.parse import urljoin from urllib.parse import urljoin, urlparse
from curl_cffi.requests import AsyncSession from curl_cffi.requests import AsyncSession
from selectolax.parser import HTMLParser, Node 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) metro_stations = _extract_metro(description)
# ── House catalog link ─────────────────────────────────────────────────── # ── 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_catalog_url: str | None = None
house_link = tree.css_first("[data-marker='nd-jk-details-button']") 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: if house_link is not None:
href = house_link.attributes.get("href", "") href = house_link.attributes.get("href", "")
if href: if href:
house_catalog_url = urljoin(AVITO_BASE, href) # отбрасываем ?context-трекинг → чистый catalog-путь
house_catalog_url = urljoin(AVITO_BASE, urlparse(href).path)
# ── Photo gallery ──────────────────────────────────────────────────────── # ── Photo gallery ────────────────────────────────────────────────────────
photo_urls: list[str] = [] photo_urls: list[str] = []

View file

@ -4,6 +4,8 @@
без сетевых запросов. без сетевых запросов.
""" """
# ruff: noqa: E501 # HTML-фикстуры содержат длинные строки разметки
import pytest import pytest
from app.services.scrapers.avito_detail import DetailEnrichment, parse_detail_html 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") 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: def test_bathroom_type_combined() -> None:
html = """ html = """
<html><body> <html><body>