AI浏览器自动化与Web Agent完全教程

教程简介

本教程全面讲解AI浏览器自动化与Web Agent的核心技术,涵盖Browser Use框架、Playwright自动化、Computer Use多模态操作、网页元素识别与交互、表单自动填写、数据采集与爬取、视觉定位与点击、多步骤任务规划、错误恢复机制等核心内容,通过完整的自动化数据采集Agent案例帮助开发者掌握AI驱动的浏览器自动化技术。

AI浏览器自动化与Web Agent完全教程

本教程全面讲解AI浏览器自动化与Web Agent的核心技术,通过丰富的代码示例和实战案例,帮助开发者掌握AI驱动的浏览器自动化技术。


目录

  1. 概述与背景
  2. 核心技术栈
  3. Playwright自动化基础
  4. Browser Use框架详解
  5. Computer Use多模态操作
  6. 网页元素识别与交互
  7. 表单自动填写
  8. 数据采集与爬取
  9. 视觉定位与点击
  10. 多步骤任务规划
  11. 错误恢复机制
  12. 实战:自动化数据采集Agent
  13. 最佳实践与注意事项
  14. 总结

概述与背景

传统的浏览器自动化依赖于Selenium、Puppeteer等工具,通过CSS选择器、XPath等方式定位网页元素。这种方式虽然有效,但存在明显的局限性:当网页结构发生变化时,脚本往往会失效;面对复杂的动态页面,编写和维护选择器的成本很高。

AI浏览器自动化的出现改变了这一局面。通过将大语言模型(LLM)与浏览器控制相结合,我们可以用自然语言描述任务,让AI Agent自主理解页面内容、规划操作步骤、执行交互动作。这种范式转变使得自动化脚本更加鲁棒、灵活,能够适应网页的变化。

AI浏览器自动化的核心优势:

  • 自然语言驱动:用自然语言描述任务,无需编写复杂的选择器
  • 视觉理解能力:AI可以"看懂"页面内容,理解语义关系
  • 自适应能力:页面结构变化时,AI能自动调整操作策略
  • 复杂推理能力:处理多步骤、有依赖关系的复杂任务

目前主流的AI浏览器自动化技术包括:

  • Browser Use:基于Playwright的AI Agent框架
  • Computer Use:Anthropic的多模态计算机操作能力
  • WebVoyager:多模态Web导航Agent
  • SeeAct:基于视觉语言模型的网页操作

核心技术栈

在深入学习之前,我们先了解一下AI浏览器自动化涉及的核心技术栈:

┌─────────────────────────────────────────────┐
│              用户自然语言指令                  │
├─────────────────────────────────────────────┤
│           AI Agent (LLM核心)                 │
│   ┌──────────┬──────────┬──────────────┐    │
│   │ 任务规划  │ 元素理解  │ 动作决策      │    │
│   └──────────┴──────────┴──────────────┘    │
├─────────────────────────────────────────────┤
│          浏览器控制层                          │
│   ┌──────────┬──────────┬──────────────┐    │
│   │ Playwright│ Selenium │ Chrome DevTools│   │
│   └──────────┴──────────┴──────────────┘    │
├─────────────────────────────────────────────┤
│          网页渲染层                            │
│   ┌──────────┬──────────┬──────────────┐    │
│   │ Chrome    │ Firefox  │   WebKit     │    │
│   └──────────┴──────────┴──────────────┘    │
└─────────────────────────────────────────────┘

关键依赖库:

# 安装核心依赖
pip install playwright browser-use langchain-openai langchain-anthropic

# 安装Playwright浏览器
playwright install chromium

# 可选:安装其他依赖
pip install beautifulsoup4 lxml pillow

Playwright自动化基础

Playwright是微软开发的现代浏览器自动化库,支持Chromium、Firefox和WebKit三大浏览器引擎。它是Browser Use等AI Agent框架的底层基础。

基础操作

import asyncio
from playwright.async_api import async_playwright

async def basic_demo():
    """Playwright基础操作演示"""
    async with async_playwright() as p:
        # 启动浏览器
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context(
            viewport={"width": 1280, "height": 720},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        )
        page = await context.new_page()

        # 导航到目标页面
        await page.goto("https://example.com", wait_until="networkidle")

        # 等待元素出现
        await page.wait_for_selector("h1", timeout=10000)

        # 获取页面标题
        title = await page.title()
        print(f"页面标题: {title}")

        # 获取元素文本
        heading = await page.inner_text("h1")
        print(f"标题文本: {heading}")

        # 截图
        await page.screenshot(path="screenshot.png", full_page=True)

        # 关闭浏览器
        await browser.close()

asyncio.run(basic_demo())

元素定位与交互

async def element_interaction():
    """元素定位与交互演示"""
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        page = await browser.new_page()
        await page.goto("https://example.com/form")

        # 多种定位方式
        # 1. CSS选择器
        button = page.locator("button.submit-btn")

        # 2. 文本内容定位
        link = page.get_by_text("了解更多")

        # 3. 角色定位(推荐的语义化定位方式)
        login_button = page.get_by_role("button", name="登录")

        # 4. 标签定位
        email_input = page.get_by_label("邮箱地址")

        # 5. 占位符定位
        search_input = page.get_by_placeholder("搜索...")

        # 6. 测试ID定位
        submit = page.get_by_test_id("submit-button")

        # 执行交互
        await email_input.fill("user@example.com")
        await search_input.fill("Playwright自动化")
        await login_button.click()

        # 等待导航完成
        await page.wait_for_load_state("networkidle")

        await browser.close()

高级功能:网络拦截与请求监听

async def network_interception():
    """网络请求拦截与监听"""
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        page = await browser.new_page()

        # 监听所有网络请求
        requests_log = []

        def on_request(request):
            requests_log.append({
                "url": request.url,
                "method": request.method,
                "resource_type": request.resource_type
            })

        page.on("request", on_request)

        # 拦截特定请求
        async def handle_route(route):
            if route.request.resource_type == "image":
                await route.abort()  # 阻止图片加载,加速爬取
            else:
                await route.continue_()

        await page.route("**/*", handle_route)

        await page.goto("https://news.ycombinator.com")
        await page.wait_for_load_state("networkidle")

        print(f"捕获到 {len(requests_log)} 个请求")
        for req in requests_log[:5]:
            print(f"  {req['method']} {req['url'][:80]}")

        await browser.close()

Browser Use框架详解

