fix(tradein): маркер полноты — encryptedPhones ИЛИ redirectPhones (#3191)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m21s

Ревью #3364. Требуя именно encryptedPhones, отбраковывали бы вечно
необмеренный класс карточек «без телефона / только чат»: в очередь они
возвращаются, а ключ не появится. redirectPhones измерен тем же замером
#3192 и присутствует в обеих ветках (с куками и без).

Текст ABORT: consecutive_none смешанный (фетч-ошибка + parse-None +
недогруз) — «N подряд без обогащения», а не «недогруженных».
This commit is contained in:
bot-backend 2026-09-06 00:14:43 +05:00
parent 4b5f8fb54c
commit 78e3da6bc3
4 changed files with 31 additions and 9 deletions

View file

@ -1010,7 +1010,7 @@ class Settings(BaseSettings):
# ── Порог полноты detail-страницы Яндекса (#3191) ────────────────────────
# Полная карточка оффера — 3-5 МБ; недорендеренная приходит с HTTP 200, валидным
# HTML и БЕЗ блока контактов (наблюдалось 1,8 МБ). Размер — второй признак к
# структурному (scraper_kit...yandex.detail.DETAIL_CONTACTS_MARKER); любой из двух
# структурному (scraper_kit...yandex.detail.DETAIL_CONTACTS_MARKERS); любой из двух
# даёт отказ, объявление остаётся в очереди (detail_enriched_at не проставляется).
# ENV: YANDEX_DETAIL_MIN_HTML_BYTES.
yandex_detail_min_html_bytes: int = 1_000_000

View file

@ -467,8 +467,10 @@ async def run_yandex_detail_backfill(
)
if consecutive_none >= max_consecutive_blocks:
logger.error(
# consecutive_none смешанный (фетч-ошибка + parse-None +
# недогруз) — «недогруженных» назвало бы только один вклад.
"yandex_detail_backfill: run_id=%d ABORT -- %d подряд "
"недогруженных страниц. enriched=%d attempted=%d",
"без обогащения. enriched=%d attempted=%d",
run_id,
consecutive_none,
counters.enriched,

View file

@ -140,3 +140,19 @@ def test_reason_names_the_two_signals() -> None:
# Размерный: контакты есть, а страница обрезана.
reason = detail_incomplete_reason(f'"{CONTACTS_MARKER}":[]', min_html_bytes=1_000_000)
assert reason is not None and "недогружена" in reason
def test_redirect_phones_alone_counts_as_complete() -> None:
"""Карточка «без телефона / только чат»: redirectPhones есть, encryptedPhones нет.
Класс в замер #3192 не попадал (там у всех 12 были оба ключа). Требуя именно
encryptedPhones, мы браковали бы такие карточки вечно они возвращаются в очередь,
а ключ у них не появится. Маркеры литералами: правку снимут тест обязан краснеть
по значению («карточка отбракована»), а не по ImportError.
"""
from scraper_kit.providers.yandex.detail import detail_incomplete_reason
body = "<html>" + "x" * 3_920_119
assert detail_incomplete_reason(body + '"redirectPhones":["a"]</html>') is None
# Фальсификация: без ОБОИХ ключей та же страница полного размера — недогруз.
assert detail_incomplete_reason(body + "</html>") is not None

View file

@ -50,11 +50,14 @@ logger = logging.getLogger(__name__)
# offer 52275656 3 920 119 б encryptedPhones 65
# offer 62983459 1 805 556 б encryptedPhones 0 <- недогруз
# offer 12108408 4 732 884 б encryptedPhones 65
# Ключ выбран так, чтобы он был у ПОЛНЫХ карточек в том же режиме, в каком мы ходим:
# `encryptedPhones` отдаётся и анонимной сессии (app/services/yandex_session.py и
# миграция 274 — с куками и без есть encryptedPhones + redirectPhones, отличается
# только mainPhone, поэтому mainPhone признаком полноты быть не может).
DETAIL_CONTACTS_MARKER = "encryptedPhones"
# Ключи выбраны так, чтобы они были у ПОЛНЫХ карточек в том же режиме, в каком мы ходим:
# оба отдаются и анонимной сессии (app/services/yandex_session.py и миграция 274 — с
# куками и без есть encryptedPhones + redirectPhones, отличается только mainPhone,
# поэтому mainPhone признаком полноты быть не может).
# Достаточно ЛЮБОГО из двух: замером #3192 покрыты 12 карточек, у всех были оба ключа,
# но класс «без телефона / только чат» в замер не попал. Требуя encryptedPhones, мы
# браковали бы такие карточки ВЕЧНО (в очередь они возвращаются, а ключ не появится).
DETAIL_CONTACTS_MARKERS = ("encryptedPhones", "redirectPhones")
# Второй, независимый признак. Полная карточка — 3-5 МБ; порог держим заметно ниже
# наблюдавшегося недогруза (1,8 МБ), потому что размер зависит от объявления. Он ловит
@ -74,8 +77,9 @@ def detail_incomplete_reason(
"""
if not html:
return "empty html"
if DETAIL_CONTACTS_MARKER not in html:
return f"нет блока контактов ({DETAIL_CONTACTS_MARKER} отсутствует), {len(html)} б"
if not any(marker in html for marker in DETAIL_CONTACTS_MARKERS):
markers = "/".join(DETAIL_CONTACTS_MARKERS)
return f"нет блока контактов ({markers} отсутствуют), {len(html)} б"
if len(html) < min_html_bytes:
return f"страница недогружена: {len(html)} б < {min_html_bytes} б"
return None