第 49 课 | Playwright 进阶:反爬对抗与浏览器指纹

使用 Playwright 替代 Selenium,配合 stealth 插件、代理 IP 池、人类行为模拟,应对更复杂的网站反爬机制。


一、Playwright vs Selenium:为什么迁移

维度SeleniumPlaywright
速度较慢(需显式等待)快(自动等待元素就绪)
反检测容易被识别内置多种反检测特性
API 设计传统 WebDriver 模式现代 async/await,简洁直观
移动端不支持原生支持移动端模拟(iPhone/Android)
网络拦截需要额外配置内置 route 拦截,可修改请求/响应
多页面窗口切换复杂Context 隔离,天然支持多标签页
安装需要 WebDriver 匹配浏览器版本一条命令安装浏览器

迁移成本低:Playwright 的 API 设计更现代,从 Selenium 迁移通常只需 1-2 小时。


二、反爬策略全景

🎯 目标网站

🕵️ 指纹伪装层
UA/Viewport/Locale/Timezone

🛡️ Stealth 层
隐藏 webdriver/自动化特征

🌐 代理层
IP 池轮换/地域伪装

🧑 行为层
随机延迟/鼠标轨迹/滚动

✅ 成功获取数据


三、浏览器指纹伪装

3.1 什么是浏览器指纹

网站通过以下信息构建你的「指纹」:

  • User-Agent(浏览器类型和版本)
  • 屏幕分辨率
  • 时区
  • 语言偏好
  • 字体列表
  • WebGL 渲染器
  • Canvas 指纹

如果指纹与正常浏览器不一致,就会被识别为爬虫。

3.2 完整指纹配置

from playwright.sync_api import sync_playwright
import random

# 真实浏览器 User-Agent 池
USER_AGENTS = [
    # Chrome Windows
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    # Chrome Mac
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    # Edge Windows
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0",
    # Firefox Windows
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
]

# 常见屏幕分辨率
VIEWPORTS = [
    {"width": 1920, "height": 1080},
    {"width": 2560, "height": 1440},
    {"width": 1366, "height": 768},
    {"width": 1440, "height": 900},
]

# 中文语言与地区
LOCALES = ["zh-CN", "zh-TW", "zh-HK"]
TIMEZONES = ["Asia/Shanghai", "Asia/Hong_Kong", "Asia/Taipei"]


def create_stealth_context(browser):
    """创建带随机指纹的浏览器上下文"""
    context = browser.new_context(
        viewport=random.choice(VIEWPORTS),
        user_agent=random.choice(USER_AGENTS),
        locale=random.choice(LOCALES),
        timezone_id=random.choice(TIMEZONES),
        # 额外指纹配置
        geolocation={"latitude": 31.2304, "longitude": 121.4737},  # 上海
        permissions=["geolocation"],
        color_scheme="light",
        device_scale_factor=1,
        is_mobile=False,
        has_touch=False,
    )
    return context

四、Stealth 插件:隐藏自动化特征

4.1 默认情况下 Playwright 仍然可被检测

// 网站在浏览器中运行以下检测代码:
navigator.webdriver  // Playwright 默认返回 true

4.2 注入反检测脚本