Browser Use是一个将AI Agent与浏览器自动化相结合的开源框架。它封装了Playwright的复杂性,让开发者可以用自然语言驱动浏览器操作。

架构设计

Browser Use的核心架构包含以下组件:

  • Agent:核心调度器,负责任务理解、步骤规划和执行控制
  • Browser:浏览器实例管理,封装Playwright操作
  • Controller:动作注册与执行,定义Agent可执行的操作
  • DOM Service:网页内容提取与元素映射
  • Message Manager:管理与LLM的对话上下文

基础使用

import asyncio
from langchain_openai import ChatOpenAI
from browser_use import Agent, Browser, BrowserConfig

async def basic_browser_use():
    """Browser Use基础用法"""

    # 配置浏览器
    browser_config = BrowserConfig(
        headless=False,
        disable_security=True,  # 禁用同源策略(仅用于开发)
        extra_chromium_args=[
            "--window-size=1280,720",
            "--disable-blink-features=AutomationControlled"
        ]
    )

    # 初始化LLM
    llm = ChatOpenAI(
        model="gpt-4o",
        temperature=0
    )

    # 创建Agent
    agent = Agent(
        task="打开Google,搜索 'Python browser automation',获取前3个搜索结果的标题和链接",
        llm=llm,
        browser=Browser(config=browser_config),
        max_actions_per_step=5  # 每步最多执行5个动作
    )

    # 执行任务
    result = await agent.run(max_steps=20)

    print("任务执行结果:")
    print(result)

asyncio.run(basic_browser_use())

自定义工具与动作

Browser Use允许开发者注册自定义工具,扩展Agent的能力:

from browser_use import Agent, Controller
from langchain_openai import ChatOpenAI
from pydantic import BaseModel

# 定义输出格式
class ProductInfo(BaseModel):
    name: str
    price: str
    rating: str
    url: str

class ProductList(BaseModel):
    products: list[ProductInfo]

# 创建控制器并注册自定义动作
controller = Controller()

@controller.action("Save product data to file", param_model=ProductList)
async def save_products(data: ProductList):
    """将产品数据保存到文件"""
    with open("products.json", "w", encoding="utf-8") as f:
        import json
        json.dump(
            [p.model_dump() for p in data.products],
            f,
            ensure_ascii=False,
            indent=2
        )
    return f"已保存 {len(data.products)} 个产品信息"

@controller.action("Take a screenshot with custom name")
async def take_screenshot(browser, name: str):
    """自定义截图功能"""
    page = await browser.get_current_page()
    await page.screenshot(path=f"screenshots/{name}.png")
    return f"截图已保存: screenshots/{name}.png"

@controller.action("Extract all links from current page")
async def extract_links(browser):
    """提取当前页面所有链接"""
    page = await browser.get_current_page()
    links = await page.evaluate("""
        () => Array.from(document.querySelectorAll('a[href]')).map(a => ({
            text: a.innerText.trim(),
            href: a.href
        })).filter(l => l.text && l.href.startsWith('http'))
    """)
    return {"links": links, "count": len(links)}

async def custom_tools_demo():
    """自定义工具演示"""
    llm = ChatOpenAI(model="gpt-4o", temperature=0)

    agent = Agent(
        task="访问电商网站,搜索'机械键盘',提取前5个产品的名称、价格、评分和链接,然后保存到文件",
        llm=llm,
        controller=controller,
        max_actions_per_step=3
    )

    result = await agent.run(max_steps=30)
    print(result)

多标签页管理

async def multi_tab_demo():
    """多标签页操作演示"""
    from browser_use import Browser

    browser = Browser()

    async with browser:
        # 创建多个标签页
        page1 = await browser.new_page()
        await page1.goto("https://example.com")

        page2 = await browser.new_page()
        await page2.goto("https://example.org")

        # 获取所有标签页
        pages = await browser.get_pages()
        print(f"当前有 {len(pages)} 个标签页")

        # 切换标签页
        await browser.switch_to_tab(0)  # 切换到第一个标签页

        # 在不同标签页中执行操作
        title1 = await page1.title()
        title2 = await page2.title()
        print(f"标签页1: {title1}")
        print(f"标签页2: {title2}")

        # 关闭指定标签页
        await browser.close_tab(1)

Computer Use多模态操作

Anthropic的Computer Use能力让Claude能够通过截图理解计算机屏幕,并执行鼠标点击、键盘输入等操作。这是一种完全基于视觉的交互方式。

工作原理

Computer Use的工作流程:

  1. 截取屏幕截图发送给Claude
  2. Claude分析截图,确定需要执行的操作
  3. 返回具体的操作指令(点击坐标、按键等)
  4. 执行操作,再次截图,循环直到任务完成

实现Computer Use Agent

import anthropic
import pyautogui
import base64
import time
from PIL import Image

