feat(tradein/scraper): чекпоинт по якорям для avito_city_sweep (#3074) #3112

Merged
lekss361 merged 1 commit from feat/3074-avito-anchor-checkpoint into main 2026-08-26 12:03:08 +00:00
Owner

Продолжение после yandex-свипа (#3098). Источник выбран по замеру, а не «доделать остальные»:

свип прогонов убито деплоем банов ср. мин макс
yandex_city_sweep 62 8 0 67 395
avito_city_sweep 68 3 25 15 97
cian_city_sweep 65 2 7 35 72
domclick_city_sweep 54 0 17 3 24

domclick исключён осознанно: прогоны по три минуты, деплоем не убит ни разу — чекпоинт там не окупается.

Прод-факт, который решает дело

Типичный итог свипа Авито:

{"anchors_done": 1, "anchors_total": 5, ...,
 "enrichment_abort_note": "detail enrichment aborted (Avito detail firewall/soft-block ...)"}

Прогон срывается блокировкой на первом из пяти якорей. Без чекпоинта следующий прогон снова идёт в первый якорь, упирается в ту же стену — и якоря 2–5 не собираются никогда. Для сравнения, у cian_city_sweep в тех же counters anchors_done: 5 из 5.

Решения и их основания

Ключ чекпоинта — имя якоря, а не индекс. Состав списка зависит от city_slug, позиция в нём между городами не устойчива.

Гарда по числу якорей не нужна — в отличие от combo-чекпоинта яндекса, где combo_label якоря не содержал и пропуск был бы пропуском чужих якорей. Здесь якорь и есть ключ.

Отдельный флаг _anchor_ok. В чекпоинт попадает только якорь, пройденный до конца. Ветка блокировки делает return и до записи не доходит, а except Exceptionдоходит: якорь упал, но цикл продолжается. Записать такой якорь пройденным значило бы, что следующий прогон пропустит его навсегда, причём молча — прогон завершится штатно, просто часть города не соберётся. Тот же инвариант, что у combo_skipped в яндексе, и ровно он остался непокрытым тестом в #3098.

Флаг сбрасывается на каждой итерации — иначе один упавший якорь заразил бы все последующие и чекпоинт перестал бы пополняться вовсе.

Пропущенный якорь двигает anchors_done — чтобы счётчик продолжал означать «докуда дошли по списку», а не «сколько собрал именно этот прогон».

Тесты

tradein-mvp/backend/tests/test_3074_avito_anchor_checkpoint.py, 4 поведенческих — с подменой AvitoScraper, AsyncSession и save_listings:

  1. якорь из чекпоинта не опрашивается вовсе — ни одного обращения к источнику;
  2. пройденный якорь дописывается поверх унаследованных, а не затирает их;
  3. без чекпоинта обходятся все якоря — прежнее поведение цело;
  4. упавший якорь в чекпоинт не попадает, а исправный — попадает.

Оговорка про фальсификацию, честно. На исходном коде все четыре падают с TypeError: unexpected keyword argument 'resume_run_id', то есть доказывают отсутствие параметра, а не неверное поведение. Поведенческую часть держат сами утверждения внутри тестов — обойти их, добавив только параметр, не выйдет.

Весь набор #3074 (этот + яндекс + claim) — 10 passed. ruff чист, в коммите нет CR.

Refs #3074, #3098

Продолжение после yandex-свипа (#3098). Источник выбран **по замеру**, а не «доделать остальные»: | свип | прогонов | убито деплоем | банов | ср. мин | макс | |---|---|---|---|---|---| | yandex_city_sweep | 62 | 8 | 0 | 67 | 395 | ← сделан в #3098 | | **avito_city_sweep** | 68 | 3 | **25** | 15 | 97 | ← этот PR | | cian_city_sweep | 65 | 2 | 7 | 35 | 72 | ← следующий шард | | domclick_city_sweep | 54 | **0** | 17 | **3** | 24 | ← намеренно не трогаю | `domclick` исключён осознанно: прогоны по три минуты, деплоем не убит ни разу — чекпоинт там не окупается. ## Прод-факт, который решает дело Типичный итог свипа Авито: ```json {"anchors_done": 1, "anchors_total": 5, ..., "enrichment_abort_note": "detail enrichment aborted (Avito detail firewall/soft-block ...)"} ``` Прогон срывается блокировкой на **первом из пяти** якорей. Без чекпоинта следующий прогон снова идёт в первый якорь, упирается в ту же стену — и якоря 2–5 не собираются никогда. Для сравнения, у `cian_city_sweep` в тех же counters `anchors_done: 5` из 5. ## Решения и их основания **Ключ чекпоинта — имя якоря, а не индекс.** Состав списка зависит от `city_slug`, позиция в нём между городами не устойчива. **Гарда по числу якорей не нужна** — в отличие от combo-чекпоинта яндекса, где `combo_label` якоря не содержал и пропуск был бы пропуском чужих якорей. Здесь якорь и есть ключ. **Отдельный флаг `_anchor_ok`.** В чекпоинт попадает только якорь, пройденный до конца. Ветка блокировки делает `return` и до записи не доходит, а `except Exception` — **доходит**: якорь упал, но цикл продолжается. Записать такой якорь пройденным значило бы, что следующий прогон пропустит его навсегда, причём **молча** — прогон завершится штатно, просто часть города не соберётся. Тот же инвариант, что у `combo_skipped` в яндексе, и ровно он остался непокрытым тестом в #3098. Флаг сбрасывается на **каждой** итерации — иначе один упавший якорь заразил бы все последующие и чекпоинт перестал бы пополняться вовсе. **Пропущенный якорь двигает `anchors_done`** — чтобы счётчик продолжал означать «докуда дошли по списку», а не «сколько собрал именно этот прогон». ## Тесты `tradein-mvp/backend/tests/test_3074_avito_anchor_checkpoint.py`, 4 поведенческих — с подменой `AvitoScraper`, `AsyncSession` и `save_listings`: 1. якорь из чекпоинта **не опрашивается вовсе** — ни одного обращения к источнику; 2. пройденный якорь дописывается **поверх унаследованных**, а не затирает их; 3. без чекпоинта обходятся все якоря — прежнее поведение цело; 4. **упавший якорь в чекпоинт не попадает**, а исправный — попадает. **Оговорка про фальсификацию, честно.** На исходном коде все четыре падают с `TypeError: unexpected keyword argument 'resume_run_id'`, то есть доказывают отсутствие параметра, а не неверное поведение. Поведенческую часть держат сами утверждения внутри тестов — обойти их, добавив только параметр, не выйдет. Весь набор #3074 (этот + яндекс + claim) — **10 passed**. `ruff` чист, в коммите нет CR. Refs #3074, #3098
lekss361 added 1 commit 2026-08-26 11:53:28 +00:00
feat(tradein/scraper): чекпоинт по якорям для avito_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m47s
0586951772
Продолжение после yandex-свипа. Выбор источника — по замеру, а не «для
полноты»: за 60 дней avito_city_sweep дал 68 прогонов, 25 банов и 3 отмены
деплоем при среднем времени 15 мин и максимуме 97.

Прод-факт, который решает дело. Типичный итог свипа:

    {"anchors_done": 1, "anchors_total": 5, ...,
     "enrichment_abort_note": "detail enrichment aborted (Avito detail
      firewall/soft-block ...)"}

Прогон срывается блокировкой на ПЕРВОМ из пяти якорей. Без чекпоинта
следующий прогон снова идёт в первый якорь, упирается в ту же стену, и якоря
2-5 не собираются никогда.

Ключ чекпоинта — ИМЯ якоря, а не индекс: состав списка зависит от city_slug,
позиция в нём между городами не устойчива. По той же причине здесь не нужна
гарда по числу якорей, которая есть у combo-чекпоинта яндекса: там ключ
якоря не содержал, здесь якорь и есть ключ.

Инвариант, ради которого отдельный флаг _anchor_ok: в чекпоинт попадает
только якорь, пройденный до конца. Ветка блокировки делает return и до записи
не доходит, а generic-except доходит — якорь упал, но цикл продолжается.
Записать такой якорь пройденным значило бы, что следующий прогон пропустит
его навсегда, причём молча: прогон завершится штатно, просто часть города не
соберётся. Флаг сбрасывается на каждой итерации, иначе один упавший якорь
заразил бы все последующие.

Пропущенный якорь двигает anchors_done — чтобы счётчик продолжал означать
«докуда дошли по списку», а не «сколько собрал именно этот прогон».

domclick_city_sweep намеренно НЕ трогаю: 54 прогона, ноль отмен деплоем,
среднее время 3 минуты — чекпоинт там не окупается. cian_city_sweep (среднее
35 мин, 2 отмены) — следующий шард.

Тесты (4) поведенческие: якорь из чекпоинта не опрашивается вовсе; пройденный
дописывается поверх унаследованных; без чекпоинта обходятся все; упавший в
чекпоинт НЕ попадает. Оговорка: на исходном коде они падают по сигнатуре
(unexpected keyword argument), то есть доказывают отсутствие параметра, а не
поведение — поведенческую часть держат сами проверки. Весь набор #3074 —
10 passed.
lekss361 merged commit 17548e9046 into main 2026-08-26 12:03:08 +00:00
lekss361 deleted branch feat/3074-avito-anchor-checkpoint 2026-08-26 12:03:09 +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#3112
No description provided.