feat(tradein/browser): имитация чтения страницы человеком, целиком opt-in (#3283) #3308

Merged
bot-backend merged 1 commit from feat/3283f-sidecar-humanized-reading into main 2026-09-01 07:34:50 +00:00
Owner

Зачем

У сайдкара не было ни одной строки имитации поведения: после page.goto шли только wait_for_timeout и content(). grep -n "scroll\|mouse\|wheel\|go_back" browser/server.py — ноль совпадений.

Контракт

Новое необязательное поле тела POST /fetchbehaviour:

Ключ Что делает
scroll 3-6 прокруток колесом (500-1400px) с паузами 700-1800 мс перед content()
dwell_ms [min, max] дополнительной случайной паузы; невалидное → 400, как у fetch_mode
hop_recommended заход на одну соседнюю карточку того же хоста; результат наружу не идёт, отказ глотается, отдельный короткий таймаут, работает только при reuse_context=True

Поля нет → не исполняется ни одна новая строка.

Почему opt-in, а не «включить и радоваться»

Вклад поведения в выход не доказан. Замер 31.08 сравнивал «без ротации и без поведения» против «ротация + поведение» — две переменные сразу. Ротацию мы с тех пор выкатили отдельно, и сама по себе она выход не подняла: 53% блоков до неё, 52% после. Прежде чем включать поведение навсегда, надо померить его одно — а для этого дефолт обязан остаться прежним.

Отбор ссылок для hop — две ступени

CSS-селектор сужает раздел, шаблон URL отличает карточку от рубрики и футера. Без второй ступени a[href] на карточке Авито увёл бы на главную, и след потерял бы смысл.

  • Авито: a[href*="/kvartiry/"] + _\d{7,}(?:\?|$) — шаблон снят на живом замере 31.08.
  • Домклик: a[href*="/card/"] + /card/sale__flat__\d+ — по логам сайдкара.
  • cian / yandex / generic — шаблона нет осознанно: их вёрстку я не снимал, а выдуманный шаблон хуже отсутствующего. При None сужение не применяется, поведение прежнее.

Джиттер пейсинга

BROWSER_PAGE_INTERVAL_JITTER_S с дефолтом 0. В отличие от остального, пейсинг телом запроса не гейтится, поэтому ненулевой дефолт сломал бы ту же измеримость. uniform(0,0) == 0.0 — интервал численно прежний, но крутилка (env) есть.

Проверка

231 passed — набор сайдкара, запущенный как в CI (pytest + aiohttp, без backend-венва).

Покрыто: отсутствие behaviour → ни прокрутки, ни dwell, ни хопа; scroll → вызовы mouse.wheel в заданном диапазоне; 5 вариантов невалидного dwell_ms и невалидный тип behaviour400; хоп только на свой хост; хоп не берёт рубрику того же хоста (вторая ступень); отказ хопа не ломает основной ответ; без reuse_context хопа нет.

Что дальше

Проводка со стороны таска (параметры расписания, разнообразие якорной выдачи) — отдельным PR, вместе с правкой порядка «ротация до бана».

## Зачем У сайдкара не было **ни одной** строки имитации поведения: после `page.goto` шли только `wait_for_timeout` и `content()`. `grep -n "scroll\|mouse\|wheel\|go_back" browser/server.py` — ноль совпадений. ## Контракт Новое **необязательное** поле тела `POST /fetch` — `behaviour`: | Ключ | Что делает | |---|---| | `scroll` | 3-6 прокруток колесом (500-1400px) с паузами 700-1800 мс перед `content()` | | `dwell_ms` | `[min, max]` дополнительной случайной паузы; невалидное → `400`, как у `fetch_mode` | | `hop_recommended` | заход на **одну** соседнюю карточку того же хоста; результат наружу не идёт, отказ глотается, отдельный короткий таймаут, работает только при `reuse_context=True` | **Поля нет → не исполняется ни одна новая строка.** ## Почему opt-in, а не «включить и радоваться» Вклад поведения в выход **не доказан**. Замер 31.08 сравнивал «без ротации и без поведения» против «ротация + поведение» — две переменные сразу. Ротацию мы с тех пор выкатили отдельно, и сама по себе она выход не подняла: 53% блоков до неё, 52% после. Прежде чем включать поведение навсегда, надо померить его одно — а для этого дефолт обязан остаться прежним. ## Отбор ссылок для hop — две ступени CSS-селектор сужает раздел, шаблон URL отличает карточку от рубрики и футера. Без второй ступени `a[href]` на карточке Авито увёл бы на главную, и след потерял бы смысл. - Авито: `a[href*="/kvartiry/"]` + `_\d{7,}(?:\?|$)` — шаблон снят на живом замере 31.08. - Домклик: `a[href*="/card/"]` + `/card/sale__flat__\d+` — по логам сайдкара. - cian / yandex / generic — шаблона **нет осознанно**: их вёрстку я не снимал, а выдуманный шаблон хуже отсутствующего. При `None` сужение не применяется, поведение прежнее. ## Джиттер пейсинга `BROWSER_PAGE_INTERVAL_JITTER_S` с дефолтом **0**. В отличие от остального, пейсинг телом запроса не гейтится, поэтому ненулевой дефолт сломал бы ту же измеримость. `uniform(0,0) == 0.0` — интервал численно прежний, но крутилка (env) есть. ## Проверка `231 passed` — набор сайдкара, запущенный как в CI (`pytest + aiohttp`, без backend-венва). Покрыто: отсутствие `behaviour` → ни прокрутки, ни dwell, ни хопа; `scroll` → вызовы `mouse.wheel` в заданном диапазоне; 5 вариантов невалидного `dwell_ms` и невалидный тип `behaviour` → `400`; хоп только на свой хост; **хоп не берёт рубрику того же хоста** (вторая ступень); отказ хопа не ломает основной ответ; без `reuse_context` хопа нет. ## Что дальше Проводка со стороны таска (параметры расписания, разнообразие якорной выдачи) — отдельным PR, вместе с правкой порядка «ротация до бана».
lekss361 added 1 commit 2026-09-01 06:49:10 +00:00
feat(tradein/browser): имитация чтения страницы человеком, целиком opt-in (#3283)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 / browser-tests (pull_request) Successful in 1m26s
8bb20c05ea
У сайдкара не было ни одной строки имитации поведения: после page.goto шли
только wait_for_timeout и content(). grep по scroll/mouse/wheel/go_back давал
ноль совпадений.

Новое НЕОБЯЗАТЕЛЬНОЕ поле тела POST /fetch — behaviour:
  scroll         — 3-6 прокруток колесом с паузами 700-1800мс перед content();
  dwell_ms       — [min,max] дополнительной случайной паузы;
  hop_recommended — заход на ОДНУ соседнюю карточку того же хоста, результат
                   наружу не идёт, отказ глотается, только при reuse_context.

Поле отсутствует → не исполняется ни одна новая строка. Это осознанно: вклад
поведения в выход НЕ доказан. Замер 31.08 сравнивал «без ротации и без
поведения» против «ротация + поведение», то есть две переменные сразу; ротацию
мы выкатили отдельно, и сама по себе она выход не подняла (53% блоков до, 52%
после). Прежде чем включать поведение навсегда, его надо померить одно — а для
этого дефолт обязан остаться прежним.

Ссылки для hop отбираются в две ступени: CSS-селектор сужает раздел, шаблон URL
отличает карточку от рубрики и футера. Без второй ступени "a[href]" на карточке
Авито увёл бы на главную. Шаблон Авито (путь с /kvartiry/ и id из 7+ цифр) снят
на живом замере 31.08; Домклик — /card/sale__flat__<id> по логам сайдкара.
cian/yandex/generic шаблона не имеют осознанно: их вёрстку я не снимал, а
выдуманный шаблон хуже отсутствующего — при None сужение не применяется.

BROWSER_PAGE_INTERVAL_JITTER_S с дефолтом 0: джиттер пейсинга, в отличие от
остального, телом запроса не гейтится, поэтому ненулевой дефолт сломал бы ту же
измеримость. uniform(0,0)==0.0 — интервал численно прежний, но крутилка есть.

Тесты сайдкара: 231 passed.
bot-backend merged commit 7daeaa4b59 into main 2026-09-01 07:34:50 +00:00
bot-backend deleted branch feat/3283f-sidecar-humanized-reading 2026-09-01 07:34:50 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3308
No description provided.