class ComputerUseAgent:
    """基于Anthropic Computer Use的Agent"""

    def __init__(self, api_key: str):
        self.client = anthropic.Anthropic(api_key=api_key)
        self.screen_width, self.screen_height = pyautogui.size()

    def take_screenshot(self) -> str:
        """截取屏幕并返回base64编码"""
        screenshot = pyautogui.screenshot()
        # 缩放到合理大小
        screenshot = screenshot.resize((1280, 800))
        import io
        buffer = io.BytesIO()
        screenshot.save(buffer, format="PNG")
        return base64.standard_b64encode(buffer.getvalue()).decode("utf-8")

    def execute_action(self, action: dict):
        """执行Claude返回的操作指令"""
        action_type = action.get("type")

        if action_type == "mouse_move":
            x, y = action["coordinate"]
            pyautogui.moveTo(x, y, duration=0.3)

        elif action_type == "left_click":
            x, y = action["coordinate"]
            pyautogui.click(x, y)

        elif action_type == "double_click":
            x, y = action["coordinate"]
            pyautogui.doubleClick(x, y)

        elif action_type == "right_click":
            x, y = action["coordinate"]
            pyautogui.rightClick(x, y)

        elif action_type == "type":
            text = action["text"]
            pyautogui.typewrite(text, interval=0.05)

        elif action_type == "key":
            key = action["key"]
            pyautogui.press(key)

        elif action_type == "scroll":
            x, y = action["coordinate"]
            direction = action["direction"]
            amount = action.get("amount", 3)
            pyautogui.moveTo(x, y)
            if direction == "down":
                pyautogui.scroll(-amount)
            else:
                pyautogui.scroll(amount)

        elif action_type == "screenshot":
            pass  # 截图操作在主循环中处理

    async def run_task(self, task: str, max_turns: int = 20):
        """执行自然语言任务"""
        messages = []

        for turn in range(max_turns):
            # 截取当前屏幕
            screenshot_b64 = self.take_screenshot()

            # 构建消息
            if turn == 0:
                messages.append({
                    "role": "user",
                    "content": [
                        {
                            "type": "text",
                            "text": f"请完成以下任务: {task}"
                        },
                        {
                            "type": "image",
                            "source": {
                                "type": "base64",
                                "media_type": "image/png",
                                "data": screenshot_b64
                            }
                        }
                    ]
                })
            else:
                messages.append({
                    "role": "user",
                    "content": [
                        {
                            "type": "image",
                            "source": {
                                "type": "base64",
                                "media_type": "image/png",
                                "data": screenshot_b64
                            }
                        },
                        {
                            "type": "text",
                            "text": "这是当前屏幕截图,请继续执行任务。"
                        }
                    ]
                })

            # 调用Claude
            response = self.client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                tools=[{
                    "type": "computer_20250124",
                    "name": "computer",
                    "display_width_px": 1280,
                    "display_height_px": 800,
                    "display_number": 1
                }],
                messages=messages
            )

            # 处理响应
            assistant_content = response.content
            messages.append({"role": "assistant", "content": assistant_content})

            # 检查是否完成
            if response.stop_reason == "end_turn":
                text_blocks = [b for b in assistant_content if b.type == "text"]
                if text_blocks:
                    print(f"任务完成: {text_blocks[0].text}")
                return

            # 执行工具调用
            tool_results = []
            for block in assistant_content:
                if block.type == "tool_use":
                    print(f"执行操作: {block.name} - {block.input}")
                    self.execute_action(block.input)
                    time.sleep(0.5)  # 等待操作生效
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": [{"type": "text", "text": "操作已执行"}]
                    })

            if tool_results:
                messages.append({"role": "user", "content": tool_results})

        print("达到最大轮次限制")

混合方案:Playwright + Computer Use

import asyncio
from playwright.async_api import async_playwright

class HybridBrowserAgent:
    """结合Playwright精确控制和Computer Use视觉理解的混合Agent"""

    def __init__(self, llm):
        self.llm = llm

    async def analyze_page_visually(self, page):
        """通过截图让AI分析页面"""
        screenshot = await page.screenshot(type="png")
        import base64
        screenshot_b64 = base64.b64encode(screenshot).decode()

        # 使用多模态LLM分析截图
        response = await self.llm.ainvoke([
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": "分析这个网页截图,识别主要的交互元素和它们的大致位置"},
                    {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{screenshot_b64}"}}
                ]
            }
        ])
        return response.content

    async def precise_interact(self, page, selector, action, value=None):
        """使用Playwright进行精确的元素交互"""
        element = page.locator(selector)

        if action == "click":
            await element.click()
        elif action == "fill":
            await element.fill(value)
        elif action == "select":
            await element.select_option(value)
        elif action == "hover":
            await element.hover()

        # 操作后等待页面稳定
        await page.wait_for_load_state("networkidle")

    async def smart_interact(self, page, description):
        """当无法通过选择器定位时,使用视觉定位"""
        # 获取页面可交互元素的坐标
        elements = await page.evaluate("""
            () => {
                const interactive = document.querySelectorAll(
                    'a, button, input, select, textarea, [role="button"], [onclick]'
                );
                return Array.from(interactive).map(el => {
                    const rect = el.getBoundingClientRect();
                    return {
                        tag: el.tagName,
                        text: el.innerText?.substring(0, 50),
                        type: el.type || '',
                        x: rect.x + rect.width / 2,
                        y: rect.y + rect.height / 2,
                        width: rect.width,
                        height: rect.height
                    };
                }).filter(el => el.width > 0 && el.height > 0);
            }
        """)

        # 让AI选择最匹配的元素
        response = await self.llm.ainvoke([
            {"role": "system", "content": "你是网页元素识别专家。根据用户描述和元素列表,返回最匹配元素的索引。只返回数字。"},
            {"role": "user", "content": f"描述: {description}\n元素列表: {elements}"}
        ])

        try:
            index = int(response.content.strip())
            target = elements[index]
            await page.mouse.click(target["x"], target["y"])
            return True
        except (ValueError, IndexError):
            return False

网页元素识别与交互

在AI浏览器自动化中,准确识别和理解网页元素是关键。现代方案结合了DOM解析、视觉理解和语义分析。

DOM内容提取

async def extract_page_content(page) -> dict:
    """提取页面的结构化内容"""

    # 提取文本内容
    text_content = await page.evaluate("""
        () => {
            // 获取所有可见文本
            const walker = document.createTreeWalker(
                document.body,
                NodeFilter.SHOW_TEXT,
                {
                    acceptNode: (node) => {
                        const parent = node.parentElement;
                        if (!parent) return NodeFilter.FILTER_REJECT;
                        const style = window.getComputedStyle(parent);
                        if (style.display === 'none' || style.visibility === 'hidden')
                            return NodeFilter.FILTER_REJECT;
                        return NodeFilter.FILTER_ACCEPT;
                    }
                }
            );
            const texts = [];
            while (walker.nextNode()) {
                const text = walker.currentNode.textContent.trim();
                if (text) texts.push(text);
            }
            return texts;
        }
    """)

    # 提取交互元素
    interactive_elements = await page.evaluate("""
        () => {
            const selectors = [
                'a[href]', 'button', 'input', 'select', 'textarea',
                '[role="button"]', '[role="link"]', '[role="tab"]',
                '[onclick]', '[tabindex]'
            ];
            const elements = [];
            const seen = new Set();

            for (const sel of selectors) {
                document.querySelectorAll(sel).forEach(el => {
                    if (seen.has(el)) return;
                    seen.add(el);

                    const rect = el.getBoundingClientRect();
                    if (rect.width === 0 || rect.height === 0) return;

                    elements.push({
                        tag: el.tagName.toLowerCase(),
                        text: el.innerText?.substring(0, 100)?.trim() || '',
                        href: el.href || '',
                        type: el.type || '',
                        name: el.name || '',
                        id: el.id || '',
                        placeholder: el.placeholder || '',
                        ariaLabel: el.getAttribute('aria-label') || '',
                        role: el.getAttribute('role') || '',
                        rect: {
                            x: Math.round(rect.x),
                            y: Math.round(rect.y),
                            width: Math.round(rect.width),
                            height: Math.round(rect.height)
                        }
                    });
                });
            }
            return elements;
        }
    """)

    # 提取页面元数据
    metadata = await page.evaluate("""
        () => ({
            title: document.title,
            url: window.location.href,
            forms: Array.from(document.forms).map(f => ({
                action: f.action,
                method: f.method,
                fields: Array.from(f.elements).map(e => ({
                    name: e.name,
                    type: e.type,
                    value: e.value
                }))
            }))
        })
    """)

    return {
        "text": text_content,
        "elements": interactive_elements,
        "metadata": metadata
    }

