Карточки Циана доставались побочным эффектом cian_history_backfill, а её
выборка ключуется по offer_price_history. Следствия на 30.08: карточка есть
у 4494 из 25222 объявлений (17.8% — последнее место при втором месте по
объёму), 19046 без истории при квоте 100/сутки (190 дней на остаток, тогда
как очередь растёт вдвадцатеро быстрее), и 1697 объявлений с историей и без
карточки, которые исторической выборке недостижимы в принципе.
Фетчер при этом исправен: прогоны 5154/5240/5328 дали 100/100, 99/100,
100/100. Чинить нечего — не выдана мощность.
Добавлен второй режим выборки (listings_pending="detail", по
detail_enriched_at, свежие первыми) и второе расписание поверх ТОГО ЖЕ тела:
машинерия работает, дублировать её новым модулем незачем. Историческая
выборка оставлена побайтово — по ней живёт суточный прогон.
batch_size=400 не на глаз: замеренный темп ~28с на объявление, порог
reap_zombies 6ч по heartbeat, бюджетного сторожа у задачи нет — 400×28с≈3.1ч
проходит, 800 как у Яндекса (≈6.2ч) убивало бы жнецом.
Расписание засеяно enabled=false, как domclick_detail_backfill в миграции
175: это третий круглосуточный добор на общий пул из четырёх узлов, влияние
на соседей надо посмотреть, а не предположить.
Тесты: 13 проверок, ключ выборки / порядок / неизменность прежнего режима /
проводка параметров через посредника / регистрация обоих source. Проверено
мутациями: снятие ORDER BY, молчаливый дефолт вместо ValueError и потеря
listings_pending в посреднике роняют по 2-3 теста каждая. Набор целиком —
5196 passed, 37 skipped.
Сайдкар вообще не читал код ответа page.goto: страница классифицировалась
только по маркерам, снятым с Авито. Домклик отдаёт статическую `403 | Домклик`
на 26 624 байта, где нет ни одного такого маркера (замер прода 28.08.2026) —
она уезжала наверх как валидный HTML, парсер не находил состояние, и прогон
получал блок неизвестной природы. За 14 дней все 14 прогонов домклика легли с
ban_kind='unknown'; у Яндекса счётчика blocked не было вовсе, поэтому ветка
перевода прогона в 'banned' была недостижима по построению — ноль банов.
- browser/server.py: статус целевой навигации сохраняется per-provider и
доезжает в тело /fetch аддитивным ключом "status" (ключ "html" не тронут);
403/429 с маркерами челленджа больше не ждут PoW — ждать нечего, статическая
страница сама себя не перезагрузит. Наверх идёт BanPageDetectedError, а не
заглушка: вернув её контентом, воскресили бы #3045.
- scraper_kit/browser_fetcher.py: BrowserFetcher.last_response_status +
ban_kind_from_status (403/429 → platform, 5xx → infra, прочее → None).
Поток управления не менялся: fetch() по-прежнему отдаёт str.
- domclick: DomClickBlockedError несёт .status — один тип исключения на
маркер-детект и на сбой фетча разводится без размножения типов; прогон
передаёт перепись диагнозов в mark_backfill_finished.
- yandex: появился счётчик blocked, оживляющий ветку бана. Серии блоков и
промахов парсера считаются РАЗДЕЛЬНО: иначе четыре промаха плюс один 403
пятым давали 'banned' с переписью {platform: 1}.
- cian: ban_kinds наполняется только диагностируемым статусом. HTTP 200 с
пустым разбором — дрейф разметки на нашей стороне, а не отказ площадки;
записав его блоком, мы бы штамповали фиктивные баны у здорового источника
(13 done против 1 banned за 14 дней).
Инвариант: непустой ban_kinds ⟺ виден ответ 403/429/5xx. Значения остаются в
пределах CHECK scrape_runs.ban_kind.
Известный пробел: шов providers/domclick/detail.py `blocked.status = status`
тестами не покрыт — существующие домкликовые тесты подают исключение готовым
моком и боевой fetch_detail не исполняют.
Migrates legacy app.services.scrapers.* imports to scraper_kit equivalents for
house_imv_backfill.py, avito_detail_backfill.py, cian_history_backfill.py,
ekb_geoportal_ingest.py, and yandex_detail_backfill.py, proving parity via
tests/support/parity.assert_parity per the epic's gate (#2304).
newbuilding_enrich_backfill.py and yandex_newbuilding_sweep.py are left fully
on legacy imports: both call into scraper_kit.providers.{cian,yandex}.newbuilding,
which construct BrowserFetcher(source=...) without the now-mandatory endpoint=
kwarg (issue #2322, verified still open against the actual provider source, not
just issue status) -- no caller-side fix is possible, matching Group A's (#2305)
precedent for the same bug in admin.py.
Config-gated kit function footguns found and fixed (Group B #2306 pattern):
- avito fetch_detail's backconnect-on-403 retry silently drops when config=
is omitted -- now passes config=RealScraperConfig() explicitly, with a
regression test proving the gate.
- kit AvitoScraper's constructor now requires ScraperConfig positionally --
wired via RealScraperConfig(), which _rotate_ip() reads for
avito_proxy_rotate_url.
- BrowserFetcher(source=...) call sites (house_imv_backfill, avito/cian
detail backfills) now pass the mandatory endpoint=settings.browser_http_endpoint.
New footgun discovered (NOT #2322, flagged for follow-up): kit's
build_warmed_session() builds its curl_cffi session via _build_detail_session()
with no config parameter at all, unlike fetch_detail -- migrating it would
silently drop the sticky MGTS-proxy egress on avito_detail_backfill's
warm-batch path (the prod default). Left build_warmed_session/
_AVITO_WARM_SEARCH_URL on legacy imports, documented inline.
Refs #2310
Route each scraper source (avito/cian/yandex/domclick) to its own camoufox
browser+proxy so they no longer wedge each other through a single global egress.
Feature-flagged (FEATURE_BROWSER_POOL_ENABLED, default OFF): with the flag off the
/fetch and /login paths are byte-for-byte the existing single-browser behavior.
When on, /fetch routes by body["source"] to a per-proxy browser via BROWSER_PROXY_MAP
(BROWSER_PROXY_AVITO/CIAN/YANDEX/DOMCLICK with legacy fallbacks), each guarded by its
own lazy-launched lock. /login stays single-browser in Phase 1.
BrowserFetcher gains a source arg (default avito) and sends it in the /fetch body;
all scraper callsites pass their source. No docker-compose/.env.runtime changes
(Phase 2, owner-gated).