From 78e3da6bc31295c065ca6a9e6fabc41577648422 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 6 Sep 2026 00:14:43 +0500 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20=D0=BC=D0=B0=D1=80=D0=BA=D0=B5?= =?UTF-8?q?=D1=80=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D1=82=D1=8B=20=E2=80=94?= =?UTF-8?q?=20encryptedPhones=20=D0=98=D0=9B=D0=98=20redirectPhones=20(#31?= =?UTF-8?q?91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ревью #3364. Требуя именно encryptedPhones, отбраковывали бы вечно необмеренный класс карточек «без телефона / только чат»: в очередь они возвращаются, а ключ не появится. redirectPhones измерен тем же замером #3192 и присутствует в обеих ветках (с куками и без). Текст ABORT: consecutive_none смешанный (фетч-ошибка + parse-None + недогруз) — «N подряд без обогащения», а не «недогруженных». --- tradein-mvp/backend/app/core/config.py | 2 +- .../app/tasks/yandex_detail_backfill.py | 4 +++- .../test_3191_yandex_detail_completeness.py | 16 ++++++++++++++++ .../src/scraper_kit/providers/yandex/detail.py | 18 +++++++++++------- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/tradein-mvp/backend/app/core/config.py b/tradein-mvp/backend/app/core/config.py index b03dafa3..44109609 100644 --- a/tradein-mvp/backend/app/core/config.py +++ b/tradein-mvp/backend/app/core/config.py @@ -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 diff --git a/tradein-mvp/backend/app/tasks/yandex_detail_backfill.py b/tradein-mvp/backend/app/tasks/yandex_detail_backfill.py index 4443c61f..31e7cad7 100644 --- a/tradein-mvp/backend/app/tasks/yandex_detail_backfill.py +++ b/tradein-mvp/backend/app/tasks/yandex_detail_backfill.py @@ -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, diff --git a/tradein-mvp/backend/tests/test_3191_yandex_detail_completeness.py b/tradein-mvp/backend/tests/test_3191_yandex_detail_completeness.py index 593ae744..5e1ad9dc 100644 --- a/tradein-mvp/backend/tests/test_3191_yandex_detail_completeness.py +++ b/tradein-mvp/backend/tests/test_3191_yandex_detail_completeness.py @@ -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 = "" + "x" * 3_920_119 + assert detail_incomplete_reason(body + '"redirectPhones":["a"]') is None + # Фальсификация: без ОБОИХ ключей та же страница полного размера — недогруз. + assert detail_incomplete_reason(body + "") is not None diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/detail.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/detail.py index 510bdf01..6decbf5c 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/detail.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/detail.py @@ -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