基于AI的元素理解

async def ai_element_understanding(llm, page_content: dict, user_intent: str) -> dict:
    """使用AI理解页面元素与用户意图的关系"""

    prompt = f"""你是一个网页分析专家。根据以下页面信息和用户意图,找出最相关的交互元素。

用户意图: {user_intent}

页面交互元素:
{json.dumps(page_content['elements'], ensure_ascii=False, indent=2)}

请返回JSON格式,包含:
- target_element: 目标元素的详细信息
- action: 建议执行的操作 (click/fill/select/hover)
- value: 如果需要填入的值
- confidence: 置信度 (0-1)
- reasoning: 选择理由

只返回JSON,不要其他内容。"""

    response = await llm.ainvoke([{"role": "user", "content": prompt}])
    return json.loads(response.content)

表单自动填写

表单自动填写是AI浏览器自动化的重要应用场景。AI可以理解表单结构,自动匹配字段并填入正确的内容。

智能表单填写器

import asyncio
from dataclasses import dataclass
from typing import Any

@dataclass
class FormData:
    """表单数据模型"""
    fields: dict[str, Any]
    metadata: dict[str, Any] = None

class SmartFormFiller:
    """AI驱动的智能表单填写器"""

    def __init__(self, llm, browser):
        self.llm = llm
        self.browser = browser

    async def analyze_form(self, page) -> list[dict]:
        """分析页面中的表单结构"""
        form_info = await page.evaluate("""
            () => {
                const forms = [];
                document.querySelectorAll('form').forEach(form => {
                    const fields = [];
                    form.querySelectorAll('input, select, textarea').forEach(el => {
                        const rect = el.getBoundingClientRect();
                        const label = el.labels?.[0]?.innerText?.trim() ||
                                     el.getAttribute('aria-label') ||
                                     el.getAttribute('placeholder') ||
                                     el.name || el.id;

                        fields.push({
                            name: el.name,
                            id: el.id,
                            type: el.type || el.tagName.toLowerCase(),
                            label: label,
                            required: el.required,
                            value: el.value,
                            options: el.tagName === 'SELECT'
                                ? Array.from(el.options).map(o => ({
                                    value: o.value,
                                    text: o.text
                                  }))
                                : [],
                            rect: {
                                x: rect.x + rect.width / 2,
                                y: rect.y + rect.height / 2
                            }
                        });
                    });
                    forms.append({
                        action: form.action,
                        method: form.method,
                        fields: fields
                    });
                });
                return forms;
            }
        """)
        return form_info

    async def match_fields(self, form_fields: list, user_data: dict) -> dict:
        """使用AI匹配表单字段与用户数据"""
        prompt = f"""将用户数据匹配到表单字段。

用户数据:
{json.dumps(user_data, ensure_ascii=False, indent=2)}

表单字段:
{json.dumps(form_fields, ensure_ascii=False, indent=2)}

返回JSON格式的匹配结果,key为字段name或id,value为要填入的值。
对于select字段,返回最匹配的option value。
对于无法匹配的字段,value设为null。

只返回JSON。"""

        response = await self.llm.ainvoke([{"role": "user", "content": prompt}])
        return json.loads(response.content)

    async def fill_form(self, page, form_index: int, field_values: dict):
        """填写表单"""
        forms = await self.analyze_form(page)
        if form_index >= len(forms):
            raise ValueError(f"表单索引 {form_index} 超出范围")

        form = forms[form_index]

        for field in form["fields"]:
            field_name = field["name"] or field["id"]
            value = field_values.get(field_name)
            if value is None:
                continue

            # 构建选择器
            if field["id"]:
                selector = f"#{field['id']}"
            elif field["name"]:
                selector = f"[name='{field['name']}']"
            else:
                # 使用坐标点击
                await page.mouse.click(field["rect"]["x"], field["rect"]["y"])
                continue

            element = page.locator(selector)

            field_type = field["type"]
            if field_type in ("text", "email", "tel", "url", "password", "number"):
                await element.fill(str(value))
            elif field_type == "select":
                await element.select_option(value=str(value))
            elif field_type == "checkbox":
                if value and not await element.is_checked():
                    await element.check()
                elif not value and await element.is_checked():
                    await element.uncheck()
            elif field_type == "radio":
                await element.check()
            elif field_type == "textarea":
                await element.fill(str(value))

            await asyncio.sleep(0.3)  # 模拟人工输入间隔

    async def fill_form_natural(self, page, description: str, user_data: dict):
        """自然语言驱动的表单填写"""
        # 分析表单
        forms = await self.analyze_form(page)
        if not forms:
            print("未找到表单")
            return

        # AI匹配字段
        matched_fields = await self.match_fields(forms[0]["fields"], user_data)

        # 过滤null值
        valid_fields = {k: v for k, v in matched_fields.items() if v is not None}
        print(f"匹配到 {len(valid_fields)} 个字段")

        # 填写表单
        await self.fill_form(page, 0, valid_fields)
        print("表单填写完成")

数据采集与爬取

AI驱动的数据采集相比传统爬虫更加智能和灵活,能够处理复杂的页面结构和反爬机制。

智能数据采集Agent

import asyncio
import json
from datetime import datetime

