Авито: у площадки есть родной фильтр вторички — качаем впятеро больше, чем нужно #3033

Closed
opened 2026-08-21 13:30:55 +00:00 by lekss361 · 4 comments
Owner

Найдено 2026-08-21 сверкой живого браузера со скраппером. Разбор целиком — в волте, research/ (запись «Авито: разбор живого браузера против скраппера»).

Находка

Выдача Слаг Объявлений в ЕКБ
Общая — то, что качаем сейчас prodam-ASgBAgICAUSSA8YQ 46 573
Вторичка — родной фильтр prodam/vtorichka-ASgBAgICAkSSA8YQ5geMUg 9 612
1-комн. вторичка prodam/1-komnatnye/vtorichka-ASgBAgICA0SSA8YQ5geMUsoIgFk 2 551

Вторичка — 20,6% общей выдачи. serp.py:1059 fetch_all_secondary(secondary_only=True) тянет всё и отбрасывает новостройки после парсинга, по listing_segment=="novostroyki".

Слаг был под носом: в коде уже есть NOVOSTROYKA_SLUG = "novostroyka-ASgBAgICAkSSA8YQ5geOUg", вторичка отличается одним символом5geOUg5geMUg, соседние значения того же enum.

Декодировать слаги не нужно

Проверено вживую: заход на /ekaterinburg/kvartiry/prodam/1-komnatnye/vtorichkaбез бинарного хвоста — редиректит на канонический URL и отдаёт правильную выдачу (2 551). Фильтры складываются читаемыми сегментами пути, Авито канонизирует сам.

Это снимает главную сложность: не нужно ни реверсить protobuf, ни собирать комбинированные слаги руками. Достаточно строить путь как /{city}/kvartiry/prodam/{room_slug_readable}/vtorichka.

Зачем

  1. Впятеро меньше страниц → впятеро меньше шансов словить SERP firewall. За 21 день из 12 прогонов avito_city_sweep: 5 done, 4 banned, 3 failed. Для источника с такой стабильностью сокращение объёма может дать больше, чем правки отпечатка.
  2. Впятеро быстрее полный проход.
  3. Московская арифметика. Множитель ×6,55 начинает считаться от 20% выдачи, а не от 100%.

Побочно чинится калибровка ценовых брекетов

11 брекетов в scraper_kit/price_brackets.py описаны как «data-driven по прод-распределению вторички ЕКБ (пик 4-8М, bulk 3-16М)», но применяются к смешанной выдаче, где 79% — новостройки с другим распределением цен. Бисекция сейчас калибрована не на те данные: где новостройки набиваются плотно, брекет чаще пробивает cap и требует лишних делений.

После фильтра брекеты начнут применяться к тому распределению, из которого выведены.

Заодно проверить: 50 или 60 карточек на страницу

serp.py: _AVITO_OFFERS_PER_PAGE = 50. На живой странице насчитано 60 элементов [data-marker="item"] — возможно, с промо-блоками. Если 60 реальны, арифметика пагинации и проверок cap занижена на 20%. Проверить в рамках этой же задачи, раз всё равно трогаем пагинацию.

Приёмка

  • Свип и exhaustive-загрузка ходят по vtorichka-пути, комбинируя с комнатностью
  • secondary_only-фильтрация после парсинга снята или оставлена только страховкой
  • Замер: страниц за полный проход до и после, доля banned прогонов за неделю до и после
  • Выяснено, 50 или 60 карточек на странице; константа приведена в соответствие
  • Проверено, что областные города (nizhniy_tagil, kamensk_uralskiy, pervouralsk, verkhnyaya_pyshma, serov) принимают тот же путь

Scope: packages/scraper-kit/src/scraper_kit/providers/avito/serp.py.

