Chapter 05

浏览器自动化 Agent 设计

结合 Playwright 构建高可靠性的 Web 浏览器 Agent

浏览器 Agent 架构

纯视觉 vs DOM 感知的选择

在浏览器自动化中,有两种截然不同的架构方案,各有优劣:

纯视觉模式
通过截图让 Claude 理解页面,模拟人类鼠标键盘操作。优势:通用性强,任何网页都能处理;劣势:速度慢,精度可能不稳定,成本高。
DOM 感知模式
用 Playwright 获取页面 DOM 结构,让 Claude 基于 HTML 分析页面,再用 Playwright API 精确操作元素。优势:快速、可靠;劣势:对动态渲染页面可能有限制。
混合模式(推荐)
用 Playwright 截图(质量比桌面截图更好)和 DOM 信息,同时提供给 Claude。Claude 可以选择基于坐标操作或基于选择器操作,灵活切换。

Playwright + Computer Use 集成架构

from playwright.async_api import async_playwright, Page
import base64


class BrowserAgent:
    """基于 Playwright 的浏览器 Agent,支持 Computer Use 操作"""

    def __init__(self):
        self.playwright = None
        self.browser = None
        self.page: Optional[Page] = None

    async def __aenter__(self):
        self.playwright = await async_playwright().start()
        self.browser = await self.playwright.chromium.launch(
            headless=False,  # 有界面模式,便于调试
            args=["--start-maximized"]
        )
        context = await self.browser.new_context(
            viewport={"width": 1280, "height": 800}
        )
        self.page = await context.new_page()
        return self

    async def __aexit__(self, *args):
        await self.browser.close()
        await self.playwright.stop()

    async def take_screenshot(self) -> str:
        """截取浏览器截图并返回 base64 字符串"""
        screenshot_bytes = await self.page.screenshot(type="jpeg", quality=80)
        return base64.b64encode(screenshot_bytes).decode()

    async def get_page_context(self) -> str:
        """获取页面的简化 HTML 结构(用于 DOM 感知模式)"""
        # 提取关键元素(按钮、链接、输入框),不返回完整 HTML
        context = await self.page.evaluate("""() => {
            const elements = [];
            const selectors = ['button', 'a', 'input', 'select', 'textarea'];
            selectors.forEach(sel => {
                document.querySelectorAll(sel).forEach(el => {
                    const rect = el.getBoundingClientRect();
                    if (rect.width > 0 && rect.height > 0) {
                        elements.push({
                            tag: el.tagName,
                            text: el.textContent?.trim().slice(0, 50),
                            type: el.type,
                            placeholder: el.placeholder,
                            x: Math.round(rect.x),
                            y: Math.round(rect.y),
                            w: Math.round(rect.width),
                            h: Math.round(rect.height),
                        });
                    }
                });
            });
            return JSON.stringify(elements);
        }""")
        return context

    async def execute_action(self, action: str, **params) -> dict:
        """执行 Computer Use API 的动作"""
        if action == "screenshot":
            img_b64 = await self.take_screenshot()
            return {
                "type": "image",
                "source": {"type": "base64", "media_type": "image/jpeg", "data": img_b64}
            }
        elif action == "left_click":
            x, y = params["coordinate"]
            await self.page.mouse.click(x, y)
            return {"type": "text", "text": f"Clicked ({x}, {y})"}
        elif action == "type":
            await self.page.keyboard.type(params["text"])
            return {"type": "text", "text": "Typed text"}
        elif action == "key":
            await self.page.keyboard.press(params["text"])
            return {"type": "text", "text": f"Pressed {params['text']}"}
        elif action == "scroll":
            x, y = params["coordinate"]
            delta_y = -300 if params.get("direction") == "up" else 300
            await self.page.mouse.wheel(0, delta_y)
            return {"type": "text", "text": "Scrolled"}

登录自动化

处理登录流程

async def login_task(agent: BrowserAgent, username: str, password: str) -> bool:
    """
    AI 驱动的登录流程。
    不硬编码 UI 结构,让 Claude 理解页面后自主操作。
    """
    task = f"""
    请登录到当前页面的系统。
    用户名: {username}
    密码: {password}

    步骤:
    1. 先截图查看当前页面状态
    2. 找到用户名输入框并输入
    3. 找到密码输入框并输入
    4. 点击登录按钮
    5. 截图确认登录成功(查看是否有欢迎信息或进入了主界面)

    如果看到验证码,停止并告知我需要手动处理。
    """

    # 导航到登录页面
    await agent.page.goto("https://example.com/login")

    # 启动 Computer Use Agent 执行登录
    result = await run_browser_agent(agent, task)
    return "登录成功" in result or "welcome" in result.lower()

会话状态持久化

async def create_browser_with_session(session_file: str):
    """创建浏览器,复用已保存的会话状态(避免每次重新登录)"""
    async with async_playwright() as p:
        browser = await p.chromium.launch()

        # 如果有保存的会话,直接使用
        if Path(session_file).exists():
            context = await browser.new_context(
                storage_state=session_file
            )
        else:
            context = await browser.new_context()

        page = await context.new_page()

        # ... 执行任务 ...

        # 任务完成后保存会话状态
        await context.storage_state(path=session_file)
        await browser.close()

错误恢复机制

常见失败场景与恢复策略

页面加载超时
实现重试逻辑:等待 2 秒后重新导航,最多重试 3 次。如果仍然失败,截图并告知 Claude 页面无法加载。
元素点击失败
点击后截图确认。如果页面没有变化,可能是坐标偏移——告知 Claude 重新定位目标元素。
意外弹窗
截图时检测是否有弹窗。Cookie 通知、广告弹窗、确认对话框——让 Claude 根据截图判断如何处理。
CAPTCHA
当检测到 CAPTCHA 时,暂停执行,通过回调通知人类处理,然后继续。可以集成 2captcha 等服务自动解决简单 CAPTCHA。

云端浏览器服务

Browserbase 集成

Browserbase 是专为 AI Agent 设计的云端浏览器服务,解决了本地运行 Playwright 的几个痛点:无头服务器上运行 GUI、IP 轮换、反爬虫绕过、会话录制回放。

from browserbase import Browserbase
from playwright.async_api import async_playwright


async def use_browserbase(task: str):
    bb = Browserbase(api_key="bb_...")

    # 创建云端浏览器会话
    session = bb.sessions.create(project_id="your_project_id")
    connect_url = session.connect_url

    async with async_playwright() as p:
        # 连接到云端浏览器,而不是本地
        browser = await p.chromium.connect_over_cdp(connect_url)
        page = browser.contexts[0].pages[0]

        # 正常使用 Playwright API
        await page.goto("https://example.com")

        # ... Computer Use Agent 逻辑 ...
何时使用云端浏览器

本地 Playwright 适合开发和测试阶段。生产环境建议使用云端浏览器服务(Browserbase、Steel.dev),原因:IP 不固定、无需维护服务器 GUI 环境、更好的并发支持、内置操作录像功能(便于调试)。

反爬虫检测与绕过

为什么 Playwright 默认会被检测为机器人

现代网站使用多种技术检测自动化工具,了解这些机制有助于构建更稳健的浏览器 Agent:

Webdriver 属性
Playwright 启动的浏览器中,navigator.webdriver 属性默认为 true,这是最直接的机器人标识。可以通过 Playwright 的隐身模式或注入脚本将其设为 undefined
User Agent 异常
Playwright 使用的 User Agent 包含 "HeadlessChrome" 字样,很多网站会拒绝此类请求。应手动设置为真实浏览器的 User Agent 字符串。
行为指纹
鼠标轨迹完全直线(无加速度变化)、点击间隔毫秒级精确、从不滚动——这些都是机器人的行为特征。增加随机延迟和鼠标轨迹变化可以降低被检测的概率。
Canvas/WebGL 指纹
无头浏览器的 Canvas 渲染结果与真实浏览器有细微差异,反爬虫系统会比对这个"指纹"来识别机器人。使用 Playwright 的有头模式(headless=False)可以避免此问题。
from playwright.async_api import async_playwright


async def create_stealth_browser():
    """
    创建带反检测配置的浏览器实例。
    注意:这些技术仅用于合法的自动化用途(如测试自己的网站)。
    """
    playwright = await async_playwright().start()
    browser = await playwright.chromium.launch(
        headless=False,      # 有头模式,避免 headless 检测
        args=[
            "--disable-blink-features=AutomationControlled",  # 禁用自动化控制标识
            "--no-sandbox",   # Docker 容器中需要
            "--start-maximized"
        ]
    )

    context = await browser.new_context(
        # 使用真实 User Agent
        user_agent=(
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/121.0.0.0 Safari/537.36"
        ),
        viewport={"width": 1280, "height": 800},
        locale="zh-CN",
        timezone_id="Asia/Shanghai",
    )

    page = await context.new_page()

    # 注入脚本:隐藏 webdriver 属性
    await page.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });
        // 模拟真实的插件列表
        Object.defineProperty(navigator, 'plugins', {
            get: () => [1, 2, 3, 4, 5]
        });
    """)

    return browser, context, page

页面状态识别模式

如何判断页面处于什么状态

浏览器 Agent 需要识别页面的多种状态,并做出相应的处理决策。以下是常见状态的识别模式:

from playwright.async_api import Page
import base64


class PageStateAnalyzer:
    """浏览器页面状态分析器"""

    def __init__(self, page: Page):
        self.page = page

    async def is_loading(self) -> bool:
        """检查页面是否正在加载"""
        try:
            return await self.page.evaluate("document.readyState !== 'complete'")
        except:
            return True

    async def has_error_page(self) -> bool:
        """检查是否是错误页面(404、503、网络错误等)"""
        url = self.page.url
        title = await self.page.title()
        error_indicators = [
            "ERR_", "404", "503", "Not Found",
            "Access Denied", "Forbidden"
        ]
        return any(ind in title or ind in url for ind in error_indicators)

    async def has_login_required(self) -> bool:
        """检查是否需要登录"""
        # 方法1:检查 URL 是否包含 login 关键词
        url = self.page.url
        if any(k in url.lower() for k in ["login", "signin", "auth"]):
            return True
        # 方法2:检查页面是否有密码输入框
        password_input = await self.page.query_selector("input[type='password']")
        return password_input is not None

    async def detect_captcha(self) -> bool:
        """检测常见的 CAPTCHA 类型"""
        # reCAPTCHA
        recaptcha = await self.page.query_selector(".g-recaptcha, #recaptcha")
        # hCaptcha
        hcaptcha = await self.page.query_selector(".h-captcha")
        # Cloudflare Turnstile
        turnstile = await self.page.query_selector(".cf-turnstile")
        return any([recaptcha, hcaptcha, turnstile])

    async def get_page_summary(self) -> str:
        """
        生成页面摘要(非截图),用于提示词中帮助 Claude 理解页面结构。
        比截图更省 token,适合在高频判断场景使用。
        """
        title = await self.page.title()
        url = self.page.url
        # 获取页面可见文本(截取前 500 字)
        text = await self.page.evaluate("document.body.innerText")
        text_preview = text[:500] if text else ""
        return f"""页面状态摘要:
URL: {url}
标题: {title}
文本预览(前500字): {text_preview}
加载状态: {"加载中" if await self.is_loading() else "已完成"}
"""
章节小结

本章讲解了浏览器自动化 Agent 的设计模式和关键技术。核心要点: