diff --git a/tradein-mvp/backend/tests/test_2767_cian_waf_block.py b/tradein-mvp/backend/tests/test_2767_cian_waf_block.py
index 660db0f7..43d793df 100644
--- a/tradein-mvp/backend/tests/test_2767_cian_waf_block.py
+++ b/tradein-mvp/backend/tests/test_2767_cian_waf_block.py
@@ -183,3 +183,49 @@ async def test_proxy_provider_reaches_the_fetcher_factory(monkeypatch, waf_html)
)
factory.assert_called_once_with(config, "cian", proxy_provider=provider)
+
+
+# ── новый фронт Циана: тот же ключ под другим именем MFE ──────────────────────
+
+# Форма живой страницы zhk-kosmos-ekb-i.cian.ru (2026-08-09): состояние лежит под
+# 'newbuilding-card-desktop-fichering-frontend', СТАРОГО имени на странице нет вовсе.
+_FICHERING_CARD = (
+ "
"
+)
+
+
+@pytest.mark.asyncio
+async def test_state_is_found_under_the_renamed_fichering_mfe(monkeypatch) -> None:
+ """ЖК на новом фронте Циана разбирается — вместе с графиком цен.
+
+ Именно из-за этого имени `houses_price_dynamics` стояла с 2026-07-26: страница
+ приходила целиком, состояние в ней было, но под другим MFE — и отказ выглядел как
+ «разметка изменилась», хотя форма состояния та же.
+ """
+ fetcher = _spy_fetcher(_FICHERING_CARD)
+ monkeypatch.setattr(
+ "scraper_kit.providers.cian.newbuilding.build_browser_fetcher",
+ MagicMock(return_value=fetcher),
+ )
+
+ result = await fetch_newbuilding(
+ "https://zhk-kosmos-ekb-i.cian.ru",
+ config=SimpleNamespace(browser_http_endpoint="http://tradein-browser:3000"),
+ )
+
+ assert result is not None
+ assert result.cian_internal_house_id == 3235089
+ assert result.name == "Космос"
+ # График — тот самый, ради которого задача и существует.
+ assert [p["month_date"] for p in result.realty_valuation_chart] == [
+ "2026-06-01",
+ "2026-07-01",
+ ]
+ assert [p["price_per_sqm"] for p in result.realty_valuation_chart] == [4100000.0, 4250000.0]
+ fetcher.report_ban.assert_not_called()
diff --git a/tradein-mvp/backend/tests/test_2767_newbuilding_parse_miss.py b/tradein-mvp/backend/tests/test_2767_newbuilding_parse_miss.py
index 28b16f56..81056835 100644
--- a/tradein-mvp/backend/tests/test_2767_newbuilding_parse_miss.py
+++ b/tradein-mvp/backend/tests/test_2767_newbuilding_parse_miss.py
@@ -53,9 +53,13 @@ def test_describe_parse_miss_reports_size_and_no_markers() -> None:
def test_describe_parse_miss_names_found_markers() -> None:
- """Заглушка: маркер назван, но диагноз не выносится — только перечисление найденного."""
+ """Стена капчи: маркер назван, но диагноз не выносится — только перечисление найденного.
+
+ Опознаётся по заголовку «Вы не робот?», а НЕ по подстроке "captcha": последняя есть
+ на любой здоровой карточке ЖК (скрипт SmartCaptcha) и была снята из списка после
+ того, как забанила здоровые узлы (#2767).
+ """
described = _describe_parse_miss("Вы не робот?
")
- assert "captcha" in described
assert "вы не робот" in described
assert "html_len=" in described
diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/cian/newbuilding.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/cian/newbuilding.py
index 89034166..4211f73f 100644
--- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/cian/newbuilding.py
+++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/cian/newbuilding.py
@@ -1,7 +1,9 @@
"""Cian.ru newbuilding (ЖК) catalog page scraper.
URL: https://zhk---i.cian.ru/
-MFE: 'newbuilding-card-desktop-frontend', key: 'initialState'
+MFE: 'newbuilding-card-desktop-frontend' ИЛИ 'newbuilding-card-desktop-fichering-frontend'
+(Циан раскатывает новый фронт на часть ЖК — оба имени живые, см. _NEWBUILDING_MFES),
+key: 'initialState'
Sister state containers extracted from same MFE initialState top-level keys:
- realtyValuation: 7-month price chart (data.priceDynamics.chart.data.{labels,values})
@@ -82,8 +84,13 @@ _CATPH_NEWBUILDING_SERP = (
# то есть по размеру НЕОТЛИЧИМА от целой карточки — «~1 МБ значит разметка» не работает,
# различает только маркер. Молчание списка читалось как «защита проверена и не нашлась»;
# на деле список просто не знал этой подписи (см. capture в #2767).
+# Голой подстроки "captcha" здесь НЕТ намеренно (живая проба 2026-08-09): ЛЮБАЯ здоровая
+# карточка ЖК грузит скрипт SmartCaptcha, поэтому "captcha" на ней есть всегда. Стена
+# капчи опознаётся по своему заголовку «Вы не робот?» — он в списке ниже и на проде
+# срабатывал вместе с "captcha" на всех 15-килобайтных стенах, так что снятие ложного
+# маркера ничего не теряет. Пока он тут был, отказ разбора большой (682-886 КБ) страницы
+# сопровождался баном ЗДОРОВОГО узла с причиной "captcha".
_ANTIBOT_MARKERS = (
- "captcha", # покрывает и recaptcha, и smartcaptcha
"qrator",
"ddos-guard",
"cf-chl", # Cloudflare challenge
@@ -109,6 +116,37 @@ def _describe_parse_miss(html: str) -> str:
return f"html_len={len(html)} antibot_markers={','.join(found) if found else 'none'}"
+# MFE, под которым Циан кладёт состояние ЖК-карточки. Имён ДВА, и оба живые — замер
+# 2026-08-09, один и тот же прогон:
+# zhk-pihtovyy-ekb-i.cian.ru → newbuilding-card-desktop-frontend
+# zhk-kosmos-ekb-i.cian.ru → newbuilding-card-desktop-fichering-frontend
+# zhk-les-ekb-i.cian.ru → newbuilding-card-desktop-fichering-frontend
+# Второе имя (`fichering`) — раскатка нового фронта Циана на ЧАСТЬ ЖК. Страницы под ним
+# приходят целиком (810-993 КБ, ни капчи, ни WAF), состояние в них лежит под тем же
+# ключом `initialState` и той же формы: `newbuilding.id`/`name` на месте,
+# `realtyValuation.data.priceDynamics` — тоже. Именно из-за него `houses_price_dynamics`
+# стояла с 2026-07-26: у ЖК, переехавших на новый фронт, график есть, но старое имя MFE
+# его не находило, а отказ выглядел как «страница целая, разметка изменилась».
+# Порядок — исторический первым: большинство ЖК всё ещё на нём, лишнего разбора не будет.
+_NEWBUILDING_MFES = (
+ "newbuilding-card-desktop-frontend",
+ "newbuilding-card-desktop-fichering-frontend",
+)
+
+
+def _extract_nb_state(html: str) -> tuple[dict[str, Any] | None, str]:
+ """Состояние ЖК-карточки + имя MFE, под которым оно нашлось.
+
+ Имя возвращается, потому что sister-контейнеры читаются из `extract_all_states()`
+ по ТОМУ ЖЕ MFE — искать их под чужим именем бессмысленно.
+ """
+ for mfe in _NEWBUILDING_MFES:
+ state = extract_state(html, mfe=mfe, key="initialState")
+ if state is not None:
+ return state, mfe
+ return None, _NEWBUILDING_MFES[0]
+
+
def _blocked_by(html: str) -> str | None:
"""Найденные подписи защиты, если страница — блок; иначе None.
@@ -227,15 +265,11 @@ async def fetch_newbuilding(
# прогон из 25 домов даёт до 25 acquire → до 25 релончей camoufox (~8 с каждый).
# При паузе 18 с между домами это терпимо и вдобавок само по себе ротация IP.
# Понадобится дешевле — поднимать фетчер в вызывающий цикл и передавать сюда.
- # Cian ЖК-карточка: MFE 'newbuilding-card-desktop-frontend', key 'initialState'
- # (verified live 2026-06-15; перепроверено живьём 2026-08-09 — разметка та же).
- mfe = "newbuilding-card-desktop-frontend"
-
async with browser:
html = await browser.fetch(zhk_url)
# Разбор — ВНУТРИ контекста, чтобы бан репортился, пока lease жив: после выхода
# из `async with` узел уже отпущен и `report_ban` стал бы no-op.
- nb_state = extract_state(html, mfe=mfe, key="initialState")
+ nb_state, mfe = _extract_nb_state(html)
# Условие бана — «состояния НЕТ и есть подпись защиты», а не одна подпись.
# Живая регрессия 2026-08-09 (поймана на проде через 17 секунд после деплоя):
# маркер по одному лишь тексту забанил два ЗДОРОВЫХ узла на странице, которая
@@ -250,8 +284,9 @@ async def fetch_newbuilding(
if nb_state is None:
logger.warning(
- "Cian newbuilding %s: initialState extraction failed — %s",
+ "Cian newbuilding %s: initialState extraction failed (искали в MFE %s) — %s",
zhk_url,
+ "/".join(_NEWBUILDING_MFES),
_describe_parse_miss(html),
)
return None