class AIDataCollector:
    """AI驱动的智能数据采集器"""

    def __init__(self, llm, browser):
        self.llm = llm
        self.browser = browser
        self.collected_data = []

    async def collect_from_page(self, page, schema: dict) -> list[dict]:
        """从当前页面按schema提取数据"""

        # 获取页面HTML结构
        html = await page.content()

        # 使用AI提取数据
        prompt = f"""从以下HTML中提取数据,按照给定的schema格式化。

Schema: {json.dumps(schema, ensure_ascii=False)}

HTML片段(前5000字符):
{html[:5000]}

返回JSON数组,每个元素对应一条数据记录。
只返回JSON,不要其他内容。"""

        response = await self.llm.ainvoke([{"role": "user", "content": prompt}])

        try:
            data = json.loads(response.content)
            return data if isinstance(data, list) else [data]
        except json.JSONDecodeError:
            print(f"AI返回的JSON解析失败: {response.content[:200]}")
            return []

    async def auto_paginate(self, page, next_button_selector: str = None) -> bool:
        """自动翻页"""
        if next_button_selector:
            next_btn = page.locator(next_button_selector)
            if await next_btn.count() > 0 and await next_btn.is_enabled():
                await next_btn.click()
                await page.wait_for_load_state("networkidle")
                return True

        # AI自动寻找翻页按钮
        page_info = await page.evaluate("""
            () => {
                const candidates = document.querySelectorAll(
                    'a.next, button.next, [aria-label="Next"], [aria-label="下一页"], a[rel="next"]'
                );
                return Array.from(candidates).map(el => ({
                    tag: el.tagName,
                    text: el.innerText,
                    href: el.href,
                    disabled: el.disabled
                }));
            }
        """)

        if page_info:
            next_link = page.locator("a.next, a[rel='next'], [aria-label='Next'], [aria-label='下一页']").first
            if await next_link.count() > 0:
                await next_link.click()
                await page.wait_for_load_state("networkidle")
                return True

        return False

    async def collect_with_pagination(self, page, schema: dict,
                                       max_pages: int = 10,
                                       next_selector: str = None) -> list[dict]:
        """带翻页的数据采集"""
        all_data = []

        for page_num in range(max_pages):
            print(f"正在采集第 {page_num + 1} 页...")

            # 采集当前页数据
            page_data = await self.collect_from_page(page, schema)
            all_data.extend(page_data)
            print(f"  采集到 {len(page_data)} 条数据")

            # 尝试翻页
            has_next = await self.auto_paginate(page, next_selector)
            if not has_next:
                print("没有更多页面")
                break

            await asyncio.sleep(1)  # 礼貌性延迟

        self.collected_data.extend(all_data)
        return all_data

    async def save_data(self, filename: str, format: str = "json"):
        """保存采集到的数据"""
        if format == "json":
            with open(filename, "w", encoding="utf-8") as f:
                json.dump(self.collected_data, f, ensure_ascii=False, indent=2)
        elif format == "csv":
            import csv
            if self.collected_data:
                keys = self.collected_data[0].keys()
                with open(filename, "w", encoding="utf-8-sig", newline="") as f:
                    writer = csv.DictWriter(f, fieldnames=keys)
                    writer.writeheader()
                    writer.writerows(self.collected_data)

        print(f"数据已保存到 {filename},共 {len(self.collected_data)} 条")

反检测策略

async def anti_detection_setup(browser_context):
    """设置反检测策略"""

    # 注入反检测脚本
    await browser_context.add_init_script("""
        // 隐藏webdriver标识
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });

        // 模拟真实浏览器的plugins
        Object.defineProperty(navigator, 'plugins', {
            get: () => [
                { name: 'Chrome PDF Plugin' },
                { name: 'Chrome PDF Viewer' },
                { name: 'Native Client' }
            ]
        });

        // 修改navigator.languages
        Object.defineProperty(navigator, 'languages', {
            get: () => ['zh-CN', 'zh', 'en-US', 'en']
        });

        // 隐藏自动化特征
        window.chrome = {
            runtime: {},
            loadTimes: function() {},
            csi: function() {},
            app: {}
        };

        // 修改权限查询
        const originalQuery = window.navigator.permissions.query;
        window.navigator.permissions.query = (parameters) => (
            parameters.name === 'notifications'
                ? Promise.resolve({ state: Notification.permission })
                : originalQuery(parameters)
        );
    """)

    # 设置真实的请求头
    await browser_context.set_extra_http_headers({
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive",
        "Upgrade-Insecure-Requests": "1"
    })

    # 随机化视口大小
    import random
    width = random.randint(1200, 1400)
    height = random.randint(700, 900)
    await browser_context.set_viewport_size({"width": width, "height": height})

    return browser_context

视觉定位与点击

当DOM结构不可靠或元素无法通过选择器定位时,视觉定位成为关键能力。

截图标注与坐标定位

from PIL import Image, ImageDraw, ImageFont
import base64

