fix(tradein/domclick): «система защиты от протечек» в объявлении читалась как блок QRATOR #3268
2 changed files with 107 additions and 9 deletions
|
|
@ -0,0 +1,80 @@
|
|||
"""#3267: маркер блока не должен срабатывать на тексте объявления.
|
||||
|
||||
Прод 30.08, прогон 5363: продавец написал в описании квартиры «Система защиты от
|
||||
протечек». Подстрока совпала с маркером блока «система защиты», ответ на 107 КБ с
|
||||
двадцатью валидными офферами был объявлен блок-страницей QRATOR, свип оборвал ВСЕ
|
||||
комнатные корзины и забанил живой узел пула на 6 часов.
|
||||
|
||||
Причина — порядок проверок: маркеры искались подстрокой по всему телу ДО разбора
|
||||
JSON. Разобранный JSON нужной формы блок-страницей быть не может (QRATOR отдаёт
|
||||
HTML), поэтому валидный разбор сам по себе доказывает отсутствие блока. Тот же
|
||||
порядок давно применён в detail.py — там маркеры смотрят только когда __SSR_STATE__
|
||||
не найден.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||
|
||||
import pytest
|
||||
from scraper_kit.domclick_exceptions import DOMCLICK_BLOCK_MARKERS, DomClickBlockedError
|
||||
from scraper_kit.providers.domclick.serp import _extract_json
|
||||
|
||||
|
||||
@pytest.mark.parametrize("маркер", sorted(DOMCLICK_BLOCK_MARKERS))
|
||||
def test_marker_inside_listing_description_is_not_a_block(маркер: str) -> None:
|
||||
"""Любой маркер в пользовательском тексте не превращает валидный ответ в блок."""
|
||||
payload = {
|
||||
"result": {
|
||||
"items": [
|
||||
{"id": 1, "description": f"Отличная квартира. {маркер.capitalize()} от протечек."}
|
||||
]
|
||||
}
|
||||
}
|
||||
data = _extract_json(json.dumps(payload, ensure_ascii=False))
|
||||
assert data["result"]["items"][0]["id"] == 1
|
||||
|
||||
|
||||
def test_real_prod_description_from_run_5363() -> None:
|
||||
"""Дословный фрагмент объявления, на котором оборвался прод-прогон 5363."""
|
||||
payload = {
|
||||
"result": {
|
||||
"items": [
|
||||
{
|
||||
"id": 2078475983,
|
||||
"description": (
|
||||
"но всё до миллиметра. Это не просто ремонт, а продуманный "
|
||||
"дизайн-проект:\r\n🔹 Двери скрытого монтажа с магнитными "
|
||||
"стопперами\r\n🔹 Система защиты от протечек\r\n🔹 Встроенная "
|
||||
"мебель и качественная техника"
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
assert _extract_json(json.dumps(payload, ensure_ascii=False))["result"]["items"]
|
||||
|
||||
|
||||
def test_actual_block_page_still_raises() -> None:
|
||||
"""Настоящая блок-страница — HTML без разбираемого JSON — по-прежнему блок."""
|
||||
html = (
|
||||
"<html><head><title>403 | ДомКлик</title></head>"
|
||||
"<body>Система защиты QRATOR: доступ ограничен</body></html>"
|
||||
)
|
||||
with pytest.raises(DomClickBlockedError):
|
||||
_extract_json(html)
|
||||
|
||||
|
||||
def test_html_wrapped_json_still_parses() -> None:
|
||||
"""Прежний путь: сайдкар в режиме навигации оборачивал JSON в <pre>."""
|
||||
data = _extract_json('<html><body><pre>{"result": {"items": []}}</pre></body></html>')
|
||||
assert data == {"result": {"items": []}}
|
||||
|
||||
|
||||
def test_garbage_without_markers_is_a_parse_error_not_a_block() -> None:
|
||||
"""Мусор без маркеров — ValueError, а не блок: узел за это банить нельзя."""
|
||||
with pytest.raises(ValueError, match="JSON"):
|
||||
_extract_json("<html><body>что-то пошло не так</body></html>")
|
||||
|
|
@ -106,8 +106,32 @@ def _extract_json(html: str) -> dict[str, Any]:
|
|||
DomClickBlockedError: если ответ содержит QRATOR/captcha маркеры.
|
||||
ValueError: если JSON не найден или не является dict.
|
||||
"""
|
||||
# Сканируем ВЕСЬ ответ (а не только первые 4096B): block-маркер может
|
||||
# стоять за пределами head в крупных challenge-страницах.
|
||||
# Порядок важен: СНАЧАЛА пробуем разобрать JSON, и только если не вышло — ищем
|
||||
# маркеры блока (#3267). Раньше было наоборот, и маркеры искались подстрокой по
|
||||
# ВСЕМУ телу, включая пользовательские описания объявлений. Прод 30.08, прогон
|
||||
# 5363: продавец написал в описании квартиры «Система защиты от протечек» —
|
||||
# подстрока совпала с маркером «система защиты», ответ на 107 КБ с двадцатью
|
||||
# валидными офферами был объявлен блок-страницей, свип оборвал все корзины и
|
||||
# забанил живой узел пула на 6 часов. Совпасть так же могут «captcha» и «qrator»
|
||||
# — в тексте объявления, в имени агентства, в ссылке.
|
||||
#
|
||||
# Разобранный JSON нужной формы блок-страницей быть не может: QRATOR отдаёт HTML.
|
||||
# Поэтому валидный разбор — сам по себе доказательство, что блока нет, и никакие
|
||||
# маркеры его переспорить не должны. Тот же порядок уже применён в detail.py
|
||||
# (маркеры смотрят ТОЛЬКО когда __SSR_STATE__ не найден).
|
||||
start = html.find("{")
|
||||
end = html.rfind("}")
|
||||
if start != -1 and end != -1 and end > start:
|
||||
try:
|
||||
data = json.loads(html[start : end + 1])
|
||||
except json.JSONDecodeError:
|
||||
data = None
|
||||
if isinstance(data, dict):
|
||||
return data # type: ignore[return-value]
|
||||
|
||||
# JSON не разобрался — вот теперь проверяем, не блок ли это. Сканируем ВЕСЬ ответ
|
||||
# (а не только первые 4096 Б): block-маркер может стоять за пределами head в
|
||||
# крупных challenge-страницах.
|
||||
html_lower = html.lower()
|
||||
if any(m in html_lower for m in DOMCLICK_BLOCK_MARKERS):
|
||||
raise DomClickBlockedError(
|
||||
|
|
@ -115,15 +139,9 @@ def _extract_json(html: str) -> dict[str, Any]:
|
|||
f"(markers checked: {DOMCLICK_BLOCK_MARKERS[:2]})"
|
||||
)
|
||||
|
||||
start = html.find("{")
|
||||
end = html.rfind("}")
|
||||
if start == -1 or end == -1 or end <= start:
|
||||
raise ValueError(f"No JSON object found in BFF response (len={len(html)})")
|
||||
|
||||
data = json.loads(html[start : end + 1])
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"BFF response JSON is not a dict: {type(data)}")
|
||||
return data # type: ignore[return-value]
|
||||
raise ValueError(f"BFF response is not a JSON dict (len={len(html)})")
|
||||
|
||||
|
||||
# ── URL builders ──────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue