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
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_waf_block_is_reported_as_a_ban_of_the_node(monkeypatch, waf_html, caplog) -> None:
|
async def test_waf_block_is_reported_as_a_ban_of_the_node(monkeypatch, waf_html, caplog) -> None:
|
||||||
"""Блок → `report_ban`, пока lease ещё жив.
|
"""Блок → `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)
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_proxy_provider_reaches_the_fetcher_factory(monkeypatch, waf_html) -> None:
|
async def test_proxy_provider_reaches_the_fetcher_factory(monkeypatch, waf_html) -> None:
|
||||||
"""Провод целиком: provider из задачи → build_browser_fetcher → пул.
|
"""Провод целиком: provider из задачи → build_browser_fetcher → пул.
|
||||||
|
|
|
||||||
|
|
@ -227,20 +227,27 @@ async def fetch_newbuilding(
|
||||||
# прогон из 25 домов даёт до 25 acquire → до 25 релончей camoufox (~8 с каждый).
|
# прогон из 25 домов даёт до 25 acquire → до 25 релончей camoufox (~8 с каждый).
|
||||||
# При паузе 18 с между домами это терпимо и вдобавок само по себе ротация IP.
|
# При паузе 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'
|
# Cian ЖК-карточка: MFE 'newbuilding-card-desktop-frontend', key 'initialState'
|
||||||
# (verified live 2026-06-15; перепроверено живьём 2026-08-09 — разметка та же).
|
# (verified live 2026-06-15; перепроверено живьём 2026-08-09 — разметка та же).
|
||||||
mfe = "newbuilding-card-desktop-frontend"
|
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:
|
if nb_state is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Cian newbuilding %s: initialState extraction failed — %s",
|
"Cian newbuilding %s: initialState extraction failed — %s",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue