fix(tradein/scrapers): детекция дрейфа разметки вместо тихой пустоты #2535
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#2535
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-audit-scrapers"
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?
Правки по аудиту. Общая тема: скрапер возвращает пустоту вместо сигнала об ошибке, а продукт продолжает считать оценку по остаткам данных, не зная, что источник отвалился.
1. Обход города Авито обнулялся без тревоги
avito/serp.py— при смене DOM-маркера карточки обход возвращал ноль лотов, и это было неотличимо от «объявлений действительно нет».Добавлена перепроверка по независимому сигналу (
_extract_total_count— счётчик на странице либо маркер «ничего не найдено»). Применено в трёх местах: обход города, обход по комнатности и обход с делением по цене. Если первая страница дала ноль карточек, а маркера «ничего не найдено» нет — это ошибка парсера, поднимается существующийAvitoContentBlockedError(механизм уже был, но только в одном из путей).Порог намеренно строгий, ноль против больше нуля: страницы после первой с нулём карточек — это нормальный конец пагинации, ломать его нельзя.
2. Расхождение счётчика и выдачи ЦИАН
cian/serp.py—totalOffersиresults.offersберутся из одного блоба состояния. Если счётчик больше нуля, а список пуст — это внутреннее противоречие ответа, то есть регрессия схемы. Поднято до ERROR с отправкой в трекер.Порог тоже строгий: функция разбора не знает номер страницы, поэтому дробный порог ложно срабатывал бы на легитимной последней неполной странице.
3. Дата публикации на границе года
avito/detail.py— объявлению от декабря, прочитанному в январе, проставлялся текущий год без отката. Свежесть лота завышалась. Теперь если дата получилась в будущем относительно момента разбора — год откатывается назад.4. Проверка сессии ЦИАН смешивала три причины
cian_session.py— смена вёрстки, ошибка сервера и сетевой сбой одинаково трактовались как «куки протухли», из-за чего оператору уходил ложный сигнал перезалить куки. Разведено на три состояния: источник недоступен (5xx или сетевой сбой), разметка изменилась (200, но структура не найдена) и собственно разлогин.Noneтеперь означает только реальный разлогин.⚠️ Незакрытый хвост, о котором надо знать
Задача ограничивала правки пакетом скраперов и
cian_session.py. Реальные вызывающиеverify_session()—product_handlers.py::_cian_pre_claim(главный сценарий из аудита: именно он шлёт «cookies expired, перезалейте») и три места вadmin.py— оказались вне разрешённых границ, и агент их намеренно откатил.Следствие: контракт исправлен и покрыт тестами, но вызывающие пока проверяют только
if state is None. Новые сигналы truthy, поэтому они проваливаются в ветку «аутентифицировано» — это лучше прежнего поведения (ложный алерт про куки больше не уходит), но точной диагностики на стороне вызывающего пока нет. Нужен отдельный follow-up PR, чтобы_cian_pre_claimреально различал причины.Test plan
uv run pytest -q— 2649 passed, 8 skippeduv run ruff checkс проектным конфигом — чисто