fix(tradein/cian): банить узел только когда состояния НЕТ, а не по слову «captcha» (#2767)
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m2s
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m2s
Живая регрессия предыдущего PR, поймана на проде через 17 секунд после деплоя:
17:47:44 BrowserFetcher: lease id=9 (cian) BANNED — cian newbuilding: captcha
17:47:44 Cian newbuilding zhk-tihiy-centr-ekb-i.cian.ru parsed: id=108855
Одна и та же страница в одну и ту же секунду: разобрана успешно И объявлена
блоком. За минуту так забанились два ЗДОРОВЫХ узла (9 и 10). Причина: обычная
карточка ЖК грузит скрипт SmartCaptcha, подстрока «captcha» на ней есть всегда.
Ошибка была в том, что признак сменил смысл при переносе. `_ANTIBOT_MARKERS`
создавался объяснять УЖЕ СЛУЧИВШИЙСЯ отказ разбора — `_describe_parse_miss`
зовётся только из ветки «состояния нет», и там подстрока действительно значит
блок. Применённый как самостоятельный детектор на каждом ответе, тот же список
означает уже другое: «страница упоминает капчу».
Разбор перенесён внутрь `async with`, бан репортится только при
`nb_state is None` И найденной подписи. Условие «нет состояния» несущее, а не
уточняющее.
Тест на здоровой карточке в текущем формате Циана, содержащей слово-ловушку:
красный на смерженном коде (report_ban called 1 times), зелёный здесь.
Refs #2767
This commit is contained in:
parent
f3bcb1a25f
commit
876bf378e9
2 changed files with 59 additions and 10 deletions
|
|
@ -96,6 +96,19 @@ def _spy_fetcher(html: str) -> MagicMock:
|
|||
return fetcher
|
||||
|
||||
|
||||
# Здоровая карточка ЖК в текущем формате Циана (.concat([...])), и на ней ЕСТЬ подстрока
|
||||
# «captcha»: обычная страница грузит скрипт SmartCaptcha. Форма взята с живой страницы
|
||||
# 2026-08-09, содержимое сокращено до того, что читает fetch_newbuilding.
|
||||
_HEALTHY_CARD_WITH_CAPTCHA_SCRIPT = (
|
||||
'<html><head><script src="https://smartcaptcha.yandexcloud.net/captcha.js" async>'
|
||||
"</script></head><body><script>"
|
||||
"window._cianConfig['newbuilding-card-desktop-frontend'] = "
|
||||
"(window._cianConfig['newbuilding-card-desktop-frontend'] || []).concat("
|
||||
'[{"key":"initialState","value":{"newbuilding":{"id":108855,"name":"ЖК «Пихтовый»"}}}]'
|
||||
");</script></body></html>"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_waf_block_is_reported_as_a_ban_of_the_node(monkeypatch, waf_html, caplog) -> None:
|
||||
"""Блок → `report_ban`, пока lease ещё жив.
|
||||
|
|
@ -123,6 +136,35 @@ async def test_waf_block_is_reported_as_a_ban_of_the_node(monkeypatch, waf_html,
|
|||
assert "cian_waf_block" in "\n".join(r.getMessage() for r in caplog.records)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parsed_page_never_bans_the_node_even_with_captcha_word(monkeypatch) -> None:
|
||||
"""Разобранная страница НЕ банит узел, даже если на ней написано «captcha».
|
||||
|
||||
Живая регрессия 2026-08-09: первая версия этой правки звала `_blocked_by` на КАЖДОМ
|
||||
ответе, и за первые 17 секунд прогона забанила два ЗДОРОВЫХ узла (9 и 10) на
|
||||
страницах, которые успешно разобрались — обычная карточка ЖК грузит скрипт
|
||||
SmartCaptcha, подстрока там есть всегда. Признак «есть подпись защиты» осмыслен
|
||||
только вместе с «состояния нет»: список маркеров создавался объяснять УЖЕ
|
||||
случившийся отказ разбора, а не служить самостоятельным детектором.
|
||||
"""
|
||||
fetcher = _spy_fetcher(_HEALTHY_CARD_WITH_CAPTCHA_SCRIPT)
|
||||
monkeypatch.setattr(
|
||||
"scraper_kit.providers.cian.newbuilding.build_browser_fetcher",
|
||||
MagicMock(return_value=fetcher),
|
||||
)
|
||||
|
||||
result = await fetch_newbuilding(
|
||||
"https://zhk-pihtovyy-ekb-i.cian.ru",
|
||||
config=SimpleNamespace(browser_http_endpoint="http://tradein-browser:3000"),
|
||||
)
|
||||
|
||||
# Предпосылка теста: страница действительно и разбирается, и содержит слово-ловушку.
|
||||
assert result is not None
|
||||
assert result.cian_internal_house_id == 108855
|
||||
assert "captcha" in _HEALTHY_CARD_WITH_CAPTCHA_SCRIPT
|
||||
fetcher.report_ban.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_provider_reaches_the_fetcher_factory(monkeypatch, waf_html) -> None:
|
||||
"""Провод целиком: provider из задачи → build_browser_fetcher → пул.
|
||||
|
|
|
|||
|
|
@ -227,20 +227,27 @@ async def fetch_newbuilding(
|
|||
# прогон из 25 домов даёт до 25 acquire → до 25 релончей camoufox (~8 с каждый).
|
||||
# При паузе 18 с между домами это терпимо и вдобавок само по себе ротация IP.
|
||||
# Понадобится дешевле — поднимать фетчер в вызывающий цикл и передавать сюда.
|
||||
async with browser:
|
||||
html = await browser.fetch(zhk_url)
|
||||
# Блок распознаётся ПОКА lease жив: после выхода из `async with` узел уже
|
||||
# отпущен и `report_ban` стал бы no-op. Пул после этого не выдаёт узел под
|
||||
# source='cian' (scrape_proxy_source_bans), и следующий дом идёт с другого IP.
|
||||
blocked = _blocked_by(html)
|
||||
if blocked is not None:
|
||||
browser.report_ban(f"cian newbuilding: {blocked}")
|
||||
|
||||
# Cian ЖК-карточка: MFE 'newbuilding-card-desktop-frontend', key 'initialState'
|
||||
# (verified live 2026-06-15; перепроверено живьём 2026-08-09 — разметка та же).
|
||||
mfe = "newbuilding-card-desktop-frontend"
|
||||
|
||||
nb_state = extract_state(html, mfe=mfe, key="initialState")
|
||||
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")
|
||||
# Условие бана — «состояния НЕТ и есть подпись защиты», а не одна подпись.
|
||||
# Живая регрессия 2026-08-09 (поймана на проде через 17 секунд после деплоя):
|
||||
# маркер по одному лишь тексту забанил два ЗДОРОВЫХ узла на странице, которая
|
||||
# РАЗОБРАЛАСЬ, — обычная карточка ЖК грузит скрипт SmartCaptcha, и подстрока
|
||||
# "captcha" на ней есть всегда. Список маркеров создавался для объяснения УЖЕ
|
||||
# СЛУЧИВШЕГОСЯ отказа разбора (`_describe_parse_miss` зовётся только оттуда);
|
||||
# применять его как самостоятельный детектор — менять смысл признака.
|
||||
if nb_state is None:
|
||||
blocked = _blocked_by(html)
|
||||
if blocked is not None:
|
||||
browser.report_ban(f"cian newbuilding: {blocked}")
|
||||
|
||||
if nb_state is None:
|
||||
logger.warning(
|
||||
"Cian newbuilding %s: initialState extraction failed — %s",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue