shockmap-api / ingestion /shared /playwright_helpers.py
Dhruva P Gowda
Build ShockMap MVP, operator UI, and deployment scaffolding
f927995
Raw History Blame Contribute Delete
1.98 kB
"""
Async helpers for Playwright browser-based scraping.
"""
import asyncio
import logging
from typing import List, Dict, Optional
from playwright.async_api import async_playwright
# Setup Logging
logger = logging.getLogger("ingestion.playwright")
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
async def fetch_page_html(
url: str,
wait_for_selector: Optional[str] = None,
timeout_ms: int = 30000
) -> str:
"""
Launches a headless browser, navigates to a URL, and returns the full HTML content.
"""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(user_agent=USER_AGENT)
page = await context.new_page()
try:
logger.info(f"Navigating to {url}...")
await page.goto(url, timeout=timeout_ms)
if wait_for_selector:
await page.wait_for_selector(wait_for_selector, timeout=timeout_ms)
html = await page.content()
return html
except Exception as e:
logger.error(f"Error fetching {url}: {e}")
return ""
finally:
await browser.close()
async def fetch_pages_concurrent(
urls: List[str],
max_concurrency: int = 3,
rate_limit_per_sec: float = 0.5
) -> Dict[str, str]:
"""
Fetches multiple pages concurrently with a semaphore and rate limiting.
"""
semaphore = asyncio.Semaphore(max_concurrency)
results = {}
async def _bound_fetch(url: str):
async with semaphore:
html = await fetch_page_html(url)
results[url] = html
# Rate limit wait
await asyncio.sleep(1.0 / rate_limit_per_sec)
tasks = [asyncio.create_task(_bound_fetch(url)) for url in urls]
await asyncio.gather(*tasks)
return results