class VisualLocator:
    """视觉定位器"""

    def __init__(self, llm):
        self.llm = llm

    async def locate_element_by_screenshot(self, page, description: str) -> tuple[int, int]:
        """通过截图和AI定位元素坐标"""

        # 截取页面
        screenshot = await page.screenshot(type="png")
        img = Image.open(io.BytesIO(screenshot))
        width, height = img.size

        # 转为base64
        screenshot_b64 = base64.b64encode(screenshot).decode()

        # 使用多模态AI定位
        response = await self.llm.ainvoke([
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": f"""请在这个截图中找到"{description}"的位置。
返回JSON格式: {{"x": 像素坐标x, "y": 像素坐标y, "confidence": 置信度0-1}}
图片尺寸: {width}x{height}
只返回JSON。"""
                    },
                    {
                        "type": "image_url",
                        "image_url": {"url": f"data:image/png;base64,{screenshot_b64}"}
                    }
                ]
            }
        ])

        result = json.loads(response.content)
        return (result["x"], result["y"]), result.get("confidence", 0.5)

    def annotate_screenshot(self, image: Image.Image,
                            points: list[dict]) -> Image.Image:
        """在截图上标注定位点"""
        draw = ImageDraw.Draw(image)

        for point in points:
            x, y = point["x"], point["y"]
            color = point.get("color", "red")
            label = point.get("label", "")

            # 画十字标记
            size = 15
            draw.line([(x - size, y), (x + size, y)], fill=color, width=2)
            draw.line([(x, y - size), (x, y + size)], fill=color, width=2)

            # 画圆圈
            draw.ellipse(
                [(x - size, y - size), (x + size, y + size)],
                outline=color, width=2
            )

            # 添加标签
            if label:
                draw.text((x + size + 5, y - 10), label, fill=color)

        return image

    async def click_by_visual(self, page, description: str, llm):
        """通过视觉定位并点击"""
        coords, confidence = await self.locate_element_by_screenshot(page, description)

        if confidence < 0.3:
            print(f"警告: 视觉定位置信度较低 ({confidence})")
            return False

        x, y = coords
        print(f"点击坐标: ({x}, {y}), 置信度: {confidence}")

        # 移动鼠标到目标位置
        await page.mouse.move(x, y)
        await asyncio.sleep(0.2)

        # 点击
        await page.mouse.click(x, y)
        return True

多步骤任务规划

复杂的自动化任务需要合理的规划和分解。AI Agent需要理解任务的依赖关系,制定执行计划。

任务规划器

from dataclasses import dataclass, field
from enum import Enum

class TaskStatus(Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    SKIPPED = "skipped"

@dataclass
class TaskStep:
    """任务步骤"""
    id: int
    description: str
    action: str
    params: dict
    status: TaskStatus = TaskStatus.PENDING
    result: Any = None
    error: str = None
    dependencies: list[int] = field(default_factory=list)
    retry_count: int = 0
    max_retries: int = 3

class TaskPlanner:
    """AI任务规划器"""

    def __init__(self, llm):
        self.llm = llm
        self.steps: list[TaskStep] = []
        self.context: dict = {}

    async def create_plan(self, task_description: str, page_info: dict = None) -> list[TaskStep]:
        """根据任务描述创建执行计划"""

        context_str = ""
        if page_info:
            context_str = f"\n当前页面信息:\n{json.dumps(page_info, ensure_ascii=False, indent=2)}"

        prompt = f"""请将以下任务分解为具体的执行步骤。

任务描述: {task_description}
{context_str}

返回JSON数组,每个步骤包含:
- description: 步骤描述
- action: 动作类型 (navigate/click/fill/extract/scroll/wait/conditional)
- params: 动作参数
- dependencies: 依赖的步骤索引数组

步骤应该具体、可执行、有明确的输入输出。"""

        response = await self.llm.ainvoke([{"role": "user", "content": prompt}])
        plan_data = json.loads(response.content)

        self.steps = []
        for i, step in enumerate(plan_data):
            self.steps.append(TaskStep(
                id=i,
                description=step["description"],
                action=step["action"],
                params=step["params"],
                dependencies=step.get("dependencies", [])
            ))

        return self.steps

    def get_executable_steps(self) -> list[TaskStep]:
        """获取当前可执行的步骤(所有依赖已完成)"""
        executable = []
        for step in self.steps:
            if step.status != TaskStatus.PENDING:
                continue
            deps_met = all(
                self.steps[dep].status == TaskStatus.COMPLETED
                for dep in step.dependencies
            )
            if deps_met:
                executable.append(step)
        return executable

    async def execute_plan(self, browser_agent) -> dict:
        """执行完整计划"""
        max_iterations = len(self.steps) * 2  # 防止无限循环
        iteration = 0

        while iteration < max_iterations:
            executable = self.get_executable_steps()
            if not executable:
                break

            for step in executable:
                step.status = TaskStatus.RUNNING
                print(f"执行步骤 {step.id}: {step.description}")

                try:
                    result = await self.execute_step(step, browser_agent)
                    step.result = result
                    step.status = TaskStatus.COMPLETED
                    print(f"  ✓ 完成")
                except Exception as e:
                    step.error = str(e)
                    step.retry_count += 1

                    if step.retry_count < step.max_retries:
                        step.status = TaskStatus.PENDING
                        print(f"  ⟳ 重试 ({step.retry_count}/{step.max_retries}): {e}")
                    else:
                        step.status = TaskStatus.FAILED
                        print(f"  ✗ 失败: {e}")

            iteration += 1

        # 汇总结果
        return {
            "completed": [s for s in self.steps if s.status == TaskStatus.COMPLETED],
            "failed": [s for s in self.steps if s.status == TaskStatus.FAILED],
            "results": [s.result for s in self.steps if s.result]
        }

    async def execute_step(self, step: TaskStep, browser_agent) -> Any:
        """执行单个步骤"""
        page = browser_agent.page
        action = step.action
        params = step.params

        if action == "navigate":
            await page.goto(params["url"])
            await page.wait_for_load_state("networkidle")

        elif action == "click":
            if "selector" in params:
                await page.click(params["selector"])
            elif "text" in params:
                await page.get_by_text(params["text"]).click()
            await page.wait_for_load_state("networkidle")

        elif action == "fill":
            await page.fill(params["selector"], params["value"])

        elif action == "extract":
            data = await page.evaluate(params["js_code"])
            return data

        elif action == "scroll":
            await page.evaluate(f"window.scrollBy(0, {params.get('amount', 500)})")

        elif action == "wait":
            if "selector" in params:
                await page.wait_for_selector(params["selector"], timeout=params.get("timeout", 10000))
            else:
                await asyncio.sleep(params.get("seconds", 2))

        elif action == "conditional":
            # 条件判断,让AI决定下一步
            condition_result = await page.evaluate(params["condition_js"])
            if condition_result:
                return params.get("true_result")
            else:
                return params.get("false_result")

        return None

错误恢复机制

健壮的错误恢复机制是生产级自动化系统的关键。

重试与恢复策略

import asyncio
import functools
import logging
from typing import Callable

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def with_retry(max_retries: int = 3,
               delay: float = 1.0,
               backoff: float = 2.0,
               exceptions: tuple = (Exception,)):
    """重试装饰器,支持指数退避"""
    def decorator(func: Callable):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            last_exception = None
            current_delay = delay

            for attempt in range(max_retries + 1):
                try:
                    return await func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt < max_retries:
                        logger.warning(
                            f"{func.__name__} 第{attempt + 1}次尝试失败: {e}, "
                            f"{current_delay:.1f}秒后重试..."
                        )
                        await asyncio.sleep(current_delay)
                        current_delay *= backoff
                    else:
                        logger.error(f"{func.__name__} 所有重试均失败: {e}")

            raise last_exception
        return wrapper
    return decorator


class ErrorRecoveryManager:
    """错误恢复管理器"""

    def __init__(self, llm):
        self.llm = llm
        self.error_history = []
        self.recovery_strategies = {
            "element_not_found": self._recover_element_not_found,
            "page_load_timeout": self._recover_page_load_timeout,
            "navigation_error": self._recover_navigation_error,
            "captcha_detected": self._recover_captcha,
            "rate_limited": self._recover_rate_limit,
        }

    async def handle_error(self, error: Exception, page, context: dict) -> bool:
        """处理错误并尝试恢复"""
        error_type = self._classify_error(error)
        self.error_history.append({
            "type": error_type,
            "message": str(error),
            "timestamp": datetime.now().isoformat(),
            "context": context
        })

        logger.error(f"遇到错误 [{error_type}]: {error}")

        # 尝试预定义的恢复策略
        if error_type in self.recovery_strategies:
            recovery_fn = self.recovery_strategies[error_type]
            try:
                recovered = await recovery_fn(page, error, context)
                if recovered:
                    logger.info(f"错误恢复成功: {error_type}")
                    return True
            except Exception as recovery_error:
                logger.error(f"恢复策略执行失败: {recovery_error}")

        # 使用AI分析错误并尝试恢复
        return await self._ai_recovery(page, error, context)

    def _classify_error(self, error: Exception) -> str:
        """错误分类"""
        error_str = str(error).lower()
        error_type = type(error).__name__

        if "timeout" in error_str or "TimeoutError" in error_type:
            return "page_load_timeout"
        if "element" in error_str and ("not found" in error_str or "not visible" in error_str):
            return "element_not_found"
        if "navigation" in error_str:
            return "navigation_error"
        if "captcha" in error_str or "recaptcha" in error_str:
            return "captcha_detected"
        if "429" in error_str or "rate" in error_str:
            return "rate_limited"
        return "unknown"

    async def _recover_element_not_found(self, page, error, context):
        """元素未找到的恢复策略"""
        selector = context.get("selector", "")
        description = context.get("description", "")

        # 策略1: 等待更长时间
        try:
            await page.wait_for_selector(selector, timeout=15000)
            return True
        except:
            pass

        # 策略2: 滚动页面后重试
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 2)")
        await asyncio.sleep(1)
        try:
            await page.wait_for_selector(selector, timeout=5000)
            return True
        except:
            pass

        # 策略3: 使用AI寻找替代元素
        if description:
            page_content = await extract_page_content(page)
            match = await ai_element_understanding(
                self.llm, page_content, f"找到 {description}"
            )
            if match.get("confidence", 0) > 0.5:
                element = match["target_element"]
                await page.click(
                    element["rect"]["x"] + element["rect"]["width"] / 2,
                    element["rect"]["y"] + element["rect"]["height"] / 2
                )
                return True

        return False

    async def _recover_page_load_timeout(self, page, error, context):
        """页面加载超时的恢复策略"""
        # 尝试停止加载并继续
        try:
            await page.evaluate("window.stop()")
            await asyncio.sleep(2)
            # 检查页面是否可用
            title = await page.title()
            if title:
                return True
        except:
            pass

        # 重新导航
        url = context.get("url")
        if url:
            try:
                await page.goto(url, timeout=30000, wait_until="domcontentloaded")
                return True
            except:
                pass

        return False

    async def _recover_navigation_error(self, page, error, context):
        """导航错误的恢复策略"""
        url = context.get("url")
        if url:
            # 尝试不同的等待策略
            try:
                await page.goto(url, wait_until="commit", timeout=30000)
                await asyncio.sleep(3)
                return True
            except:
                pass
        return False

    async def _recover_captcha(self, page, error, context):
        """验证码恢复策略"""
        logger.warning("检测到验证码,暂停30秒后重试...")
        await asyncio.sleep(30)
        # 可以在这里集成验证码识别服务
        return False

    async def _recover_rate_limit(self, page, error, context):
        """频率限制恢复策略"""
        logger.warning("触发频率限制,等待60秒...")
        await asyncio.sleep(60)
        return True

    async def _ai_recovery(self, page, error, context) -> bool:
        """使用AI进行智能错误恢复"""
        try:
            screenshot = await page.screenshot(type="png")
            screenshot_b64 = base64.b64encode(screenshot).decode()

            page_url = page.url
            page_title = await page.title()

            prompt = f"""浏览器自动化遇到错误,请分析并提供恢复建议。

错误信息: {error}
错误类型: {type(error).__name__}
当前URL: {page_url}
页面标题: {page_title}
任务上下文: {json.dumps(context, ensure_ascii=False)}

请分析截图和错误信息,返回JSON格式的恢复方案:
{{
    "action": "恢复动作 (retry/navigate/scroll/click/wait/abort)",
    "params": {{}},
    "reasoning": "分析原因"
}}"""

            response = await self.llm.ainvoke([
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{screenshot_b64}"}}
                    ]
                }
            ])

            recovery = json.loads(response.content)
            action = recovery["action"]
            params = recovery.get("params", {})

            logger.info(f"AI恢复方案: {action} - {recovery.get('reasoning', '')}")

            if action == "retry":
                return True
            elif action == "navigate" and "url" in params:
                await page.goto(params["url"])
                return True
            elif action == "scroll":
                amount = params.get("amount", 500)
                await page.evaluate(f"window.scrollBy(0, {amount})")
                return True
            elif action == "wait":
                await asyncio.sleep(params.get("seconds", 5))
                return True

        except Exception as ai_error:
            logger.error(f"AI恢复分析失败: {ai_error}")

        return False

实战:自动化数据采集Agent

综合以上技术,我们构建一个完整的自动化数据采集Agent。

import asyncio
import json
import logging
from datetime import datetime
from pathlib import Path

from langchain_openai import ChatOpenAI
from playwright.async_api import async_playwright
from browser_use import Agent, Browser, BrowserConfig

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class DataCollectionAgent:
    """完整的自动化数据采集Agent"""

    def __init__(self, config: dict):
        self.config = config
        self.llm = ChatOpenAI(
            model=config.get("model", "gpt-4o"),
            temperature=0
        )
        self.error_manager = ErrorRecoveryManager(self.llm)
        self.collector = AIDataCollector(self.llm, None)  # browser稍后设置
        self.results = []

    async def initialize(self):
        """初始化浏览器和Agent"""
        self.playwright = await async_playwright().start()
        self.browser = await self.playwright.chromium.launch(
            headless=self.config.get("headless", True),
            args=[
                "--disable-blink-features=AutomationControlled",
                "--no-sandbox"
            ]
        )
        self.context = await self.browser.new_context(
            viewport={"width": 1280, "height": 720},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        )

        # 应用反检测
        await anti_detection_setup(self.context)

        self.page = await self.context.new_page()
        self.collector.browser = type('Browser', (), {'get_current_page': lambda s: self.page})()

    async def run_task(self, task: str, data_schema: dict = None,
                       max_pages: int = 5, output_file: str = None):
        """执行数据采集任务"""
        logger.info(f"开始执行任务: {task}")

        # 创建执行计划
        planner = TaskPlanner(self.llm)

        # 先导航到目标页面以获取页面信息
        target_url = self.config.get("target_url")
        if target_url:
            await self.page.goto(target_url, wait_until="domcontentloaded")
            await asyncio.sleep(2)

        page_info = await extract_page_content(self.page)
        plan = await planner.create_plan(task, {
            "url": self.page.url,
            "title": await self.page.title(),
            "elements_count": len(page_info.get("elements", []))
        })

        logger.info(f"执行计划包含 {len(plan)} 个步骤")
        for step in plan:
            logger.info(f"  Step {step.id}: {step.description}")

        # 执行计划
        execution_result = await planner.execute_plan(self)

        # 如果有数据schema,执行数据采集
        if data_schema:
            logger.info("开始数据采集...")
            data = await self.collector.collect_with_pagination(
                self.page, data_schema, max_pages=max_pages
            )
            self.results.extend(data)

        # 保存结果
        if output_file:
            await self.save_results(output_file)

        return {
            "plan_result": execution_result,
            "collected_data": self.results,
            "total_items": len(self.results)
        }

    async def save_results(self, output_file: str):
        """保存采集结果"""
        output_path = Path(output_file)
        output_path.parent.mkdir(parents=True, exist_ok=True)

        if output_file.endswith(".json"):
            with open(output_file, "w", encoding="utf-8") as f:
                json.dump({
                    "timestamp": datetime.now().isoformat(),
                    "config": self.config,
                    "total_items": len(self.results),
                    "data": self.results
                }, f, ensure_ascii=False, indent=2)
        elif output_file.endswith(".csv"):
            await self.collector.save_data(output_file, format="csv")

        logger.info(f"结果已保存到 {output_file}")

    async def cleanup(self):
        """清理资源"""
        await self.page.close()
        await self.context.close()
        await self.browser.close()
        await self.playwright.stop()


# 主函数
async def main():
    """主入口"""
    config = {
        "model": "gpt-4o",
        "headless": True,
        "target_url": "https://news.ycombinator.com"
    }

    agent = DataCollectionAgent(config)
    await agent.initialize()

    try:
        result = await agent.run_task(
            task="采集Hacker News首页的所有新闻标题、链接和分数",
            data_schema={
                "title": "新闻标题",
                "url": "新闻链接",
                "score": "分数",
                "author": "作者"
            },
            max_pages=3,
            output_file="output/hn_data.json"
        )

        print(f"\n采集完成!共采集 {result['total_items']} 条数据")
        for item in result["collected_data"][:5]:
            print(f"  - {item.get('title', 'N/A')}")

    finally:
        await agent.cleanup()


if __name__ == "__main__":
    asyncio.run(main())

最佳实践与注意事项

1. 性能优化

# 使用headless模式提高速度
browser = await p.chromium.launch(headless=True)

# 禁用不必要的资源加载
async def block_resources(route):
    blocked_types = {"image", "stylesheet", "font", "media"}
    if route.request.resource_type in blocked_types:
        await route.abort()
    else:
        await route.continue_()

await page.route("**/*", block_resources)

# 设置合理的超时时间
page.set_default_timeout(15000)
page.set_default_navigation_timeout(30000)

2. 成本控制

  • 选择合适的模型:简单任务使用小模型,复杂推理使用大模型
  • 减少截图频率:能用DOM交互的场景不必截图
  • 缓存AI决策:相同页面结构的决策可以缓存复用
  • 限制LLM调用次数:设置max_actions_per_stepmax_steps

3. 遵守规范

# 遵守robots.txt
import urllib.robotparser

def can_crawl(url: str, user_agent: str = "*") -> bool:
    """检查是否允许爬取"""
    from urllib.parse import urlparse
    parsed = urlparse(url)
    robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"

    rp = urllib.robotparser.RobotFileParser()
    rp.set_url(robots_url)
    try:
        rp.read()
        return rp.can_fetch(user_agent, url)
    except:
        return True  # 无法读取时默认允许

# 礼貌性延迟
import random

async def polite_delay(min_seconds=1, max_seconds=3):
    """随机延迟,避免对服务器造成压力"""
    delay = random.uniform(min_seconds, max_seconds)
    await asyncio.sleep(delay)

4. 日志与监控

import logging
from datetime import datetime

class AgentLogger:
    """Agent操作日志记录器"""

    def __init__(self, log_file: str = "agent.log"):
        self.logger = logging.getLogger("AgentLogger")
        self.logger.setLevel(logging.INFO)

        handler = logging.FileHandler(log_file, encoding="utf-8")
        formatter = logging.Formatter(
            '%(asctime)s - %(levelname)s - %(message)s'
        )
        handler.setFormatter(formatter)
        self.logger.addHandler(handler)

        self.action_count = 0
        self.start_time = datetime.now()

    def log_action(self, action: str, result: str, details: dict = None):
        self.action_count += 1
        self.logger.info(
            f"[Action #{self.action_count}] {action} -> {result} | "
            f"Details: {json.dumps(details or {}, ensure_ascii=False)}"
        )

    def log_error(self, error: str, context: dict = None):
        self.logger.error(
            f"[Error] {error} | Context: {json.dumps(context or {}, ensure_ascii=False)}"
        )

    def get_summary(self) -> dict:
        elapsed = (datetime.now() - self.start_time).total_seconds()
        return {
            "total_actions": self.action_count,
            "elapsed_seconds": elapsed,
            "actions_per_minute": self.action_count / (elapsed / 60) if elapsed > 0 else 0
        }

总结

AI浏览器自动化与Web Agent代表了自动化技术的重要发展方向。通过将大语言模型的推理能力与浏览器控制相结合,我们能够构建更加智能、灵活、鲁棒的自动化系统。

本教程涵盖的核心要点:

  1. Playwright基础:现代浏览器自动化的基石,提供强大的页面控制能力
  2. Browser Use框架:将AI Agent与浏览器自动化的优雅结合
  3. Computer Use:纯视觉驱动的计算机操作方案
  4. 元素识别与交互:DOM解析与AI视觉理解的结合
  5. 表单自动填写:智能表单识别与数据填充
  6. 数据采集:AI驱动的智能数据采集与翻页策略
  7. 视觉定位:基于截图的元素定位与交互
  8. 任务规划:复杂任务的分解与依赖管理
  9. 错误恢复:多层次的错误处理与智能恢复

随着多模态AI模型的持续进步,浏览器自动化将变得更加智能和通用。建议开发者从简单的任务开始,逐步构建更复杂的自动化工作流,在实践中掌握AI浏览器自动化的核心技术。


本教程持续更新中,欢迎反馈和建议。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录