def inject_stealth_scripts(page):
    """注入反检测脚本,隐藏自动化特征"""
    
    # 1. 隐藏 navigator.webdriver
    page.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined,
        });
    """)
    
    # 2. 伪造 Chrome 运行时对象
    page.add_init_script("""
        window.chrome = {
            runtime: {},
            loadTimes: function() {},
            csi: function() {},
            app: {},
        };
    """)
    
    # 3. 伪造权限查询
    page.add_init_script("""
        const originalQuery = window.navigator.permissions.query;
        window.navigator.permissions.query = (parameters) => (
            parameters.name === 'notifications' ?
            Promise.resolve({ state: Notification.permission }) :
            originalQuery(parameters)
        );
    """)
    
    # 4. 伪造插件列表
    page.add_init_script("""
        Object.defineProperty(navigator, 'plugins', {
            get: () => [1, 2, 3, 4, 5],
        });
    """)
    
    # 5. 伪造语言列表
    page.add_init_script("""
        Object.defineProperty(navigator, 'languages', {
            get: () => ['zh-CN', 'zh', 'en'],
        });
    """)

五、代理 IP 池

import random
import time

class ProxyPool:
    """简易代理 IP 池"""
    
    def __init__(self, proxies: list[str]):
        self.proxies = proxies
        self.failed_proxies: dict[str, float] = {}  # 失败的代理及其冷却时间
        self.cooldown_seconds = 300  # 失败后冷却 5 分钟
    
    def get_proxy(self) -> str:
        """获取一个可用代理"""
        now = time.time()
        available = [
            p for p in self.proxies
            if p not in self.failed_proxies or now > self.failed_proxies[p]
        ]
        
        if not available:
            # 所有代理都失败了,重置冷却
            self.failed_proxies.clear()
            available = self.proxies
        
        return random.choice(available)
    
    def mark_failed(self, proxy: str):
        """标记代理失败"""
        self.failed_proxies[proxy] = time.time() + self.cooldown_seconds


def create_browser_with_proxy(playwright, proxy_config: dict):
    """使用代理创建浏览器"""
    # 代理格式: {"server": "http://proxy_ip:port", "username": "user", "password": "pass"}
    return playwright.chromium.launch(
        headless=True,
        proxy=proxy_config if proxy_config else None,
    )

六、人类行为模拟

import random
import time
import asyncio
from playwright.async_api import Page

class HumanBehavior:
    """模拟人类浏览行为"""
    
    @staticmethod
    async def random_delay(min_ms=500, max_ms=2000):
        """随机延迟,模拟人类阅读和思考时间"""
        delay = random.uniform(min_ms, max_ms) / 1000
        await asyncio.sleep(delay)
    
    @staticmethod
    async def human_type(page: Page, selector: str, text: str):
        """模拟人类打字(逐字输入,带随机间隔)"""
        element = await page.query_selector(selector)
        await element.click()
        
        for char in text:
            await page.keyboard.type(char, delay=random.randint(50, 200))
            # 偶尔停顿(模拟思考)
            if random.random() < 0.1:
                await asyncio.sleep(random.uniform(0.3, 1.0))
    
    @staticmethod
    async def human_scroll(page: Page):
        """模拟人类滚动(不规则的滚动距离和速度)"""
        for _ in range(random.randint(3, 8)):
            scroll_distance = random.randint(200, 800)
            await page.evaluate(f"window.scrollBy(0, {scroll_distance})")
            await asyncio.sleep(random.uniform(1.0, 3.0))
    
    @staticmethod
    async def human_mouse_move(page: Page):
        """模拟鼠标移动轨迹"""
        # 从当前位置移动到随机目标
        x = random.randint(100, 800)
        y = random.randint(100, 600)
        await page.mouse.move(x, y, steps=random.randint(5, 15))

七、完整爬取示例

from playwright.sync_api import sync_playwright

def crawl_36kr_articles():
    """爬取 36 氪文章(教学示例)"""
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = create_stealth_context(browser)
        page = context.new_page()
        
        # 注入反检测脚本
        inject_stealth_scripts(page)
        
        # 访问目标页面
        page.goto("https://36kr.com/", wait_until="networkidle")
        
        # 等待内容加载
        page.wait_for_selector(".article-item", timeout=10000)
        
        # 提取文章列表
        articles = page.evaluate("""
            () => {
                const items = document.querySelectorAll('.article-item');
                return Array.from(items).slice(0, 10).map(item => ({
                    title: item.querySelector('.article-title')?.innerText || '',
                    url: item.querySelector('a')?.href || '',
                    summary: item.querySelector('.article-description')?.innerText || '',
                }));
            }
        """)
        
        context.close()
        browser.close()
        return articles

八、运行

cd code/lesson-49-playwright
uv sync
uv run playwright_advanced.py

九、本课小结

  1. Playwright 是 Selenium 的现代替代品,API 更简洁、反检测更强
  2. 四层反爬策略:指纹伪装 → Stealth 注入 → 代理轮换 → 行为模拟
  3. 浏览器指纹包含 UA、分辨率、时区、语言等十余个维度
  4. 人类行为模拟的关键是「随机性」——固定的延迟反而容易被识别
  5. 反爬是攻防博弈,没有一劳永逸的方案,需要持续更新策略

配套代码:code/lesson-49-playwright/playwright_advanced.py

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