Найдено 2026-08-21 сверкой живого браузера со скраппером. Разбор целиком — в волте, `research/` (запись «Авито: разбор живого браузера против скраппера»). ## Находка | Выдача | Слаг | Объявлений в ЕКБ | |---|---|---| | Общая — **то, что качаем сейчас** | `prodam-ASgBAgICAUSSA8YQ` | **46 573** | | Вторичка — родной фильтр | `prodam/vtorichka-ASgBAgICAkSSA8YQ5geMUg` | **9 612** | | 1-комн. вторичка | `prodam/1-komnatnye/vtorichka-ASgBAgICA0SSA8YQ5geMUsoIgFk` | **2 551** | Вторичка — **20,6%** общей выдачи. `serp.py:1059` `fetch_all_secondary(secondary_only=True)` тянет всё и отбрасывает новостройки **после парсинга**, по `listing_segment=="novostroyki"`. Слаг был под носом: в коде уже есть `NOVOSTROYKA_SLUG = "novostroyka-ASgBAgICAkSSA8YQ5geOUg"`, вторичка отличается **одним символом** — `5geOUg` → `5geMUg`, соседние значения того же enum. ## Декодировать слаги не нужно Проверено вживую: заход на `/ekaterinburg/kvartiry/prodam/1-komnatnye/vtorichka` — **без бинарного хвоста** — редиректит на канонический URL и отдаёт правильную выдачу (2 551). Фильтры складываются читаемыми сегментами пути, Авито канонизирует сам. Это снимает главную сложность: не нужно ни реверсить protobuf, ни собирать комбинированные слаги руками. Достаточно строить путь как `/{city}/kvartiry/prodam/{room_slug_readable}/vtorichka`. ## Зачем 1. **Впятеро меньше страниц → впятеро меньше шансов словить SERP firewall.** За 21 день из 12 прогонов `avito_city_sweep`: 5 `done`, 4 `banned`, 3 `failed`. Для источника с такой стабильностью сокращение объёма может дать больше, чем правки отпечатка. 2. **Впятеро быстрее полный проход.** 3. **Московская арифметика.** Множитель ×6,55 начинает считаться от 20% выдачи, а не от 100%. ## Побочно чинится калибровка ценовых брекетов 11 брекетов в `scraper_kit/price_brackets.py` описаны как «data-driven по прод-распределению **вторички** ЕКБ (пик 4-8М, bulk 3-16М)», но применяются к смешанной выдаче, где 79% — новостройки с другим распределением цен. Бисекция сейчас калибрована не на те данные: где новостройки набиваются плотно, брекет чаще пробивает cap и требует лишних делений. После фильтра брекеты начнут применяться к тому распределению, из которого выведены. ## Заодно проверить: 50 или 60 карточек на страницу `serp.py`: `_AVITO_OFFERS_PER_PAGE = 50`. На живой странице насчитано **60** элементов `[data-marker="item"]` — возможно, с промо-блоками. Если 60 реальны, арифметика пагинации и проверок cap занижена на 20%. Проверить в рамках этой же задачи, раз всё равно трогаем пагинацию. ## Приёмка - [ ] Свип и exhaustive-загрузка ходят по `vtorichka`-пути, комбинируя с комнатностью - [ ] `secondary_only`-фильтрация после парсинга снята или оставлена только страховкой - [ ] Замер: страниц за полный проход до и после, доля `banned` прогонов за неделю до и после - [ ] Выяснено, 50 или 60 карточек на странице; константа приведена в соответствие - [ ] Проверено, что областные города (`nizhniy_tagil`, `kamensk_uralskiy`, `pervouralsk`, `verkhnyaya_pyshma`, `serov`) принимают тот же путь Scope: `packages/scraper-kit/src/scraper_kit/providers/avito/serp.py`.
lekss361 added the
performance
priority/p1
scope/backend
scrapers
tradein
labels 2026-08-21 13:32:29 +00:00
Collaborator

Сделано в PR #3039: fetch_all_secondary(secondary_only=True) строит читаемый путь /{city}/kvartiry/prodam/{room_readable}/vtorichka (флаг на инстансе на время вызова, сброс в finally), query с pmin/pmax без изменений — бисекция та же, но уже по распределению вторички. Post-parse фильтр оставлен страховкой и счётчиком: dropped_novostroyki теперь пишется в scrape_runs.counters (зеркало cian #1781) — при работающем пути ≈0, рост = Авито перестал уважать путь, и это видно по числу.

Чего не делал и почему:

  • fetch_around (city-sweep по геоточкам) и fetch_by_rooms (вызовов нет) не трогал — постановка про полный обход; city-sweep намеренно берёт оба сегмента.
  • Живая приёмка — только когда Авито пускает (сейчас SERP-баны 37–83 %, #2827/#3034). Критерий: первый avito_full_load после деплоя — unique_fetched ≈ прежний, страниц впятеро меньше, dropped_novostroyki ≈ 0. Если счётчик остаётся в сотнях — редирект ведёт на общую выдачу, откатывать.

Тест без сети (подмена _fetch_serp_html): на main 4/6 красных по значению (в пути ASgB-slug и нет /vtorichka), контроли зелёные.

Сделано в PR #3039: `fetch_all_secondary(secondary_only=True)` строит читаемый путь `/{city}/kvartiry/prodam/{room_readable}/vtorichka` (флаг на инстансе на время вызова, сброс в `finally`), query с `pmin/pmax` без изменений — бисекция та же, но уже по распределению вторички. Post-parse фильтр оставлен страховкой и счётчиком: `dropped_novostroyki` теперь пишется в `scrape_runs.counters` (зеркало cian #1781) — при работающем пути ≈0, рост = Авито перестал уважать путь, и это видно по числу. Чего не делал и почему: - `fetch_around` (city-sweep по геоточкам) и `fetch_by_rooms` (вызовов нет) не трогал — постановка про полный обход; city-sweep намеренно берёт оба сегмента. - Живая приёмка — только когда Авито пускает (сейчас SERP-баны 37–83 %, #2827/#3034). Критерий: первый `avito_full_load` после деплоя — `unique_fetched` ≈ прежний, страниц впятеро меньше, `dropped_novostroyki ≈ 0`. Если счётчик остаётся в сотнях — редирект ведёт на общую выдачу, откатывать. Тест без сети (подмена `_fetch_serp_html`): на main 4/6 красных по значению (в пути ASgB-slug и нет `/vtorichka`), контроли зелёные.
Author
Owner

Проверка на проде после деплоя #3039 — работает, но покрывает половину задачи

Прогон avito_city_sweep запущен вручную сразу после деплоя (7382ae12, образ 14:30). Результат — run 4539:

status=done  total_seen=137  new_count=45  длительность 2,6 мин  ошибок нет

Тихой поломки нет: URL строится корректно, ноль-результата не случилось, объём в историческом коридоре 93–160. Это была главная проверка — при ошибке в пути Авито вернул бы валидную страницу без карточек, и прогон записался бы done с пустым результатом.

Но фильтр не применился к этому свипу

Сегменты того, что он принёс:

listing_segment строк
vtorichka 41
novostroyki 26

Если бы путь вторички работал на этом обходе, новостроек было бы около нуля. Причина видна в счётчиках прогона: anchors_total: 5, anchors_done: 1 — городской свип идёт якорным радиусным путём, а не комнатным.

Проверил по коду в main: фильтр получил только _build_rooms_url. Два других билдера остались на общей выдаче:

Билдер Путь после #3039 Кто использует
_build_rooms_url /prodam/{room}/vtorichka fetch_by_rooms, fetch_all_secondary
_build_web_url (geoCoords+radius) prodam-ASgBAgICAUSSA8YQ avito_city_sweep — якорный обход
_build_citywide_url prodam-ASgBAgICAUSSA8YQ citywide-обход без гео-фильтра

Что это значит по приоритетам

Хорошая новость: правка легла именно туда, где больнее всего. fetch_all_secondary — это avito_full_load и avito_full_load_exhaustive, у которых доля отказов 83% и 67% соответственно (замер в #3034). Пятикратное сокращение объёма бьёт ровно по ним.

Городской свип, оставшийся непокрытым, — самый лёгкий из обходов: 137 объявлений за 2,6 минуты, доля отказов 37,5%. Выигрыш там меньше, но он есть, и это не «доделка ради полноты»: свип ходит ежедневно, и три четверти того, что он качает, — новостройки, которые тут же отбрасываются.

Follow-up

Распространить путь вторички на _build_web_url и _build_citywide_url. Перед этим проверить живьём то же, что проверяли для комнатного пути: принимает ли /prodam/vtorichka параметры geoCoords и radius — вся якорная логика на них держится. Для pmin/pmax совместимость уже подтверждена, так что шансы хорошие, но проверять надо.

Попутно: detail по-прежнему лежит

Из тех же счётчиков: detail_attempted: 8, detail_enriched: 2, detail_failed: 6 — 75% отказов, и явный enrichment_abort_note: «Avito detail firewall/soft-block (browser-mode)». Это #3035, ничего нового, но подтверждает, что смена прокси detail-путь не вылечила — там своя причина.

Refs #3039, #3035, #3034

## Проверка на проде после деплоя #3039 — работает, но покрывает половину задачи Прогон `avito_city_sweep` запущен вручную сразу после деплоя (`7382ae12`, образ 14:30). Результат — run `4539`: ``` status=done total_seen=137 new_count=45 длительность 2,6 мин ошибок нет ``` Тихой поломки нет: URL строится корректно, ноль-результата не случилось, объём в историческом коридоре 93–160. Это была главная проверка — при ошибке в пути Авито вернул бы валидную страницу без карточек, и прогон записался бы `done` с пустым результатом. ## Но фильтр не применился к этому свипу Сегменты того, что он принёс: | `listing_segment` | строк | |---|---| | `vtorichka` | 41 | | **`novostroyki`** | **26** | Если бы путь вторички работал на этом обходе, новостроек было бы около нуля. Причина видна в счётчиках прогона: `anchors_total: 5, anchors_done: 1` — городской свип идёт **якорным радиусным путём**, а не комнатным. Проверил по коду в `main`: фильтр получил только `_build_rooms_url`. Два других билдера остались на общей выдаче: | Билдер | Путь после #3039 | Кто использует | |---|---|---| | `_build_rooms_url` | `/prodam/{room}/vtorichka` ✅ | `fetch_by_rooms`, `fetch_all_secondary` | | `_build_web_url` (`geoCoords`+`radius`) | `prodam-ASgBAgICAUSSA8YQ` ❌ | `avito_city_sweep` — якорный обход | | `_build_citywide_url` | `prodam-ASgBAgICAUSSA8YQ` ❌ | citywide-обход без гео-фильтра | ## Что это значит по приоритетам Хорошая новость: правка легла именно туда, где больнее всего. `fetch_all_secondary` — это `avito_full_load` и `avito_full_load_exhaustive`, у которых доля отказов **83%** и **67%** соответственно (замер в #3034). Пятикратное сокращение объёма бьёт ровно по ним. Городской свип, оставшийся непокрытым, — самый лёгкий из обходов: 137 объявлений за 2,6 минуты, доля отказов 37,5%. Выигрыш там меньше, но он есть, и это не «доделка ради полноты»: свип ходит ежедневно, и три четверти того, что он качает, — новостройки, которые тут же отбрасываются. ## Follow-up Распространить путь вторички на `_build_web_url` и `_build_citywide_url`. Перед этим проверить живьём то же, что проверяли для комнатного пути: принимает ли `/prodam/vtorichka` параметры `geoCoords` и `radius` — вся якорная логика на них держится. Для `pmin`/`pmax` совместимость уже подтверждена, так что шансы хорошие, но проверять надо. ## Попутно: detail по-прежнему лежит Из тех же счётчиков: `detail_attempted: 8, detail_enriched: 2, detail_failed: 6` — 75% отказов, и явный `enrichment_abort_note`: «Avito detail firewall/soft-block (browser-mode)». Это #3035, ничего нового, но подтверждает, что смена прокси detail-путь не вылечила — там своя причина. Refs #3039, #3035, #3034
Collaborator

Follow-up сделан: PR #3042 (якорный fetch_around и fetch_city_wide → путь вторички). Предусловие проверено вживую

Проба 21.08 15:20 UTC (сайдкар, узел 9, один якорь — центр ЕКБ, radius=1):

путь карточек total сегменты
общий prodam-ASgB… (как было) 55 45 663 vtorichka 41 / novostroyki 14
prodam/vtorichka (читаемый) 50 9 071 vtorichka 50
prodam/vtorichka-ASgBAgICAkSSA8YQ5geMUg (канон) 60 9 036 vtorichka 60

geoCoords/radius путь вторички принимает; канонический slug без редиректа — его и взял для путей без комнатности. fetch_around/fetch_city_wide получили secondary_only=True по умолчанию (класс — парсер вторички; новостройки — свой fetch_newbuildings, не тронут), secondary_only=False возвращает общую выдачу. Приёмка: следующий avito_city_sweep (~06:4x UTC) — в его строках listing_segment='novostroyki' = 0.

Но та же проба показала вещь крупнее: гео-параметры сервером не применяются — #3043

Три якоря в 10+ км друг от друга (центр / Уралмаш / Академический) на общем пути дали одну и ту же страницу: пересечение по source_id 55 из 55 и 45 из 45, те же первые адреса; total — городской (45 тыс.), а не «в радиусе 1 км». То есть якорный свип = первые ~3 страницы города, скачанные пять раз (в 4539: anchors_total=5, уникальных 137 из ~750 скачанных). Это не регрессия — общий путь так же; подробности, таблица и варианты решения — в #3043.

Detail (detail_attempted: 8 / enriched: 2) — согласен, это #3035, прокси его не лечат.

## Follow-up сделан: PR #3042 (якорный `fetch_around` и `fetch_city_wide` → путь вторички). Предусловие проверено вживую Проба 21.08 15:20 UTC (сайдкар, узел 9, один якорь — центр ЕКБ, `radius=1`): | путь | карточек | total | сегменты | |---|---:|---:|---| | общий `prodam-ASgB…` (как было) | 55 | 45 663 | vtorichka 41 / novostroyki 14 | | `prodam/vtorichka` (читаемый) | 50 | 9 071 | vtorichka 50 | | `prodam/vtorichka-ASgBAgICAkSSA8YQ5geMUg` (канон) | 60 | 9 036 | vtorichka 60 | `geoCoords`/`radius` путь вторички принимает; канонический slug без редиректа — его и взял для путей без комнатности. `fetch_around`/`fetch_city_wide` получили `secondary_only=True` по умолчанию (класс — парсер вторички; новостройки — свой `fetch_newbuildings`, не тронут), `secondary_only=False` возвращает общую выдачу. Приёмка: следующий `avito_city_sweep` (~06:4x UTC) — в его строках `listing_segment='novostroyki'` = 0. ## Но та же проба показала вещь крупнее: гео-параметры сервером не применяются — #3043 Три якоря в 10+ км друг от друга (центр / Уралмаш / Академический) на общем пути дали **одну и ту же** страницу: пересечение по `source_id` 55 из 55 и 45 из 45, те же первые адреса; `total` — городской (45 тыс.), а не «в радиусе 1 км». То есть якорный свип = первые ~3 страницы города, скачанные пять раз (в 4539: `anchors_total=5`, уникальных 137 из ~750 скачанных). Это не регрессия — общий путь так же; подробности, таблица и варианты решения — в #3043. Detail (`detail_attempted: 8 / enriched: 2`) — согласен, это #3035, прокси его не лечат.
Collaborator

Приёмка на проде — путь вторички применился к якорному свипу

Утренний avito_city_sweep 4591 (22.08 06:01 UTC): status=done, total_seen=160, lots_inserted=17, lots_updated=143. Сегменты строк этого прогона:

listing_segment | count
vtorichka       | 160
novostroyki     | 0

Ровно критерий приёмки (novostroyki=0, vtorichka≥40). До #3042 тот же свип приносил 41 vtorichka + 26 novostroyki (прогон 4539). PR #3039 (комнатный fetch_all_secondary) + #3042 (якорный fetch_around/citywide) — оба на проде, оба пути вторички работают.

Закрываю. avito_full_load*-приёмка (комнатный путь) остаётся отдельной проверкой на 23.08 15:33 UTC — она про объём/долю банов, а не про сегмент. Побочная находка о нерабочих geoCoords/radius вынесена в #3043.

## Приёмка на проде — путь вторички применился к якорному свипу Утренний `avito_city_sweep` **4591** (22.08 06:01 UTC): `status=done`, `total_seen=160`, `lots_inserted=17`, `lots_updated=143`. Сегменты строк этого прогона: ``` listing_segment | count vtorichka | 160 novostroyki | 0 ``` Ровно критерий приёмки (novostroyki=0, vtorichka≥40). До #3042 тот же свип приносил 41 vtorichka + 26 novostroyki (прогон 4539). PR #3039 (комнатный `fetch_all_secondary`) + #3042 (якорный `fetch_around`/citywide) — оба на проде, оба пути вторички работают. Закрываю. `avito_full_load*`-приёмка (комнатный путь) остаётся отдельной проверкой на 23.08 15:33 UTC — она про объём/долю банов, а не про сегмент. Побочная находка о нерабочих `geoCoords/radius` вынесена в #3043.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#3033
No description provided.