feat(11-01): create stealth fingerprint masking module

- Add StealthConfig Pydantic model with all playwright-stealth options
- Add apply_stealth_sync/apply_stealth_async functions for browser context
- Add get_default_stealth_config convenience function
- Add 9 unit tests covering config defaults, stealth application, and disabled mode
- Update conftest.py with stealth_config and mock_browser_context fixtures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 23:47:50 +08:00
parent d6026660a9
commit e152b8ea05
4 changed files with 336 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
"""Stealth module for browser fingerprint masking."""
from .stealth_config import (
StealthConfig,
apply_stealth_sync,
apply_stealth_async,
get_default_stealth_config,
)
__all__ = [
"StealthConfig",
"apply_stealth_sync",
"apply_stealth_async",
"get_default_stealth_config",
]
@@ -0,0 +1,169 @@
"""Stealth configuration for browser fingerprint masking.
Uses playwright-stealth library to hide Playwright automation signatures.
"""
from typing import Optional
from playwright.sync_api import BrowserContext
from playwright.async_api import BrowserContext as AsyncBrowserContext
from pydantic import BaseModel, Field
from playwright_stealth import Stealth
class StealthConfig(BaseModel):
"""Configuration for browser stealth settings.
All options map to playwright-stealth evasion scripts.
Default values optimized for Zhihu anti-detection.
"""
stealth_enabled: bool = Field(
default=True,
description="Enable stealth mode (set False to disable)"
)
# Navigator evasion options
navigator_webdriver: bool = Field(
default=True,
description="Hide navigator.webdriver property (STEALTH-01)"
)
navigator_user_agent: bool = Field(
default=True,
description="Replace HeadlessChrome in User-Agent (STEALTH-04)"
)
navigator_plugins: bool = Field(
default=True,
description="Mock navigator.plugins array"
)
navigator_languages: bool = Field(
default=True,
description="Set navigator.languages"
)
navigator_platform: bool = Field(
default=True,
description="Set navigator.platform"
)
navigator_vendor: bool = Field(
default=True,
description="Set navigator.vendor"
)
navigator_permissions: bool = Field(
default=True,
description="Override navigator.permissions"
)
# Chrome object evasion (STEALTH-03)
chrome_app: bool = Field(
default=True,
description="Mock chrome.app object"
)
chrome_csi: bool = Field(
default=True,
description="Mock chrome.csi function"
)
chrome_load_times: bool = Field(
default=True,
description="Mock chrome.loadTimes function"
)
# WebGL and rendering
webgl_vendor: bool = Field(
default=True,
description="Override WebGL vendor/renderer strings"
)
# Other evasion options
hairline: bool = Field(
default=True,
description="Fix hairline detection"
)
iframe_content_window: bool = Field(
default=True,
description="Fix iframe contentWindow"
)
media_codecs: bool = Field(
default=True,
description="Mock media codecs"
)
error_prototype: bool = Field(
default=True,
description="Fix error prototype chain"
)
sec_ch_ua: bool = Field(
default=True,
description="Set sec-ch-ua headers"
)
def create_stealth(self) -> Stealth:
"""Create playwright-stealth Stealth instance from config.
Returns:
Configured Stealth instance
"""
return Stealth(
navigator_webdriver=self.navigator_webdriver,
navigator_user_agent=self.navigator_user_agent,
navigator_plugins=self.navigator_plugins,
navigator_languages=self.navigator_languages,
navigator_platform=self.navigator_platform,
navigator_vendor=self.navigator_vendor,
navigator_permissions=self.navigator_permissions,
chrome_app=self.chrome_app,
chrome_csi=self.chrome_csi,
chrome_load_times=self.chrome_load_times,
webgl_vendor=self.webgl_vendor,
hairline=self.hairline,
iframe_content_window=self.iframe_content_window,
media_codecs=self.media_codecs,
error_prototype=self.error_prototype,
sec_ch_ua=self.sec_ch_ua,
)
def apply_stealth_sync(
context: BrowserContext,
config: Optional[StealthConfig] = None
) -> None:
"""Apply stealth settings to browser context (synchronous).
Args:
context: Playwright BrowserContext to apply stealth to
config: StealthConfig instance (uses defaults if not provided)
"""
if config is None:
config = StealthConfig()
if not config.stealth_enabled:
return
stealth = config.create_stealth()
stealth.apply_stealth_sync(context)
async def apply_stealth_async(
context: AsyncBrowserContext,
config: Optional[StealthConfig] = None
) -> None:
"""Apply stealth settings to browser context (asynchronous).
Args:
context: Playwright async BrowserContext to apply stealth to
config: StealthConfig instance (uses defaults if not provided)
"""
if config is None:
config = StealthConfig()
if not config.stealth_enabled:
return
stealth = config.create_stealth()
await stealth.apply_stealth_async(context)
def get_default_stealth_config() -> StealthConfig:
"""Get default StealthConfig for Zhihu crawling.
Returns:
StealthConfig with all evasion options enabled
"""
return StealthConfig()