fix(scrapers): bypass DOM.РФ WAF + update renamed extras endpoints
DOM.РФ накатил WAF между 2026-05-17 и 2026-05-24 на /сервисы/api/object/*/*. Без session cookies (___dmpkit___, domain_sid) — все запросы → 403 + WAF challenge HTML. Cookies выдаются после visit на /сервисы/каталог-новостроек/. Дополнительно DOM.РФ переименовал 6 endpoints (photos, 5 × doc_*) — старые URLs возвращают тот же WAF catch-all. Run 26 (2026-05-24): 15,340 fail requests (1534 obj × 10 endpoints). Extras snapshots не обновлялись с 2026-05-17. - stealth.py: BrowserSession.warm_up() — visit catalog listing, кэшит WAF cookies - domrf_kn.py: warm_up() перед Phase A/B/C (покрывает все extras запросы) - domrf_catalog_object.py: warm_up() перед SSR HTML fetch batch
This commit is contained in:
parent
02b48b8d04
commit
1ccb44687c
3 changed files with 48 additions and 0 deletions
|
|
@ -441,6 +441,11 @@ async def scrape_catalog_objects(
|
||||||
# Страницы каталога публичные — Basic auth не нужен
|
# Страницы каталога публичные — Basic auth не нужен
|
||||||
auth=None,
|
auth=None,
|
||||||
) as session:
|
) as session:
|
||||||
|
# Warm-up: visit /сервисы/каталог-новостроек/ to obtain WAF cookies.
|
||||||
|
# DOM.РФ WAF (2026-05-24) блокирует SSR-страницы объектов без этих cookies.
|
||||||
|
# Idempotent — один вызов покрывает весь batch через этот BrowserSession.
|
||||||
|
await session.warm_up()
|
||||||
|
|
||||||
for obj_id in obj_ids:
|
for obj_id in obj_ids:
|
||||||
stats["processed"] += 1
|
stats["processed"] += 1
|
||||||
ok = await scrape_catalog_object(db, session, obj_id, snapshot_date)
|
ok = await scrape_catalog_object(db, session, obj_id, snapshot_date)
|
||||||
|
|
|
||||||
|
|
@ -1684,6 +1684,11 @@ async def run_region_sweep(
|
||||||
load_state=load_state,
|
load_state=load_state,
|
||||||
save_state=save_state,
|
save_state=save_state,
|
||||||
) as sess:
|
) as sess:
|
||||||
|
# Warm-up: visit /сервисы/каталог-новостроек/ to obtain WAF cookies
|
||||||
|
# (___dmpkit___, domain_sid). Required since 2026-05-24 — without these
|
||||||
|
# cookies all /сервисы/api/object/*/* requests return 403 + WAF HTML.
|
||||||
|
await sess.warm_up()
|
||||||
|
|
||||||
# ── Phase A — fetch objects (skip on resume) ────────────────────
|
# ── Phase A — fetch objects (skip on resume) ────────────────────
|
||||||
if not resume_from_run_id:
|
if not resume_from_run_id:
|
||||||
for status in statuses:
|
for status in statuses:
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ class BrowserSession:
|
||||||
self._page: Page | None = None
|
self._page: Page | None = None
|
||||||
self._sem = asyncio.Semaphore(8)
|
self._sem = asyncio.Semaphore(8)
|
||||||
self._request_count = 0
|
self._request_count = 0
|
||||||
|
self._warmed_up = False
|
||||||
|
|
||||||
async def __aenter__(self) -> BrowserSession:
|
async def __aenter__(self) -> BrowserSession:
|
||||||
await self._bootstrap()
|
await self._bootstrap()
|
||||||
|
|
@ -142,6 +143,43 @@ class BrowserSession:
|
||||||
logger.info("bootstrap: storage_state saved to %s", self.save_state)
|
logger.info("bootstrap: storage_state saved to %s", self.save_state)
|
||||||
logger.info("bootstrap: page ready, WAF challenge passed")
|
logger.info("bootstrap: page ready, WAF challenge passed")
|
||||||
|
|
||||||
|
async def warm_up(self, force: bool = False) -> None:
|
||||||
|
"""Visit catalog listing to obtain WAF cookies (___dmpkit___, domain_sid).
|
||||||
|
|
||||||
|
DOM.РФ WAF (накатан между 2026-05-17 и 2026-05-24) требует session cookies для
|
||||||
|
любого запроса на /сервисы/api/object/*/*. Cookies выдаются JS-челленджем при
|
||||||
|
visit на /сервисы/каталог-новостроек/. Один warm-up на BrowserSession достаточен —
|
||||||
|
cookies валидны для всего домена.
|
||||||
|
|
||||||
|
Idempotent: повторные вызовы без force=True — no-op после первого успешного warm_up.
|
||||||
|
"""
|
||||||
|
if self._warmed_up and not force:
|
||||||
|
return
|
||||||
|
if self._context is None:
|
||||||
|
raise RuntimeError("BrowserSession not bootstrapped — call inside `async with` block")
|
||||||
|
page = await self._context.new_page()
|
||||||
|
try:
|
||||||
|
await page.goto(
|
||||||
|
f"{BASE_URL}/сервисы/каталог-новостроек/",
|
||||||
|
wait_until="domcontentloaded",
|
||||||
|
timeout=30_000,
|
||||||
|
)
|
||||||
|
await page.wait_for_timeout(2000) # JS challenge ставит cookies async
|
||||||
|
cookies = await self._context.cookies()
|
||||||
|
names = {c["name"] for c in cookies}
|
||||||
|
critical = {"___dmpkit___", "domain_sid"}
|
||||||
|
got = names & critical
|
||||||
|
if not got:
|
||||||
|
logger.warning(
|
||||||
|
"warm_up: WAF cookies missing after catalog visit (have: %s)",
|
||||||
|
sorted(names),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info("warm_up: got WAF cookies %s (total=%d)", sorted(got), len(cookies))
|
||||||
|
self._warmed_up = True
|
||||||
|
finally:
|
||||||
|
await page.close()
|
||||||
|
|
||||||
async def get_json(self, path: str, params: dict[str, Any]) -> dict[str, Any]:
|
async def get_json(self, path: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Fetch JSON via fetch() in browser. Retries with backoff on transient errors."""
|
"""Fetch JSON via fetch() in browser. Retries with backoff on transient errors."""
|
||||||
if self._page is None:
|
if self._page is None:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue