معماری کلاستر خزش پرسرعت با Playwright Stealth و مدیریت بهینه رم
راهنمای جامع طراحی استخر کانتکستهای مرورگر (Context Pooling)، جلوگیری از نشت حافظه V8 در کانتینرهای Docker و تزریق اسکریپتهای ضدتشخیص سطح CDP.
چالش مقیاسپذیری مرورگرهای Headless در محیط پروداکشن
اجرای مرورگرهای Headless در مقیاس چند ده هزار صفحه در ساعت، یکی از پرهزینهترین بخشهای مهندسی داده است. اشتباه رایج در بسیاری از اسکرپرها، فراخوانی browser.launch() به ازای هر وظیفه یا باز نگه داشتن یک نمونه مرورگر برای روزها بدون چرخه بازیافت است. موتور V8 و فرآیندهای رندرینگ Chromium به مرور زمان دچار Memory Fragmentation میشوند و در محیطهای کانتینری (Docker) به دلیل محدودیت پیشفرض ۶۴ مگابایتی پارتیشن /dev/shm، فرآیندهای زامبی ایجاد کرده و باعث کرش کامل نود میشوند.
معماری Browser Context Pooling به جای ساخت نمونه جدید
برای دستیابی به توان عملیاتی بالا (High Throughput) و مصرف بهینه رم، باید چرخه حیات مرورگر را به سه لایه مجزا تفکیک کنیم:
- لایه Browser Process: تنها ۱ تا ۲ نمونه از پروسه اصلی Chromium روی هر ورکر اجرا میشود و پس از پردازش تعداد مشخصی درخواست (مثلاً ۵۰۰ صفحه) به طور کامل ریاستارت میگردد.
- لایه BrowserContext Pool: به جای اجرای مرورگر جدید، از
browser.new_context()استفاده میکنیم. هر کانتکست تنها چند مگابایت رم مصرف میکند، در کمتر از ۲۰ میلیثانیه ساخته میشود و ایزولاسیون کامل کوکیها،localStorageو پراکسی را فراهم میآورد. - لایه Page Lifecycle: هر تب (
Page) پس از استخراج داده باید بلافاصله در بلوکfinallyبسته شود تا DOM Tree و شنوندههای رویداد از حافظه Heap آزاد شوند.
بهینهسازی فلگهای کرومیوم و پارتیشن اشتراکی در Docker
در کانتینرهای لینوکسی، فلگ --disable-dev-shm-usage کرومیوم را مجبور میکند به جای حافظه اشتراکی کوچک، از /tmp استفاده کند. همچنین محدود کردن حافظه موتور جاوااسکریپت با --js-flags=--max-old-space-size=512 مانع از بلعیده شدن رم سرور توسط اسکریپتهای سنگین صفحات هدف میشود.
مسدودسازی منابع غیرضروری و تزریق پچهای ضد تشخیص (Stealth)
بیش از ۷۰ درصد پهنای باند و زمان پردازش CPU در مرورگر صرف دانلود و دیکد کردن تصاویر، فونتها، ویدیوها و اسکریپتهای ترکینگ (مانند Google Analytics و Hotjar) میشود. با رهگیری درخواستها در لایه شبکه (page.route) و مسدود کردن منابع غیرضروری، سرعت بارگذاری صفحه تا ۴ برابر افزایش مییابد.
همزمان، برای دور زدن اسکریپتهای تشخیص اتوماسیون که ویژگیهای شیء navigator و پروتکل CDP (Chrome DevTools Protocol) را بررسی میکنند، باید پیش از اجرای هر اسکریپتی در صفحه، از طریق context.add_init_script مقادیر افشاگر را بازنویسی کنیم.
پیادهسازی استخر کانتکست خودمراقبت (Self-Healing Context Pool)
کد زیر یک معماری کامل و مقاوم در برابر خطا برای مدیریت همزمان کانتکستهای Playwright همراه با پایش مصرف رم، تزریق پچهای Stealth و مسدودسازی هوشمند منابع سنگین است:
import asyncio
import psutil
from typing import Optional, List
from playwright.async_api import async_playwright, Browser, BrowserContext, Route
STEALTH_INIT_SCRIPT = """
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {} };
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(param) {
if (param === 37445) return 'Intel Inc.';
if (param === 37446) return 'Intel Iris OpenGL Engine';
return getParameter.apply(this, arguments);
};
"""
class PlaywrightStealthCluster:
BLOCKED_RESOURCE_TYPES = {'image', 'media', 'font', 'stylesheet'}
BLOCKED_DOMAINS = ('google-analytics.com', 'doubleclick.net', 'hotjar.com', 'clarity.ms')
def __init__(self, concurrency: int = 8, max_pages_per_browser: int = 250, max_ram_percent: float = 82.0):
self.semaphore = asyncio.Semaphore(concurrency)
self.max_pages_per_browser = max_pages_per_browser
self.max_ram_percent = max_ram_percent
self._playwright = None
self._browser: Optional[Browser] = None
self._processed_pages = 0
self._lock = asyncio.Lock()
async def start(self) -> None:
self._playwright = await async_playwright().start()
await self._launch_browser()
async def _launch_browser(self) -> None:
if self._browser:
await self._browser.close()
self._browser = await self._playwright.chromium.launch(
headless=True,
args=[
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage',
'--no-sandbox',
'--disable-gpu',
'--js-flags=--max-old-space-size=512'
]
)
self._processed_pages = 0
async def _intercept_route(self, route: Route) -> None:
req = route.request
if req.resource_type in self.BLOCKED_RESOURCE_TYPES or any(d in req.url for d in self.BLOCKED_DOMAINS):
await route.abort()
else:
await route.continue_()
async def _ensure_healthy_browser(self) -> None:
async with self._lock:
ram_usage = psutil.virtual_memory().percent
if self._processed_pages >= self.max_pages_per_browser or ram_usage > self.max_ram_percent:
await self._launch_browser()
async def fetch_rendered_html(self, url: str, wait_selector: str = 'body', proxy: Optional[str] = None) -> str:
async with self.semaphore:
await self._ensure_healthy_browser()
context: BrowserContext = await self._browser.new_context(
viewport={'width': 1440, 'height': 900},
user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
proxy={'server': proxy} if proxy else None
)
await context.add_init_script(STEALTH_INIT_SCRIPT)
page = await context.new_page()
await page.route('**/*', self._intercept_route)
try:
await page.goto(url, wait_until='domcontentloaded', timeout=25000)
await page.wait_for_selector(wait_selector, timeout=10000)
self._processed_pages += 1
return await page.content()
finally:
await page.close()
await context.close()
async def close(self) -> None:
if self._browser:
await self._browser.close()
if self._playwright:
await self._playwright.stop()نکته کلیدی مهندسی: در وبسایتهای محافظتشده با فایروالهای حساس، مسدود کردن کامل فایلهای stylesheet (CSS) گاهی میتواند باعث صفر شدن ابعاد المانها (offsetWidth === 0) شود که اسکریپتهای ضدبات از آن برای تشخیص محیطهای دستکاریشده استفاده میکنند. در چنین اهدافی فقط image، media و font را مسدود کنید.