449 lines
20 KiB
Markdown
449 lines
20 KiB
Markdown
# 浏览器自动化技术架构与实现
|
||
|
||
|
||
|
||
## 概述
|
||
|
||
浏览器自动化(Browser Automation)是指通过程序代码控制真实浏览器执行用户操作的技术栈。它在 Web 测试、数据抓取、UI 验证、表单填写、截图对比等场景中被广泛应用。
|
||
|
||
---
|
||
|
||
## 核心技术架构
|
||
|
||
### 1. 控制协议层
|
||
|
||
浏览器自动化的本质是通过特定协议与浏览器进程通信,向浏览器发送指令并接收执行结果。目前业界主流的通信协议有三种:
|
||
|
||
#### 1.1 Chrome DevTools Protocol (CDP)
|
||
|
||
**CDP** 是 Chromium/Chrome 浏览器内置的远程调试协议,基于 WebSocket 通信。
|
||
|
||
- **原理**:Chrome 启动时开放一个调试端口(如 `--remote-debugging-port=9222`),外部程序通过 WebSocket 连接到该端口,发送 JSON 格式的命令来控制浏览器
|
||
- **能力**:几乎可以控制浏览器的所有行为,包括 DOM 操作、网络拦截、性能监控、截图、执行 JavaScript、模拟鼠标键盘事件等
|
||
- **优势**:原生支持、速度快、功能全、无额外驱动依赖
|
||
- **代表工具**:Puppeteer、Playwright(Chromium 模式)、Chrome Remote Interface
|
||
|
||
```
|
||
┌─────────────┐ WebSocket ┌──────────────────┐
|
||
│ 控制程序 │ ◄──────────────► │ Chrome 浏览器 │
|
||
│ (Node.js) │ JSON-RPC 协议 │ (调试端口 9222) │
|
||
└─────────────┘ └──────────────────┘
|
||
```
|
||
|
||
#### 1.2 WebDriver Protocol (W3C / JsonWire)
|
||
|
||
**WebDriver** 是 W3C 标准化的浏览器控制协议,基于 HTTP RESTful API。
|
||
|
||
- **原理**:浏览器厂商或第三方提供 WebDriver 驱动程序(如 ChromeDriver、GeckoDriver),控制程序通过 HTTP 请求向驱动发送指令,驱动再转译为浏览器内部操作
|
||
- **能力**:标准化、跨浏览器支持(Chrome、Firefox、Safari、Edge)
|
||
- **劣势**:相比 CDP 速度较慢,部分高级功能(如网络拦截)支持有限
|
||
- **代表工具**:Selenium、WebdriverIO
|
||
|
||
```
|
||
┌─────────────┐ HTTP ┌─────────────┐ IPC ┌──────────────────┐
|
||
│ 控制程序 │ ◄──────────► │ ChromeDriver │ ◄────────► │ Chrome 浏览器 │
|
||
└─────────────┘ REST API └─────────────┘ └──────────────────┘
|
||
```
|
||
|
||
#### 1.3 BiDi Protocol (WebDriver-BiDi)
|
||
|
||
**BiDi**(Bidirectional)是 W3C 正在推进的新一代双向通信协议,目标是结合 WebDriver 的标准化和 CDP 的高性能。
|
||
|
||
- **原理**:基于 WebSocket 的双向通信,既保留了 WebDriver 的标准化语义,又支持事件订阅和实时推送
|
||
- **状态**:Firefox 已原生支持,Chrome 正在逐步推进
|
||
- **代表工具**:Playwright(部分实现)、Selenium 4+
|
||
|
||
---
|
||
|
||
## 2. 典型工具链实现
|
||
|
||
### 2.1 Playwright 架构
|
||
|
||
Playwright 是微软开源的浏览器自动化框架,支持 Chromium、Firefox、WebKit。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Playwright API (Python/JS/...) │
|
||
│ - page.click() │
|
||
│ - page.fill() │
|
||
│ - page.screenshot() │
|
||
│ - page.evaluate() │
|
||
└──────────────────┬────────────────────────────┘
|
||
│
|
||
┌──────────────────▼────────────────────────────┐
|
||
│ Playwright Driver (语言绑定层) │
|
||
│ - 协议序列化/反序列化 │
|
||
│ - 浏览器进程管理 │
|
||
└──────────────────┬────────────────────────────┘
|
||
│ WebSocket (CDP) / Pipe
|
||
┌──────────────────▼────────────────────────────┐
|
||
│ Browser 进程 │
|
||
│ - Chromium: CDP 协议 │
|
||
│ - Firefox: Juggler / WebDriver BiDi │
|
||
│ - WebKit: WebKit Inspector Protocol │
|
||
└───────────────────────────────────────────────┘
|
||
```
|
||
|
||
**核心特点**:
|
||
- **自动等待**:操作前自动等待元素可见/可交互,避免 flaky 测试
|
||
- **浏览器上下文隔离**:每个测试用例运行在独立的浏览器上下文中(类似隐身窗口),彼此隔离 Cookie/Storage
|
||
- **网络拦截**:可拦截、修改、模拟 HTTP 请求响应
|
||
- **多浏览器支持**:一套 API 控制三种内核
|
||
|
||
### 2.2 Puppeteer 架构
|
||
|
||
Puppeteer 是 Google Chrome 团队维护的 Node.js 库,专注于 Chromium 自动化。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Puppeteer API (Node.js) │
|
||
│ - puppeteer.launch() │
|
||
│ - page.goto() │
|
||
│ - page.pdf() │
|
||
└──────────────────┬────────────────────────────┘
|
||
│
|
||
┌──────────────────▼────────────────────────────┐
|
||
│ Chrome DevTools Protocol (CDP) │
|
||
│ - Target domain (创建/管理页面) │
|
||
│ - DOM domain (元素查询/操作) │
|
||
│ - Input domain (鼠标/键盘事件) │
|
||
│ - Network domain (请求拦截) │
|
||
│ - Emulation domain (设备模拟) │
|
||
└──────────────────┬────────────────────────────┘
|
||
│ WebSocket
|
||
┌──────────────────▼────────────────────────────┐
|
||
│ Chrome/Chromium Browser │
|
||
└───────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.3 Selenium 架构
|
||
|
||
Selenium 是最老牌的浏览器自动化框架,跨语言和跨浏览器支持最广。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Selenium WebDriver Client (多语言) │
|
||
│ - Java / Python / C# / JavaScript / Ruby │
|
||
└──────────────────┬────────────────────────────┘
|
||
│ HTTP (W3C WebDriver Protocol)
|
||
┌──────────────────▼────────────────────────────┐
|
||
│ WebDriver Server (Browser-specific) │
|
||
│ - ChromeDriver (Chrome) │
|
||
│ - GeckoDriver (Firefox) │
|
||
│ - SafariDriver (Safari) │
|
||
│ - EdgeDriver (Edge) │
|
||
└──────────────────┬────────────────────────────┘
|
||
│ Browser-specific protocol
|
||
┌──────────────────▼────────────────────────────┐
|
||
│ Target Browser │
|
||
└───────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 关键实现方法详解
|
||
|
||
### 3.1 页面导航与 DOM 操作
|
||
|
||
```javascript
|
||
// 启动浏览器并打开页面
|
||
const browser = await puppeteer.launch({ headless: false });
|
||
const page = await browser.newPage();
|
||
await page.goto('https://example.com');
|
||
|
||
// 等待元素出现并点击
|
||
await page.waitForSelector('button.submit');
|
||
await page.click('button.submit');
|
||
|
||
// 填写表单
|
||
await page.type('input[name="username"]', 'admin');
|
||
await page.type('input[name="password"]', 'secret');
|
||
|
||
// 执行自定义 JavaScript
|
||
const title = await page.evaluate(() => document.title);
|
||
```
|
||
|
||
**底层实现**:
|
||
1. `goto` 调用 CDP 的 `Page.navigate` 命令
|
||
2. `waitForSelector` 调用 CDP 的 `DOM.querySelector`,轮询直到元素存在
|
||
3. `click` 先通过 CDP `DOM.getBoxModel` 获取元素坐标,再调用 `Input.dispatchMouseEvent` 模拟鼠标事件
|
||
|
||
### 3.2 截图实现
|
||
|
||
```javascript
|
||
// 整页截图
|
||
await page.screenshot({ path: 'page.png', fullPage: true });
|
||
|
||
// 元素截图
|
||
const element = await page.$('.chart');
|
||
await element.screenshot({ path: 'chart.png' });
|
||
```
|
||
|
||
**底层实现**:
|
||
1. CDP 的 `Page.captureScreenshot` 命令
|
||
2. 浏览器渲染引擎将当前视口内容编码为 base64 PNG
|
||
3. 如需整页截图,先通过 `Page.getLayoutMetrics` 获取页面总高度,调整视口后截取
|
||
4. 元素截图先获取元素边界框,再裁剪整页截图
|
||
|
||
### 3.3 网络拦截
|
||
|
||
```javascript
|
||
await page.setRequestInterception(true);
|
||
page.on('request', (req) => {
|
||
if (req.url().endsWith('.png')) {
|
||
req.abort(); // 拦截图片加载
|
||
} else {
|
||
req.continue();
|
||
}
|
||
});
|
||
```
|
||
|
||
**底层实现**:
|
||
1. CDP 的 `Network.setRequestInterception` 启用拦截
|
||
2. 浏览器在发起请求前暂停,通过 CDP 事件通知控制程序
|
||
3. 控制程序发送 `Network.continueInterceptedRequest` 或 `Network.failRequest`
|
||
|
||
---
|
||
|
||
## 4. 元素定位机制:截图 vs DOM 分析(重要)
|
||
|
||
### 4.1 核心原理:直接操作 DOM,而非分析图片像素
|
||
|
||
**常见误解**:很多人以为自动化工具是先截图,再用图像识别找到按钮的屏幕像素坐标,然后让鼠标移过去点击。
|
||
|
||
**实际机制**:完全不是这样。自动化工具直接通过 CDP/WebDriver 与浏览器内核通信,**查询 DOM 树,获取元素的逻辑标识和渲染位置**,再触发浏览器原生事件。这与图像识别自动化(如 PyAutoGUI)是两种截然不同的技术路线。
|
||
|
||
```
|
||
错误理解(图像识别路线):
|
||
截图 → OCR/图像识别 → 找到像素坐标(320, 45) → 移动鼠标 → 点击
|
||
|
||
实际机制(DOM 操作路线):
|
||
CDP 查询 DOM 树 → 找到元素节点 → 获取 Layout 坐标 → 触发浏览器输入事件
|
||
```
|
||
|
||
### 4.2 元素定位的完整底层流程
|
||
|
||
以 `page.click('button[type=submit]')` 为例,CDP 层面实际发生了什么:
|
||
|
||
**第 1 步:定位 DOM 节点**
|
||
|
||
发送 CDP `Runtime.evaluate` 命令,在浏览器 JS 环境执行 `document.querySelector`,返回节点的 `objectId` 和 `nodeId`(例如 nodeId=42)。
|
||
|
||
**第 2 步:获取渲染坐标(Box Model)**
|
||
|
||
发送 CDP `DOM.getBoxModel` 命令,传入 nodeId=42,返回元素四个顶角的屏幕坐标:
|
||
```
|
||
content: [120, 40, 200, 40,
|
||
200, 60, 120, 60] ← 左上、右上、右下、左下
|
||
计算中心点: x = (120+200)/2 = 160, y = (40+60)/2 = 50
|
||
```
|
||
|
||
**第 3 步:触发鼠标事件**
|
||
|
||
连续发送三次 CDP `Input.dispatchMouseEvent` 命令:
|
||
```
|
||
{ type: mouseMoved, x: 160, y: 50 } ← 鼠标移入
|
||
{ type: mousePressed, x: 160, y: 50, button: left }
|
||
{ type: mouseReleased, x: 160, y: 50, button: left }
|
||
```
|
||
|
||
整个过程**完全不依赖截图或图像识别**,是纯 DOM 查询 + 浏览器事件驱动。
|
||
|
||
---
|
||
|
||
### 4.3 agent-browser 的定位方式:Accessibility Tree(无障碍树)
|
||
|
||
本项目使用的 `agent-browser` 工具在 DOM 查询基础上更进一步,默认使用 **Accessibility Tree(无障碍语义树)** 来定位元素,而非原始 CSS 选择器。
|
||
|
||
```
|
||
传统 CSS 选择器(脆弱,依赖 DOM 层级):
|
||
div.nav-bar > ul > li:nth-child(3) > a.btn-primary
|
||
|
||
Accessibility Tree(语义稳定):
|
||
role=button, name="立即咨询"
|
||
role=textbox, label="用户名"
|
||
```
|
||
|
||
**底层工作原理:**
|
||
|
||
浏览器为每个 DOM 元素生成对应的 AX 节点,包含 `role`(语义角色)、`name`(可读名称)、`state`、`boundingBox` 等属性。CDP 提供 `Accessibility.getFullAXTree` 命令导出完整的 AX 树。
|
||
|
||
`agent-browser snapshot -i` 就是调用此命令,解析后为每个可交互节点生成 `@e1`、`@e2` 等 ref 引用:
|
||
|
||
```bash
|
||
$ agent-browser snapshot -i
|
||
# 输出(AX 树的可交互节点):
|
||
textbox "用户名" [ref=e1] <- role=textbox
|
||
textbox "密码" [ref=e2] <- role=textbox
|
||
button "登录" [ref=e3] <- role=button
|
||
button "立即咨询" [ref=e8] <- role=button
|
||
link "首页" [ref=e10] <- role=link
|
||
|
||
# 直接用 ref 操作,无需写任何选择器:
|
||
$ agent-browser fill @e1 "admin"
|
||
$ agent-browser fill @e2 "abcd1234"
|
||
$ agent-browser click @e3
|
||
```
|
||
|
||
**AX Tree 的优势:**
|
||
- React/Vue 组件重构、CSS 类名变化后依然稳定,因为语义角色不变
|
||
- 对异步渲染的 SPA 应用更鲁棒(等待节点出现而非等待选择器)
|
||
- 天然对齐无障碍标准(和屏幕阅读器使用同一接口)
|
||
|
||
---
|
||
|
||
### 4.4 截图的真正用途
|
||
|
||
**截图不是用来定位元素的,而是用于视觉验证。**
|
||
|
||
| 截图用途 | 具体场景 |
|
||
|----------|----------|
|
||
| **验证渲染结果** | 图片是否正常显示(无破图)、CSS 样式是否正确 |
|
||
| **验证交互状态** | hover 后面板是否展开、点击后弹窗是否出现 |
|
||
| **调试记录** | 留存测试过程的视觉证据,便于人工排查问题 |
|
||
| **视觉回归测试** | 与基线截图做像素级对比,检测 UI 意外变化 |
|
||
|
||
操控阶段使用 DOM/AX 树,验证阶段才使用截图:
|
||
|
||
```
|
||
操控阶段(DOM/AX 操作,不需要截图)
|
||
snapshot -> 获取元素 ref 列表
|
||
fill @e1 -> 直接写入 DOM value,无需截图
|
||
click @e3 -> 触发浏览器输入事件,无需截图
|
||
wait --url -> 监听 Page.frameNavigated 事件
|
||
|
||
验证阶段(截图,确认视觉结果正确)
|
||
screenshot -> 保存截图,人工或程序检查渲染是否符合预期 ✓
|
||
```
|
||
|
||
---
|
||
|
||
### 4.5 与图像识别自动化的对比
|
||
|
||
| 维度 | DOM/AX 方案(CDP / agent-browser)| 图像识别方案(PyAutoGUI / SikuliX)|
|
||
|------|-----------------------------------|------------------------------------|
|
||
| **定位方式** | 查询 DOM/AX 语义树 | 截图 → 模板像素匹配 |
|
||
| **定位速度** | 毫秒级 | 秒级(截图+识别开销高)|
|
||
| **稳定性** | 高(语义不变则稳定)| 低(分辨率/主题/字体变化即失效)|
|
||
| **无头模式** | 完全支持(无需屏幕)| 需要真实屏幕渲染 |
|
||
| **适用范围** | Web 应用(有 DOM 的页面)| 桌面软件、游戏等无 API 的场景 |
|
||
| **可读性** | 高(`click "登录"` 语义清晰)| 低(依赖截图文件匹配)|
|
||
|
||
---
|
||
|
||
## 5. 在 Web 测试中的应用模式
|
||
|
||
### 5.1 端到端测试 (E2E Testing)
|
||
|
||
模拟真实用户从打开页面到完成业务流程的完整路径:
|
||
|
||
```javascript
|
||
test('用户登录流程', async ({ page }) => {
|
||
await page.goto('/login');
|
||
await page.fill('[data-testid="username"]', 'user1');
|
||
await page.fill('[data-testid="password"]', 'pass123');
|
||
await page.click('[data-testid="login-btn"]');
|
||
await expect(page).toHaveURL('/dashboard');
|
||
await expect(page.locator('.welcome-msg')).toContainText('欢迎回来');
|
||
});
|
||
```
|
||
|
||
### 5.2 视觉回归测试 (Visual Regression)
|
||
|
||
通过截图对比检测 UI 意外变化:
|
||
|
||
```javascript
|
||
// 基线截图
|
||
await page.screenshot({ path: 'baseline/homepage.png' });
|
||
|
||
// 后续对比
|
||
const diff = await compareImages('baseline/homepage.png', 'current/homepage.png');
|
||
expect(diff.pixelDifference).toBeLessThan(0.01);
|
||
```
|
||
|
||
### 5.3 性能监控
|
||
|
||
利用 CDP 的 Performance 和 Tracing domain 采集页面加载指标:
|
||
|
||
```javascript
|
||
await page.tracing.start({ path: 'trace.json' });
|
||
await page.goto('https://example.com');
|
||
await page.tracing.stop();
|
||
// 分析 trace.json 中的 firstContentfulPaint, largestContentfulPaint 等指标
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 无头模式 vs 有头模式
|
||
|
||
| 模式 | 说明 | 适用场景 |
|
||
|------|------|----------|
|
||
| **Headless(无头)** | 浏览器在后台运行,不显示 GUI | CI/CD 流水线、服务器端爬虫、大规模自动化 |
|
||
| **Headful(有头)** | 浏览器正常显示窗口 | 本地调试、需要人工观察的测试、反爬虫对抗 |
|
||
|
||
现代无头浏览器(Chromium Headless、Firefox Headless)已能完美渲染 CSS/Canvas/WebGL,与有头模式行为一致。
|
||
|
||
---
|
||
|
||
## 7. 反检测与指纹对抗
|
||
|
||
当自动化工具被用于数据抓取时,目标网站可能通过以下方式检测:
|
||
|
||
- **navigator.webdriver**:Chrome 在自动化模式下会设置该属性为 `true`
|
||
- **插件列表**:真实浏览器有 PDF 插件、Chrome 插件等,无头环境通常缺失
|
||
- **WebGL/Canvas 指纹**:无头环境的 GPU 指纹与真实设备不同
|
||
- **鼠标移动轨迹**:真实用户的鼠标移动有随机性和曲线,自动化工具通常是直线瞬移
|
||
|
||
**对抗手段**:
|
||
- 使用 `puppeteer-extra-plugin-stealth` 等插件修改指纹
|
||
- 模拟真实鼠标移动轨迹(贝塞尔曲线)
|
||
- 使用真实用户代理和视口尺寸
|
||
|
||
---
|
||
|
||
## 8. 选型建议
|
||
|
||
| 需求 | 推荐工具 |
|
||
|------|----------|
|
||
| 现代 Web 应用测试,追求稳定性 | **Playwright** |
|
||
| 纯 Chromium 生态,需要 PDF/打印 | **Puppeteer** |
|
||
| 跨浏览器兼容性测试 | **Selenium** / **Playwright** |
|
||
| 已有 Selenium 生态,渐进升级 | **Selenium 4 + BiDi** |
|
||
| 反爬虫数据抓取 | **Puppeteer + Stealth 插件** |
|
||
|
||
---
|
||
|
||
## 9. 总结
|
||
|
||
浏览器自动化的技术本质可以概括为:
|
||
|
||
> **通过标准化协议(CDP / WebDriver / BiDi)与浏览器进程通信,将高级 API(click、fill、screenshot)翻译为底层的浏览器内部指令,从而程序化为浏览器赋予"机器手"和"机器眼"。**
|
||
|
||
无论使用哪种具体工具,其架构都遵循相似的分层设计:**应用 API 层 → 协议适配层 → 浏览器内核层**。理解这一通用架构,有助于在不同工具和场景之间灵活迁移和深度定制。
|
||
|
||
|
||
## 对话记录
|
||
|
||
### 你要把你的这个测试方法记录下来,记忆下来。后续我可以让你经常通过这个方法进行测试
|
||
已记录到工作区记忆中,关键词:测试、E2E、Playwright、浏览器自动化、agent-browser。后续你想做测试时,直接说类似这样的话即可:
|
||
"帮我测试一下访客端首页的导航菜单下拉效果"
|
||
"用浏览器测试后台登录和菜单管理功能"
|
||
"截图看一下移动端菜单的显示效果"
|
||
|
||
|
||
刚才记录的是 **workspace 级别**(仅当前项目有效)。但 `agent-browser` 技能本身是全局通用的,任何有 Web 界面的项目都能测。
|
||
|
||
我把它升级为 **global 记忆**,这样你其他项目也能直接调用:
|
||
已升级为 **global 记忆**,现在跨项目通用。
|
||
|
||
**agent-browser** 不限技术栈,只要项目有 Web 界面(React/Vue/纯 HTML/后台管理系统都可以),我都能打开真实浏览器进行测试。后续任何项目你都可以直接说:
|
||
|
||
> "测试一下这个页面"
|
||
> "截图看看效果"
|
||
> "验证一下功能是否正常"
|
||
|
||
我会自动调用浏览器自动化执行。
|
||
|
||
我会自动调用 agent-browser 打开真实 Chromium 浏览器,执行操作并截图返回验证结果!
|
||
|