fix(cian): капча/страница ошибки Циана (HTTP 200) — отказ площадки в сайдкаре и kit, ротация аренды внутри батча (#3402) #3403

Merged
bot-backend merged 2 commits from fix/cian-captcha-is-platform-refusal into main 2026-09-06 10:01:30 +00:00
Collaborator

Closes #3402.

Дефект. Циан отдаёт капчу (Captcha - база объявлений ЦИАН, 44 КБ) и «Ошибка - Циан» с HTTP 200; детектор бан-страниц сайдкара их не знал → HTML уходил как успех → extract_state(... "defaultState") → None → «defaultState extraction failed» → cian_history_backfill (= cian_detail_backfill) держал одну аренду на весь батч и сжигал 210–400 карточек через капча-узел (6200: 0/210; шесть прогонов 04–05.09: 0/400; через здоровый узел 13: 161/162). Проба одной карточки по узлам: 13 — карточка, 14 — капча, 1 — «Ошибка». База: 14 028 из 20 451 активных cian без detail.

Слой 1 — сайдкар (browser/server.py:1978-1994, хуки :2374, :2417): _CIAN_REFUSAL_TITLES по <title> (нормализация пробелов/регистра/тире), per-provider cianBanPageDetectedError → существующий путь (#3379): 403 + ban_page: true + status: 200 (upstream честно). Слово captcha в нормальной карточке (×11) не триггер — только title.

Слой 2 — kit (providers/cian/detail.py:109-117, :243-256, :170-181; образы деплоятся раздельно): при провале extract_state с опознанным title → report_platform_ban по живой аренде + raise CianBlockedError (лог «капча/страница ошибки Циана (title=…)»); без маркеров — прежний None; except SidecarBanPageError перед общим except — отказ, опознанный сайдкаром, больше не гасится в None (иначе ротация чинилась бы, а прогон рапортовал «0/400 done»).

Ротация внутри батча — правок в cian_history_backfill не потребовалось: report_platform_ban (browser_fetcher.py:886, публичное имя вместо _report_platform_ban — переименованы 2 внутренних вызова + комментарии) → report_ban (mark_banned пары узел×cian, acquire("cian") его больше не выдаст) + _report_fetch_result(False, health=False) → на 3-й капче _LEASE_ROTATE_AFTER_FAILS_acquire_lease() (:861-884), следующий /fetch с нового узла.

Тесты: browser/test_server_cian_captcha.py (7: маркеры, «captcha в карточке ≠ бан», тире, _fetch_onceBanPageDetectedError, fetch_handler → 403+ban_page+status:200, чужой провайдер не банится); tests/test_3402_cian_captcha_http200.py (капча/ошибка → CianBlockedError + рапорт; HTML без state и маркеров → None; батч через настоящий fetch_detail + фейковый фетчер: капча на первом узле → рапорт, второй листинг обогащён).

Фальсификация (git apply -R kit-файлов): 3 failed — оба листинга сожжены на одном узле, капча не отрапортована пулу — узел доработает батч, в логе прежние «extraction failed». Восстановлено.

Прогоны: backend 5598 passed, 35 skipped (rc=0); сайдкар 241 passed (rc=0); ruff OK (server.py вне скоупа хуков, не форматировался).

Приёмка на проде (после ОБОИХ образов): следующий cian_detail_backfilllistings_succeeded > 0; в логах капча-узла lease id=N (cian) BANNED — reporting to pool: … капча/страница ошибки Циана и при трёх подряд провалил 3 /fetch подряд — меняем прокси; «defaultState extraction failed» на капче — 0; строка в scrape_proxy_source_bans для пары узел×cian. Не в PR: ban_kind для капчи остаётся None (_note_refusal судит по статусу, тут честный 200) — решение уровня #3196.

Closes #3402. **Дефект.** Циан отдаёт капчу (`Captcha - база объявлений ЦИАН`, 44 КБ) и «Ошибка - Циан» с **HTTP 200**; детектор бан-страниц сайдкара их не знал → HTML уходил как успех → `extract_state(... "defaultState")` → None → «defaultState extraction failed» → `cian_history_backfill` (= `cian_detail_backfill`) держал одну аренду на весь батч и сжигал 210–400 карточек через капча-узел (6200: 0/210; шесть прогонов 04–05.09: 0/400; через здоровый узел 13: 161/162). Проба одной карточки по узлам: 13 — карточка, 14 — капча, 1 — «Ошибка». База: 14 028 из 20 451 активных cian без detail. **Слой 1 — сайдкар** (`browser/server.py:1978-1994`, хуки `:2374`, `:2417`): `_CIAN_REFUSAL_TITLES` по `<title>` (нормализация пробелов/регистра/тире), per-provider `cian` → `BanPageDetectedError` → существующий путь (#3379): **403 + `ban_page: true` + `status: 200`** (upstream честно). Слово `captcha` в нормальной карточке (×11) не триггер — только title. **Слой 2 — kit** (`providers/cian/detail.py:109-117, :243-256, :170-181`; образы деплоятся раздельно): при провале `extract_state` с опознанным title → `report_platform_ban` по живой аренде + `raise CianBlockedError` (лог «капча/страница ошибки Циана (title=…)»); без маркеров — прежний `None`; `except SidecarBanPageError` перед общим `except` — отказ, опознанный сайдкаром, больше не гасится в `None` (иначе ротация чинилась бы, а прогон рапортовал «0/400 done»). **Ротация внутри батча — правок в `cian_history_backfill` не потребовалось:** `report_platform_ban` (`browser_fetcher.py:886`, публичное имя вместо `_report_platform_ban` — переименованы 2 внутренних вызова + комментарии) → `report_ban` (`mark_banned` пары узел×cian, `acquire("cian")` его больше не выдаст) + `_report_fetch_result(False, health=False)` → на 3-й капче `_LEASE_ROTATE_AFTER_FAILS` → `_acquire_lease()` (`:861-884`), следующий `/fetch` с нового узла. **Тесты:** `browser/test_server_cian_captcha.py` (7: маркеры, «captcha в карточке ≠ бан», тире, `_fetch_once` → `BanPageDetectedError`, `fetch_handler` → 403+`ban_page`+`status:200`, чужой провайдер не банится); `tests/test_3402_cian_captcha_http200.py` (капча/ошибка → `CianBlockedError` + рапорт; HTML без state и маркеров → `None`; батч через настоящий `fetch_detail` + фейковый фетчер: капча на первом узле → рапорт, второй листинг обогащён). **Фальсификация** (`git apply -R` kit-файлов): 3 failed — оба листинга сожжены на одном узле, `капча не отрапортована пулу — узел доработает батч`, в логе прежние «extraction failed». Восстановлено. **Прогоны:** backend `5598 passed, 35 skipped` (rc=0); сайдкар `241 passed` (rc=0); ruff OK (`server.py` вне скоупа хуков, не форматировался). **Приёмка на проде (после ОБОИХ образов):** следующий `cian_detail_backfill` — `listings_succeeded > 0`; в логах капча-узла `lease id=N (cian) BANNED — reporting to pool: … капча/страница ошибки Циана` и при трёх подряд `провалил 3 /fetch подряд — меняем прокси`; «defaultState extraction failed» на капче — 0; строка в `scrape_proxy_source_bans` для пары узел×cian. Не в PR: `ban_kind` для капчи остаётся `None` (`_note_refusal` судит по статусу, тут честный 200) — решение уровня #3196.
bot-backend added 1 commit 2026-09-06 09:12:54 +00:00
fix(tradein/cian): капча Циана с HTTP 200 — отказ площадки в обоих слоях (#3402)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 5m11s
ff979d4afd
Циан отдаёт капчу (`<title>Captcha - база объявлений ЦИАН`, 44 КБ) и страницу
ошибки (`<title>Ошибка - Циан`, 374 КБ) с кодом 200. Детектор сайдкара их не знал
(_REFUSAL_STATUSES {403,429} + маркеры Авито/Домклика), HTML уезжал клиенту как
успех, extract_state возвращал None и провайдер печатал «defaultState extraction
failed» — отказ ПЛОЩАДКИ читался как дрейф НАШЕЙ разметки. Аренда при этом не
менялась: fetch() уже отрапортовал mark_health(ok=True), fail-streak обнулялся, и
один капча-узел сжигал батч целиком (6200: 0/210; 6123/6091/6052/6032/6010/5981:
0/400 — против 161/162 через здоровый узел на прогоне 13).

Два слоя, потому что образы backend и browser деплоятся раздельно и расходятся
на часы:
* сайдкар (browser/server.py) — детект по <title> на обоих путях (navigate и
  подзапрос) → BanPageDetectedError → прежний путь #3288/#3379: 403 + ban_page +
  ЧЕСТНЫЙ upstream-статус 200;
* kit (providers/cian/detail.py) — при провале extract_state те же маркеры →
  CianBlockedError вместо тихого None, плюс report_platform_ban по живому lease.
  Там же ветка SidecarBanPageError: отказ, опознанный сайдкаром, больше не
  гасится общим `except` в «не смогли разобрать».

Слово `captcha` признаком быть не может: в нормальной карточке оно встречается 11
раз, на капче 17. Детект по <title> с нормализацией тире.

`_report_platform_ban` → `report_platform_ban` (публичный): тем же путём обязан
идти отказ, распознанный не сайдкаром, а провайдером. report_ban один только
пишет бан пары «узел×источник» — сменить сожжённую аренду ВНУТРИ батча позволяет
только fail-streak (_LEASE_ROTATE_AFTER_FAILS).
Author
Collaborator

Code-review (06.09): ⚠️ minor. Подтверждено: маркеры только по <title> в обоих слоях (кортежи побайтово равны), SERP-путь не регрессирует (except Exception → [], но теперь узел рапортуется), acquire("cian") забаненный узел не вернёт (scrape_proxy_source_bans фильтруется), NoProxyAvailableError из ре-acquire намеренно глотается в report_platform_ban (#3288), из fetch_detail по-прежнему летит; CianBlockedError в бэкфилле не растит ban_kinds (статус 200/None) → преждевременного banned нет; переименование чистое (0 вхождений старого имени); тесты через настоящие _fetch_once/fetch_handler/fetch_detail.

Прод-датум по вопросу ревью (09:25 UTC, повторная проба по узлам): узел 14 — уже настоящая карточка (капча снялась за ~2 ч); узел 1 — третий вариант: <title>Вы не робот?</title>, 16 КБ (час назад там была «Ошибка - Циан», 374 КБ). Доработка: капча = «Captcha…» + «Вы не робот?» (бан+ротация); «Ошибка - Циан» — на цикл наблюдения только лог (без бана/raise) в обоих слоях. Follow-up после деплоя: ban_kinds["platform"] по ТИПУ CianBlockedError (сейчас капча-волна в отчёте прогона всё ещё «не разобрали»); curl-путь fetch_detail (pipeline.py:3113/3750, session=) детектит, но не банит — узел успевает получить mark_health(ok=True); первый cian-sweep после деплоя — посмотреть errors_count/abort detail-фазы (капча теперь не обнуляет _cian_detail_consec_failures).

Code-review (06.09): ⚠️ minor. Подтверждено: маркеры только по `<title>` в обоих слоях (кортежи побайтово равны), SERP-путь не регрессирует (`except Exception → []`, но теперь узел рапортуется), `acquire("cian")` забаненный узел не вернёт (`scrape_proxy_source_bans` фильтруется), `NoProxyAvailableError` из ре-acquire намеренно глотается в `report_platform_ban` (#3288), из `fetch_detail` по-прежнему летит; `CianBlockedError` в бэкфилле не растит `ban_kinds` (статус 200/None) → преждевременного `banned` нет; переименование чистое (0 вхождений старого имени); тесты через настоящие `_fetch_once`/`fetch_handler`/`fetch_detail`. **Прод-датум по вопросу ревью (09:25 UTC, повторная проба по узлам):** узел 14 — уже настоящая карточка (капча снялась за ~2 ч); узел 1 — **третий вариант: `<title>Вы не робот?</title>`, 16 КБ** (час назад там была «Ошибка - Циан», 374 КБ). Доработка: капча = «Captcha…» + «Вы не робот?» (бан+ротация); «Ошибка - Циан» — на цикл наблюдения только лог (без бана/raise) в обоих слоях. Follow-up после деплоя: `ban_kinds["platform"]` по ТИПУ `CianBlockedError` (сейчас капча-волна в отчёте прогона всё ещё «не разобрали»); curl-путь `fetch_detail` (`pipeline.py:3113/3750`, `session=`) детектит, но не банит — узел успевает получить `mark_health(ok=True)`; первый cian-sweep после деплоя — посмотреть `errors_count`/abort detail-фазы (капча теперь не обнуляет `_cian_detail_consec_failures`).
Light1YT added 1 commit 2026-09-06 09:55:57 +00:00
fix(#3402): капча = «Captcha…» и «Вы не робот?» (бан+ротация); «Ошибка - Циан» — только лог на цикл наблюдения
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m8s
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 1m27s
957e9e8c01
Ревью (⚠️ minor) на #3403: «Ошибка - Циан» может быть транзиентной 5xx-страницей,
отданной с кодом 200, а не отказом конкретному узлу. Цена ошибки несимметрична —
mark_banned эскалирует TTL до часов, поэтому 20-минутный сбой площадки выбил бы из
выдачи весь пул. Один список маркеров этого различить не мог: и капча, и страница
ошибки шли одним путём в BanPageDetectedError.

Проба прода 06.09.2026 09:25 UTC (одна карточка по узлам через сайдкар):
* узел 14, час назад отдававший «Captcha - база объявлений ЦИАН», вернул НАСТОЯЩУЮ
  карточку — капча снимается за 1-2 часа, то есть TTL бана по назначению;
* узел 1 отдал ТРЕТИЙ вариант отказа — `<title>Вы не робот?`, 16 КБ (час назад —
  «Ошибка - Циан», 374 КБ). Прежние маркеры его не знали вовсе: отказ уезжал наверх
  как валидный HTML ровно так же, как до #3402.

Маркеры разделены на два класса, одинаково в обоих слоях (образы backend и browser
деплоятся раздельно и расходятся на часы):
* КАПЧА — «captcha - база объявлений циан» + «вы не робот?»: безусловный отказ
  площадки, прежний путь (сайдкар → BanPageDetectedError → 403 + ban_page; kit →
  report_platform_ban + CianBlockedError). За ней нет контента, и узел, которому её
  показали, будет получать её дальше;
* «ошибка - циан» — ТОЛЬКО ЛОГ: сайдкар отдаёт HTML клиенту как есть и пишет WARNING
  «страница ошибки Циана (title=…, upstream=…) — не бан, только лог (#3402)», kit при
  провале extract_state пишет WARNING и возвращает прежний None. Ни бана, ни рапорта,
  ни исключения — решение принимаем по частоте в логах за цикл наблюдения, а не по
  догадке о природе страницы.

Нормализация заголовка прежняя (регистр/пробелы/тире). `_is_cian_refusal` →
`_is_cian_captcha` + `_log_cian_error_page`; `_refusal_title` → `_page_title` и два
кортежа маркеров рядом.

Фальсификация: «вы не робот?» убран из маркеров обоих слоёв → kit 1 failed
(«DID NOT RAISE CianBlockedError»), сайдкар 3 failed («DID NOT RAISE
BanPageDetectedError», `_is_cian_captcha` → assert False is True). Маркер возвращён,
обе сьюты зелёные: backend 5599 passed / 35 skipped, browser 246 passed.
bot-backend merged commit 3d154f4ec0 into main 2026-09-06 10:01:30 +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#3403
No description provided.