Merge pull request 'fix(tradein/avito): страница SERP отдаёт 60 карточек, а не 50 — лишние 17% запросов' (#3041) from fix/avito-offers-per-page into main
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
This commit is contained in:
commit
28f5e8079e
2 changed files with 77 additions and 2 deletions
61
tradein-mvp/backend/tests/test_avito_offers_per_page.py
Normal file
61
tradein-mvp/backend/tests/test_avito_offers_per_page.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""Регрессия на размер страницы Avito SERP.
|
||||
|
||||
Замер живьём 2026-08-21 (tradein-browser / camoufox, JS исполняется): страница
|
||||
выдачи отдаёт 59-60 уникальных `data-item-id`. Константа стояла 50 — то есть
|
||||
`ceil(total / PAGE)` завышал число страниц примерно на 17 %, и каждый лишний
|
||||
запрос — лишний шанс словить SERP firewall.
|
||||
|
||||
Тест намеренно проверяет НЕ только значение, но и то, что константа
|
||||
по-прежнему участвует в расчёте страниц: иначе правку значения легко потерять
|
||||
при рефакторинге, оставив мёртвую константу.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from scraper_kit.providers.avito import serp
|
||||
|
||||
_SERP_SOURCE = Path(serp.__file__)
|
||||
|
||||
|
||||
def test_offers_per_page_matches_live_measurement() -> None:
|
||||
"""60, а не 50 — замер 2026-08-21 через реальный браузер."""
|
||||
assert serp._AVITO_OFFERS_PER_PAGE == 60
|
||||
|
||||
|
||||
def test_pages_needed_arithmetic_uses_the_constant() -> None:
|
||||
"""`ceil(total / PAGE)` при 60 даёт меньше страниц, чем при 50.
|
||||
|
||||
Закрепляет направление эффекта: занижение размера страницы ЗАВЫШАЛО число
|
||||
запрашиваемых страниц. В PR-разборе это было сформулировано наоборот, и
|
||||
тест существует, чтобы неверная трактовка не вернулась.
|
||||
"""
|
||||
total = 1000
|
||||
assert math.ceil(total / 60) == 17
|
||||
assert math.ceil(total / 50) == 20
|
||||
assert math.ceil(total / serp._AVITO_OFFERS_PER_PAGE) == 17
|
||||
|
||||
|
||||
def test_constant_is_still_wired_into_pagination() -> None:
|
||||
"""Константа реально используется, а не осталась мёртвой после правки."""
|
||||
tree = ast.parse(_SERP_SOURCE.read_text(encoding="utf-8"))
|
||||
uses = sum(
|
||||
1
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Name) and node.id == "_AVITO_OFFERS_PER_PAGE"
|
||||
)
|
||||
# 1 присваивание + минимум два места расчёта (pages_needed в бисекции и в
|
||||
# пагинации листа); если станет меньше — кто-то отвязал константу.
|
||||
assert uses >= 3, f"_AVITO_OFFERS_PER_PAGE упоминается {uses} раз, ожидалось >= 3"
|
||||
|
||||
|
||||
def test_no_stale_fifty_in_page_size_comment() -> None:
|
||||
"""Комментарий рядом с константой не должен утверждать «~50 карточек»."""
|
||||
src = _SERP_SOURCE.read_text(encoding="utf-8")
|
||||
marker = "_AVITO_OFFERS_PER_PAGE = "
|
||||
idx = src.index(marker)
|
||||
preceding = src[max(0, idx - 1200) : idx]
|
||||
assert "~50 карточек" not in preceding
|
||||
|
|
@ -260,8 +260,22 @@ _AVITO_MIN_BRACKET = 50_000
|
|||
# Последний брекет ОТКРЫТ (hi=None → pmax не ставится) — ловит весь хвост люкса
|
||||
# без потолка; он крошечный (avito >50М ≈ 73), пагинируется напрямую без бисекции.
|
||||
_AVITO_PRICE_SEED_BRACKETS: list[tuple[int, int | None]] = get_price_seed_brackets()
|
||||
# Avito SERP показывает ~50 карточек на страницу.
|
||||
_AVITO_OFFERS_PER_PAGE = 50
|
||||
# Avito SERP отдаёт 60 карточек на страницу. Замер живьём 2026-08-21 через
|
||||
# tradein-browser (camoufox, JS исполняется): 59-60 уникальных `data-item-id`
|
||||
# на странице выдачи. «Лишние» сверх 50 — обычные объявления с платным
|
||||
# продвижением (`vas-icon_type-promoted`), они лежат в том же списке под
|
||||
# `page-title/count`, а не отдельным рекламным блоком, и собираются наравне.
|
||||
#
|
||||
# Константа стояла 50 и участвует ТОЛЬКО в `ceil(total / PAGE)` — то есть
|
||||
# занижение размера страницы ЗАВЫШАЛО расчётное число страниц, а не занижало:
|
||||
# - запрашивали примерно на 17 % страниц больше, чем нужно (лишние запросы,
|
||||
# лишняя экспозиция под SERP firewall — при нашей доле банов это и есть
|
||||
# основная цена ошибки);
|
||||
# - `tail_loss` считался как `total - cap * 50` и завышал потерю;
|
||||
# - `complete` (пагинация листа) чаще ложно показывал «неполно».
|
||||
# Тихой потери данных не было: условия «страница вернула меньше PAGE карточек,
|
||||
# значит последняя» в коде нет — пагинация ограничена только `max_pages`.
|
||||
_AVITO_OFFERS_PER_PAGE = 60
|
||||
|
||||
|
||||
def _avito_bisection_config(cap: int) -> BisectionConfig:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue