feat(tradein): avito exhaustive full-load (room×price bisection) — mirror cian #1753

Merged
lekss361 merged 1 commit from feat/avito-exhaustive-fullload into main 2026-06-18 11:05:27 +00:00
Owner

Проблема

Avito вторичка катастрофически недобрана: 643 active из 15918 total (4%). Anchor/citywide-стратегия упирается в Avito SERP-cap ~5000 (100 стр × 50) и теряет хвост. Эталон полноты — cian/yandex с exhaustive-бисекцией КОМНАТНОСТЬ×ЦЕНА (cian #926/#929/#930/#938, yandex). У avito такого загрузчика НЕ было.

Решение

Зеркало cian run_cian_full_load для avito: рекурсивная адаптивная бинарная бисекция ценового диапазона внутри каждой комнатности → полное покрытие citywide без anchor-мозаики и SERP-cap.

Эмпирически подтверждено (зонд живого avito через прод camoufox-пул, apw-egress):

  • price-URL pmin/pmax работают (фильтр 3-4 млн → 1178 результатов);
  • total-счётчик для gate бисекции: data-marker="page-title/count" span (1 178 → 1178) — аналог cian totalOffers, надёжен → fixed-grid fallback не нужен;
  • ~50 офферов/стр; segment тегируется DOM-маркером item-development-name (novostroyki vs vtorichka) → secondary_only работает идентично cian.

Изменения

  • scrapers/avito.pypmin/pmax в _build_rooms_url (backward-compat); _extract_total_count (парс page-title/count, None на captcha); fetch_all_secondary + _walk_price_range (рекурсивная бисекция, leaf parallel-paginate через Semaphore, dedup по source_id, on_bucket incremental callback, skip_buckets resume, secondary_only, IP-rotate+retry на total=None, переиспользует _fetch_serp_html anti-block). Константы _AVITO_MAX_PRICE/_AVITO_MIN_BRACKET/_AVITO_OFFERS_PER_PAGE/_AVITO_DEFAULT_ROOMS (7 комнатностей вкл. студии/своб.планир.).
  • scrape_pipeline.pyAvitoFullLoadCounters + run_avito_full_load (incremental on_bucket save, done_buckets resume, cooperative cancel, heartbeat; AvitoBlockedError/RateLimitedmark_banned с сохранением partial).
  • scheduler.py — import, trigger_avito_full_load_run, dispatch elif source == "avito_full_load", docstring-registry.
  • data/sql/127_scrape_schedules_seed_avito_full_load.sql — idempotent seed (enabled=true, окно 13-15 UTC — не конфликтует на apw-прокси с avito_city_sweep 6-7 / newbuilding 2-5 / detail_backfill 9-12; ON CONFLICT DO UPDATE без затирания enabled).
  • tests/scrapers/test_avito_exhaustive.py (+) — URL pmin/pmax, _extract_total_count (real-span 1\xa0178→1178, captcha→None, 0→0), бисекция (split/paginate/min-bracket-guard/dedup/skip/secondary_only/cancel). tests/test_scheduler.py — avito_full_load dispatch.

Тесты

Full suite (prescribed deselects): 1963 passed, 1 skipped, 0 failed (~20s). Изолированный worktree чист — yandex-WIP-шума нет. Ruff clean.

Прод-смоук после merge

UPDATE scrape_schedules SET next_run_at=now() WHERE source='avito_full_load' → scheduler в tradein-scraper подхватит ≤60с → сравнить avito vtorichka active ДО (643) / ПОСЛЕ (ожидается десятки тысяч). Сверить с эталоном cian full_load (#46: 29511 уникальных за 25 мин).

## Проблема Avito вторичка катастрофически недобрана: **643 active из 15918 total (4%)**. Anchor/citywide-стратегия упирается в Avito SERP-cap ~5000 (100 стр × 50) и теряет хвост. Эталон полноты — cian/yandex с exhaustive-бисекцией КОМНАТНОСТЬ×ЦЕНА (cian #926/#929/#930/#938, yandex). У avito такого загрузчика НЕ было. ## Решение Зеркало cian `run_cian_full_load` для avito: рекурсивная адаптивная бинарная бисекция ценового диапазона внутри каждой комнатности → полное покрытие citywide без anchor-мозаики и SERP-cap. **Эмпирически подтверждено** (зонд живого avito через прод camoufox-пул, apw-egress): - price-URL `pmin`/`pmax` работают (фильтр 3-4 млн → 1178 результатов); - total-счётчик для gate бисекции: `data-marker="page-title/count"` span (`1 178` → 1178) — аналог cian `totalOffers`, надёжен → fixed-grid fallback не нужен; - ~50 офферов/стр; segment тегируется DOM-маркером `item-development-name` (novostroyki vs vtorichka) → `secondary_only` работает идентично cian. ## Изменения - `scrapers/avito.py` — `pmin`/`pmax` в `_build_rooms_url` (backward-compat); `_extract_total_count` (парс `page-title/count`, None на captcha); `fetch_all_secondary` + `_walk_price_range` (рекурсивная бисекция, leaf parallel-paginate через Semaphore, dedup по source_id, on_bucket incremental callback, skip_buckets resume, secondary_only, IP-rotate+retry на total=None, переиспользует `_fetch_serp_html` anti-block). Константы `_AVITO_MAX_PRICE`/`_AVITO_MIN_BRACKET`/`_AVITO_OFFERS_PER_PAGE`/`_AVITO_DEFAULT_ROOMS` (7 комнатностей вкл. студии/своб.планир.). - `scrape_pipeline.py` — `AvitoFullLoadCounters` + `run_avito_full_load` (incremental on_bucket save, done_buckets resume, cooperative cancel, heartbeat; `AvitoBlockedError`/`RateLimited` → `mark_banned` с сохранением partial). - `scheduler.py` — import, `trigger_avito_full_load_run`, dispatch `elif source == "avito_full_load"`, docstring-registry. - `data/sql/127_scrape_schedules_seed_avito_full_load.sql` — idempotent seed (enabled=true, окно 13-15 UTC — не конфликтует на apw-прокси с avito_city_sweep 6-7 / newbuilding 2-5 / detail_backfill 9-12; `ON CONFLICT DO UPDATE` без затирания enabled). - `tests/scrapers/test_avito_exhaustive.py` (+) — URL pmin/pmax, `_extract_total_count` (real-span `1\xa0178`→1178, captcha→None, 0→0), бисекция (split/paginate/min-bracket-guard/dedup/skip/secondary_only/cancel). `tests/test_scheduler.py` — avito_full_load dispatch. ## Тесты Full suite (prescribed deselects): **1963 passed, 1 skipped, 0 failed** (~20s). Изолированный worktree чист — yandex-WIP-шума нет. Ruff clean. ## Прод-смоук после merge `UPDATE scrape_schedules SET next_run_at=now() WHERE source='avito_full_load'` → scheduler в `tradein-scraper` подхватит ≤60с → сравнить avito vtorichka active ДО (643) / ПОСЛЕ (ожидается десятки тысяч). Сверить с эталоном cian full_load (#46: 29511 уникальных за 25 мин).
lekss361 added 1 commit 2026-06-18 10:56:32 +00:00
feat(tradein): avito exhaustive full-load (room×price bisection) — mirror cian
All checks were successful
CI / changes (pull_request) Successful in 6s
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
1ddbf867f8
lekss361 added the
enhancement
priority/p1
scope/backend
scope/db
scrapers
status/review
labels 2026-06-18 10:56:36 +00:00
bot-reviewer approved these changes 2026-06-18 11:05:21 +00:00
bot-reviewer left a comment
Collaborator

APPROVE — reviewed on clean worktree at head 1ddbf867 (мирроринг только что смерженного cian #1752).

Проверено (7 концернов):

  1. _walk_price_range — split [lo,mid]/[mid+1,hi] непрерывен, без off-by-one/gap/overlap (целочисленные границы, mid+1 contiguous). page=1 переиспользует probe-HTML. MIN_BRACKET-guard (need_split & too_narrow → paginate as-is) исключает бесконечную рекурсию. dedup по source_id/source_url в seen.
  2. _extract_total_count — regex page-title/count → strip  / /пробелы → int; None на captcha/missing/пусто/non-int → rotate IP + 1 retry → при повторном None skip bucket (return, не падение, не runaway).
  3. secondary_only — фильтрует ТОЛЬКО listing_segment=="novostroyki", вторичку оставляет. До дедупа и on_bucket.
  4. anti-block — gather(return_exceptions=True), затем AvitoBlockedError/AvitoRateLimitedError из результатов re-raise (не глотаются) → run_avito_full_load ловит → mark_banned с partial (on_bucket уже закоммитил прошлые бакеты). probe total=None обрабатывается отдельно (rotate/skip).
  5. concurrency=5 на apw — tuning-риск, не дефект. Митигировано request_delay_sec=7.0 (консервативнее cian 4.0) + mark_banned-on-block + on_bucket incremental save. Watch-item на смоуке (apw-блоки в tradein-browser); при блоках снизить concurrency в default_params — без правки кода.
  6. SQL 127 — номер свободен (на main после #1752 максимум 126); idempotent ON CONFLICT DO UPDATE НЕ затирает enabled. Окно 13-15 UTC отделено от newbuilding(2-5)/city_sweep+deactivate(6-7)/detail_backfill(9-12) на shared apw.
  7. _build_rooms_url — pmin/pmax добавляются только при не-None; существующий вызов fetch_by_rooms (avito.py:1070) без цен → URL не меняется.

trigger_avito_full_load_run — зеркало cian: _claim_run advisory-xact-lock (дубль-ран исключён), все 4 параметра проброшены, resume_run_id=None, dispatch elif корректен.

Тесты на чистом дереве: 1963 passed, 1 skipped, 0 failed (2 cache-теста deselected per gate). Ruff clean. yandex-WIP-шума нет.

Merge (merge style).

✅ APPROVE — reviewed on clean worktree at head 1ddbf867 (мирроринг только что смерженного cian #1752). Проверено (7 концернов): 1. _walk_price_range — split [lo,mid]/[mid+1,hi] непрерывен, без off-by-one/gap/overlap (целочисленные границы, mid+1 contiguous). page=1 переиспользует probe-HTML. MIN_BRACKET-guard (need_split & too_narrow → paginate as-is) исключает бесконечную рекурсию. dedup по source_id/source_url в seen. 2. _extract_total_count — regex page-title/count → strip  / /пробелы → int; None на captcha/missing/пусто/non-int → rotate IP + 1 retry → при повторном None skip bucket (return, не падение, не runaway). 3. secondary_only — фильтрует ТОЛЬКО listing_segment=="novostroyki", вторичку оставляет. До дедупа и on_bucket. 4. anti-block — gather(return_exceptions=True), затем AvitoBlockedError/AvitoRateLimitedError из результатов re-raise (не глотаются) → run_avito_full_load ловит → mark_banned с partial (on_bucket уже закоммитил прошлые бакеты). probe total=None обрабатывается отдельно (rotate/skip). 5. concurrency=5 на apw — tuning-риск, не дефект. Митигировано request_delay_sec=7.0 (консервативнее cian 4.0) + mark_banned-on-block + on_bucket incremental save. Watch-item на смоуке (apw-блоки в tradein-browser); при блоках снизить concurrency в default_params — без правки кода. 6. SQL 127 — номер свободен (на main после #1752 максимум 126); idempotent ON CONFLICT DO UPDATE НЕ затирает enabled. Окно 13-15 UTC отделено от newbuilding(2-5)/city_sweep+deactivate(6-7)/detail_backfill(9-12) на shared apw. 7. _build_rooms_url — pmin/pmax добавляются только при не-None; существующий вызов fetch_by_rooms (avito.py:1070) без цен → URL не меняется. trigger_avito_full_load_run — зеркало cian: _claim_run advisory-xact-lock (дубль-ран исключён), все 4 параметра проброшены, resume_run_id=None, dispatch elif корректен. Тесты на чистом дереве: 1963 passed, 1 skipped, 0 failed (2 cache-теста deselected per gate). Ruff clean. yandex-WIP-шума нет. Merge (merge style).
lekss361 merged commit 00f6464f54 into main 2026-06-18 11:05:27 +00:00
lekss361 deleted branch feat/avito-exhaustive-fullload 2026-06-18 11:05:27 +00:00
Sign in to join this conversation.
No reviewers
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#1753
No description provided.