fix(tradein/avito): detect HTTP-200 soft-block → rotate (vs silent failed) + coords-priority backfill ordering #2034
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2034
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-avito-detail-coords-priority-firewall"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #1950 #1967.
Проблема (прод-замер 2026-06-28)
В каждом ночном
avito_detail_backfillrunblocked=0, при этомfailedдоминирует иenrichedкаскадно падает к 0:Блок-ветка не срабатывала никогда — весь анти-бот уходил в generic
failed, IP не ротировался.Root cause (подтверждён живым fetch с прод-прокси): под нагрузкой Avito вместо detail-страницы отдаёт HTTP 200 с общей витриной
<title>Авито — Объявления на сайте Авито</title>(293 KB, безitem-view) — мягкий анти-бот deflect._is_firewall_pageловит лишь 3 IP-block маркера («доступ ограничен» / «проблема с ip» / «firewall-container») — этой витрины в них нет →parse_detail_htmlпадаетValueError("Cannot extract item_id")→ genericexcept Exception→failed, без ротации. Доминирующий лог:Cannot extract item_id from https://www.avito.ru/ekaterinburg/kvartiry/...(каждый URL ×5).FIX 2 — soft-block → block → ротация
Новый
_is_detail_soft_block(html): нетitem-view/item-id/item-view/title-infoИ generic<title>в первых 16 KB. OR-ится в сигналis_firewallв обоих путяхfetch_detail(curl + browser). Эффект: soft-block проходит существующую 403/firewall reconnect-машинерию → пересоздание эфемерной сессии = свежий backconnect exit-IP → escape; либо после_AVITO_DETAIL_403_MAX_RETRIES→AvitoBlockedError→ блок-ветка backfill ротирует и сбрасываетconsecutive_blocksпри первом успехе.Точность детекта (не задевает легит-ответы): реальное объявление содержит
item-view→False(guard первым, до title-скана); снятое объявление Avito рендерит как item-view + баннер →False; 404/не-200 обрабатывается выше (ValueError→failed), сюда не доходит. Множество страниц, достигающих title-скана = ровно те, что и так падали быValueError— никогда не парсящееся объявление.Взаимодействие с #1950 timeout: soft-block-страница возвращается ~1 c (293 KB за <1 c, замерено), 5 reconnect ≈ 16 c ≪
fetch_timeout_s=90s→ raisesAvitoBlockedError(ротация), не режется вTimeoutError→failed.FIX 1 — coords-priority ordering (#1967)
Snapshot SELECT:
ORDER BY (lat IS NULL) DESC, scraped_at DESC NULLS LAST— сперва листинги без координат (detail-страница даёт точные координаты здания), затем по свежести.lat— колонкаlistings, в ORDER BY без добавления в SELECT.Честная сверка scope: из 13 832 active avito с
detail NULL + lat NULLв snapshot-scope (/ekaterinburg/-URL, #1814) попадает лишь 321; остальные 13 511 — non-EKB URL (moskva/spb/tyumen legacy), намеренно исключённые scope-фильтром #1814 (browser-fetch на legacy не-ЕКБ → curl-бан). Т.е. FIX 1 приоритизирует 321 in-scope coord-hole (гарантированно первыми каждый run вместо ~92 размазанных), а не 13 832 — bulk coord-gap вне snapshot scope by design и здесь не адресуется.Тесты
tests/scrapers/test_avito_detail_soft_block.py(new): reconnect-escape (200 soft-block → reconnect → clean 200), exhausted →AvitoBlockedError(build.call_count == _AVITO_DETAIL_403_MAX_RETRIES), unit-детектор. + assert(lat IS NULL) DESCв snapshot-тесте. Локально: ruff-100 clean,20 passed.Post-deploy verify
Admin-триггер вне окна + проверка в БД: доля enriched/attempted ↑ vs runs 434-437 (~0-2/150),
blocked > 0(ротация включилась), обогащаются именно lat-null листинги. Замер до/после первого окна (next_run 10:39 UTC) — комментом в #1950.Под нагрузкой Avito отдаёт HTTP 200 с общей витриной ("Авито — Объявления на сайте Авито", без item-view) вместо detail-страницы — мягкий анти-бот deflect. _is_firewall_page (3 IP-block маркера) её не ловил → parse падал ValueError("Cannot extract item_id") → generic failed, blocked=0, IP не ротировался, enriched каскадно падал к 0 (прод runs 431→437: 115→0). FIX 2: _is_detail_soft_block() (нет item-view + generic <title> в первых 16KB) OR-ится в is_firewall в обоих путях fetch_detail (curl + browser) → soft-block проходит существующую 403/firewall reconnect-машинерию (свежий backconnect exit-IP) либо после MAX_RETRIES → AvitoBlockedError → loop-ротация. FIX 1: snapshot ORDER BY (lat IS NULL) DESC, scraped_at DESC NULLS LAST — сперва листинги без координат (detail-страница даёт координаты здания, #1967). Tests: test_avito_detail_soft_block.py (reconnect-escape, exhausted->blocked, unit-detector) + lat-priority assert в snapshot-тесте.