
Chrome Devtools Skill
- 5 installs
- 9 repo stars
- Updated August 4, 2026
- steelan9199/wechat-publisher-skill
Helps with ai & agent building tasks.
About
chrome-devtools-skill is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- chrome-devtools-skill
- AI & Agent Building
- AI-coding skill
Chrome Devtools Skill by the numbers
- 5 all-time installs (skills.sh)
- Ranked #13,063 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/steelan9199/wechat-publisher-skill --skill chrome-devtools-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 9 |
| Last updated | August 4, 2026 |
| Repository | steelan9199/wechat-publisher-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
Chrome DevTools Skill
使用 Chrome DevTools Protocol 进行网页数据获取和分析。
工作流程(AI 自动执行)
当用户使用此 skill 时,AI 应该自动完成以下所有步骤,而不是让用户手动执行:
1. 自动启动 Chrome 远程调试
AI 自动检测操作系统并启动 Chrome:
Windows:
start chrome --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-devtools-profile" [URL]重要提示: 使用固定的--user-data-dir(如chrome-devtools-profile),不要每次使用不同的文件夹。这样 Chrome 会记住登录状态、Cookie 和缓存,下次启动更快。
macOS:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir="$TMPDIR/chrome-profile-stable" [URL]Linux:
google-chrome --remote-debugging-port=9222 --user-data-dir="/tmp/chrome-profile-stable" [URL]2. 自动启动 MCP 服务器
AI 自动启动 MCP 服务器(在后台运行):
npx -y chrome-devtools-mcp@latest --browser-url=http://127.0.0.1:92223. 执行用户请求的操作
使用以下 MCP 工具完成用户任务:
mcp__chrome-devtools__navigate_page- 导航到页面mcp__chrome-devtools__wait_for- 等待页面加载mcp__chrome-devtools__take_snapshot- 获取页面快照mcp__chrome-devtools__evaluate_script- 执行 JavaScript 提取数据mcp__chrome-devtools__screenshot- 截取屏幕截图mcp__chrome-devtools__list_network_requests- 列出网络请求
标准操作流程(AI 使用)
当用户要求打开/分析/抓取某个网页时,AI 按以下顺序执行:
1. 启动 Chrome(调试模式)
2. 启动 MCP 服务器
3. 导航到目标 URL
4. 等待页面加载
5. 执行用户请求的操作(截图/提取数据/监控网络等)
6. 返回结果给用户数据提取技巧
提取列表数据:
() => {
const items = document.querySelectorAll(".item");
return Array.from(items).map((item) => ({
title: item.querySelector(".title")?.innerText,
link: item.querySelector("a")?.href,
}));
};提取表格数据:
() => {
const rows = document.querySelectorAll("table tr");
return Array.from(rows).map((row) =>
Array.from(row.querySelectorAll("td, th")).map((cell) => cell.innerText)
);
};常见用例
- 打开网页并截图: 启动 Chrome → 导航 → 截图 → 返回结果
- 商品信息抓取: 导航 → 滚动加载 → 提取列表
- 表单自动化: 导航 → fill_form → click → wait_for
- API 监控: 导航 → list_network_requests → get_network_request
- 性能分析: performance_start_trace → 操作 → performance_stop_trace
最佳实践
1. 自动处理所有前置步骤 - 不要要求用户手动启动 Chrome 或 MCP 服务器 2. 使用固定的用户数据目录 - 保持 --user-data-dir 一致,避免每次都重新加载 3. 先用 take_snapshot 了解页面结构 4. 复杂提取用 evaluate_script 执行 JavaScript 5. 确保返回 JSON 可序列化的数据 6. 处理动态内容时使用 wait_for
技术原理
Chrome DevTools Protocol (CDP) 通信方式
| 方式 | 协议 | 用途 |
|---|---|---|
| HTTP | http://127.0.0.1:9222/json/list | 获取页面列表、基本信息 |
| WebSocket | ws://127.0.0.1:9222/devtools/page/{pageId} | 实时控制页面、执行脚本 |
WebSocket 通信流程
import asyncio
import websockets
import json
async def cdp_example():
# 1. 连接到 Chrome DevTools WebSocket
ws_url = "ws://127.0.0.1:9222/devtools/page/{pageId}"
async with websockets.connect(ws_url) as ws:
# 2. 启用 Runtime 域
await ws.send(json.dumps({
"id": 1,
"method": "Runtime.enable"
}))
# 3. 执行 JavaScript
await ws.send(json.dumps({
"id": 2,
"method": "Runtime.evaluate",
"params": {
"expression": "document.title",
"returnByValue": True
}
}))
# 4. 循环接收响应,匹配 id
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == 2: # 匹配请求 id
return data['result']['result']['value']关键注意事项
1. 响应匹配: WebSocket 会收到多种消息(consoleAPICalled、executionContextCreated 等),必须通过 id 匹配请求和响应 2. 等待页面加载: 执行操作后使用 await asyncio.sleep(2) 或等待特定元素 3. 编码问题: Windows 终端需要 sys.stdout.reconfigure(encoding='utf-8') 来正确显示中文
实战经验与踩坑记录
问题 1: Windows 中文输出乱码
现象: 抓取的中文内容显示为乱码或抛出 UnicodeEncodeError: 'gbk' codec can't encode character 原因: Windows 终端默认使用 GBK 编码,而 Python 输出 UTF-8 中文时会冲突 解决: 在 Python 脚本开头添加编码配置:
import sys
sys.stdout.reconfigure(encoding='utf-8')重要: 所有示例脚本(baidu_search_example.py、cdp_helper.py)都已添加此编码配置,直接运行不会产生乱码。
问题 2: HTML 字符实体转义
现象: JavaScript 代码中的 => 箭头函数在通过 JSON 传输时被转义为 => 原因: JSON 标准会对某些字符进行 Unicode 转义 解决: 这是正常现象,不影响实际执行。如果需要在 Python 中处理,可以使用:
import codecs
decoded = codecs.decode(string, 'unicode_escape')问题 3: 页面 ID 获取
现象: 不知道当前页面的 WebSocket URL 解决: 通过 HTTP 接口获取页面列表:
curl -s http://127.0.0.1:9222/json/list然后提取 webSocketDebuggerUrl 字段
注意: 页面 ID 是动态生成的,每次启动 Chrome 都会变化。应该在代码中动态获取而不是硬编码:
import urllib.request
import json
def get_ws_url():
req = urllib.request.Request('http://127.0.0.1:9222/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
return pages[0]['webSocketDebuggerUrl'] if pages else None问题 4: Windows 等待命令跨平台兼容性问题
现象: 在 Git Bash 中使用 timeout /t 3 /nobreak 报错 invalid time interval 原因: timeout 是 Windows CMD 命令,在 Bash 中语法不兼容 解决: 使用跨平台的等待方案:
# Windows CMD
timeout /t 3 /nobreak >nul
# Git Bash / Linux
sleep 3
# 跨平台通用(Windows)
ping -n 4 127.0.0.1 > nul问题 5: Claude Code 文件写入限制
现象: 直接使用 Write 工具创建新文件时提示 "File has not been read yet" 原因: Claude Code 要求必须先读取文件才能写入(安全机制) 解决: 1. 先使用 Read 工具读取文件(如果不存在会报错,需要用 Bash 创建空文件) 2. 或者使用 Bash 的 echo 或 cat 命令直接写入 3. 或者使用 Python 脚本动态生成并执行代码
问题 6: 动态内容提取不完整
现象: 提取的搜索结果中某些字段(如摘要)为空 原因: 1. 页面使用了动态加载,需要滚动才能显示全部内容 2. 不同网站使用不同的 class 名称 3. 某些内容是异步加载的
解决: 1. 使用多个选择器匹配:
const abstract = item.querySelector('.c-abstract, .abstract, [class*="abstract"]')?.innerText;2. 滚动页面加载更多内容:
window.scrollTo(0, document.body.scrollHeight);3. 增加等待时间确保异步内容加载完成
问题 7: 前台 vs 后台操作浏览器
CDP 支持两种操作模式,根据用户需求选择:
后台操作(默认):
- 通过 WebSocket 发送 JavaScript 命令直接操作 DOM
- 浏览器界面不会显示操作过程(用户看不到输入、点击等动作)
- 适合数据抓取、自动化测试等不需要可视化反馈的场景
- 代码示例:
# 直接设置输入框的值(用户看不到输入过程)
await ws.send(json.dumps({
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').value = '美女';"
}
}))前台操作(可视化):
- 使用 CDP 的 Input 域模拟真实用户输入
- 浏览器会显示完整的操作过程(键盘输入、鼠标点击等)
- 适合演示、教学、需要用户观察操作过程的场景
- 代码示例:
# 1. 先聚焦输入框
await ws.send(json.dumps({
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').focus();"
}
}))
# 2. 模拟键盘输入(用户能看到逐字输入)
for char in "美女":
await ws.send(json.dumps({
"method": "Input.dispatchKeyEvent",
"params": {
"type": "char",
"text": char
}
}))
await asyncio.sleep(0.1) # 模拟真实打字间隔
# 3. 模拟点击搜索按钮(用户能看到点击效果)
await ws.send(json.dumps({
"method": "Input.dispatchMouseEvent",
"params": {
"type": "mousePressed",
"x": 100,
"y": 200,
"button": "left"
}
}))选择建议:
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 数据抓取 | 后台 | 速度快,不需要可视化 |
| 自动化测试 | 后台 | 稳定可靠,不受UI影响 |
| 搜索类任务 | 后台 | 直接构造URL更简单可靠 |
| 操作演示 | 前台 | 用户能看到完整过程 |
| 教学演示 | 前台 | 便于观察和理解 |
| 表单填写 | 两者皆可 | 后台更快,前台更直观 |
搜索类任务推荐方案:
- 后台方式(推荐):直接构造搜索URL,如
https://www.baidu.com/s?wd=关键词 - 前台方式:仅当用户明确要求"可视化"、"能看到操作过程"时使用
问题 8: 搜索类任务的最佳实践
场景: 用户要求"在百度/谷歌搜索某某关键词并抓取结果"
方案对比:
| 方案 | 实现方式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| A | 后台直接访问搜索URL | 简单、快速、稳定 | 用户看不到过程 | ⭐⭐⭐⭐⭐ |
| B | 前台模拟输入点击 | 可视化、有反馈 | 复杂、慢、需处理坐标 | ⭐⭐⭐ |
推荐方案 A(后台方式):
# 直接构造搜索URL,一步到位
url = f"https://www.baidu.com/s?wd={urllib.parse.quote(keyword)}"
await client.navigate(url)
await asyncio.sleep(3) # 等待加载
results = await client.evaluate(extract_script)何时使用方案 B(前台方式):
- 用户明确要求"可视化操作"、"让我看到过程"
- 教学演示场景
- 需要展示自动化操作过程
前台方式注意事项: 1. 必须先 focus() 输入框 2. 打字间隔 0.1-0.15 秒最合适 3. 百度搜素按钮坐标约 x:650, y:230(1920x1080分辨率) 4. 需要启用 Input 域:Input.enable
前台输入文字详细步骤(以百度搜索框为例):
# 步骤 1: 启用 Input 域(必须先启用才能发送键盘事件)
await ws.send(json.dumps({
"id": 1,
"method": "Input.enable"
}))
# 步骤 2: 聚焦输入框(关键步骤,否则键盘事件可能无效)
await ws.send(json.dumps({
"id": 2,
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').focus();", # '#kw' 是百度搜索框ID
"returnByValue": True
}
}))
await asyncio.sleep(0.5) # 等待聚焦完成
# 步骤 3: 清空输入框(可选,确保输入框为空)
await ws.send(json.dumps({
"id": 3,
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').value = '';",
"returnByValue": True
}
}))
# 步骤 4: 逐字输入文字(用户能看到逐字输入动画)
text = "hello 你好呀"
for i, char in enumerate(text):
await ws.send(json.dumps({
"id": 10 + i,
"method": "Input.dispatchKeyEvent",
"params": {
"type": "char", # 输入字符
"text": char # 要输入的字符
}
}))
await asyncio.sleep(0.15) # 打字间隔,让用户能看到动画关键要点:
type: "char"用于输入普通字符- 必须先
focus()输入框,否则输入可能无效 - 间隔时间建议 0.1-0.2 秒,太短可能漏字,太长用户等待久
- 支持中文输入,无需额外编码处理
问题 9: 表单自动化执行顺序
正确顺序: 1. 启用 Runtime → 2. 填充输入框 → 3. 点击按钮 → 4. 等待页面加载 → 5. 提取数据
示例代码(百度搜索后台方式):
# 1. 填充搜索词
await ws.send(json.dumps({
"id": 2,
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#kw').value = '搜索词';",
"returnByValue": True
}
}))
# 2. 点击搜索按钮
await ws.send(json.dumps({
"id": 3,
"method": "Runtime.evaluate",
"params": {
"expression": "document.querySelector('#su').click();",
"returnByValue": True
}
}))
# 3. 等待加载
await asyncio.sleep(3)
# 4. 提取结果
await ws.send(json.dumps({
"id": 4,
"method": "Runtime.evaluate",
"params": {
"expression": """
(() => {
const items = document.querySelectorAll('.result');
return Array.from(items).map(item => ({
title: item.querySelector('h3')?.innerText,
link: item.querySelector('a')?.href
}));
})()
""",
"returnByValue": True
}
}))用户使用指南
如何使用此 Skill
用户可以通过以下方式触发此 skill:
1. 直接说明需求 - 描述你想对网页做什么 2. 使用 `/chrome-devtools-skill` 命令 - 显式调用 skill 3. 提及网页操作 - 任何涉及"打开网页、抓取数据、截图"等关键词
提示词示例
以下是一些用户可以使用的提示词模板:
基础操作
打开 https://www.example.com 并截图抓取 https://www.example.com 的页面内容分析一下这个网页:https://github.com/xxx/xxx数据提取
打开 https://www.jd.com,提取首页所有商品标题和价格访问 https://www.zhihu.com/explore,获取热门问题列表抓取 https://www.example.com 页面中的所有链接监控和分析
打开 https://www.example.com,监控页面加载的所有 API 请求分析 https://www.example.com 的页面性能访问 https://www.example.com,提取页面中的所有图片地址自动化操作
打开 https://www.example.com,在搜索框输入"关键词"并搜索访问 https://www.example.com,点击"加载更多"按钮,抓取所有列表数据提示词技巧
1. 明确 URL - 提供完整的网页地址 2. 说明目标 - 清楚描述你想获取什么数据或执行什么操作 3. 指定格式 - 如果需要特定格式,可以说明(如"以表格形式返回") 4. 复杂任务分步 - 对于多步骤任务,可以分步描述
示例对话
用户: 帮我打开百度,看看热搜榜有什么内容
AI: [自动启动 Chrome → 打开百度 → 提取热搜数据 → 返回结果]
---
用户: 抓取 https://news.ycombinator.com 的前 10 条新闻标题和链接
AI: [自动启动 Chrome → 打开 Hacker News → 提取新闻数据 → 返回表格]
---
用户: 分析一下淘宝首页都加载了哪些接口
AI: [自动启动 Chrome → 打开淘宝 → 监控网络请求 → 返回 API 列表]
参考资源
- 示例脚本: 见
scripts/目录 baidu_search_example.py- 百度搜索完整示例cdp_helper.py- CDP 客户端封装类README.md- 快速开始指南
Chrome DevTools MCP 配置信息
如果用户询问chrome-devtools MCP怎么配置,提供以下 JSON 配置信息:
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": [
"chrome-devtools-mcp@latest",
"--browser-url=http://127.0.0.1:9222"
]
}
}
}// 测试修改 // 另一个测试修改 // 测试修改 pre-push 方案
百度搜索前台模式完整实战指南
记录一次成功的百度搜索前台模式操作经验
日期: 2026-02-17
任务: 使用浏览器前台模式打开百度,输入关键词并搜索,抓取结果
---
任务概述
目标: 使用 Chrome DevTools Skill 以前台模式(可视化操作)完成百度搜索任务
前台模式特点:
- 用户能在浏览器中看到完整的操作过程(输入文字、点击按钮)
- 使用 CDP 的
Input.dispatchKeyEvent模拟真实键盘输入 - 适合演示、教学等需要可视化反馈的场景
---
完整执行步骤
步骤 1: 启动 Chrome 浏览器(远程调试模式)
Windows 命令:
start chrome --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-devtools-profile" https://www.baidu.com关键参数说明:
--remote-debugging-port=9222: 启用远程调试端口,CDP 通过此端口通信--user-data-dir="%TEMP%\chrome-devtools-profile": 使用固定用户数据目录,保持登录状态和缓存https://www.baidu.com: 直接打开百度首页
注意事项: 1. 使用固定的 user-data-dir 名称,不要每次使用不同文件夹 2. 这样 Chrome 会记住 Cookie 和缓存,下次启动更快 3. Windows 使用 start 命令可以在前台打开浏览器窗口
等待时间: 启动后等待 3-5 秒确保 Chrome 完全加载
ping -n 4 127.0.0.1 > nul---
步骤 2: 前台模式输入搜索关键词
核心代码 (Python + WebSocket):
import asyncio
import websockets
import json
import urllib.request
async def search():
# 1. 获取页面 WebSocket URL
req = urllib.request.Request('http://127.0.0.1:9222/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
ws_url = pages[0]['webSocketDebuggerUrl']
async with websockets.connect(ws_url) as ws:
# 2. 启用 Input 域(必须先启用才能发送键盘事件)
await ws.send(json.dumps({'id': 1, 'method': 'Input.enable'}))
# 3. 聚焦搜索框(关键步骤!)
await ws.send(json.dumps({
'id': 2,
'method': 'Runtime.evaluate',
'params': {
'expression': 'document.querySelector("#kw").focus();',
'returnByValue': True
}
}))
await asyncio.sleep(0.5) # 等待聚焦完成
# 4. 清空输入框(确保输入框为空)
await ws.send(json.dumps({
'id': 3,
'method': 'Runtime.evaluate',
'params': {
'expression': 'document.querySelector("#kw").value = "";',
'returnByValue': True
}
}))
# 5. 逐字输入文字(前台模式,用户能看到逐字输入动画)
text = '球球大作战团队模式教程'
for i, char in enumerate(text):
await ws.send(json.dumps({
'id': 10 + i,
'method': 'Input.dispatchKeyEvent',
'params': {
'type': 'char', # 输入字符
'text': char # 要输入的字符
}
}))
await asyncio.sleep(0.15) # 打字间隔 0.15 秒
print('输入完成:', text)
asyncio.run(search())关键要点:
1. 必须先启用 Input 域: Input.enable 是使用键盘事件的前提 2. 必须先聚焦输入框: document.querySelector("#kw").focus() 是关键,否则键盘事件可能无效 3. 百度搜索框选择器: #kw 是百度搜索框的 ID 4. 打字间隔: 0.1-0.2 秒最合适,太短可能漏字,太长用户等待久 5. 支持中文: 无需额外编码处理,直接发送中文字符即可
---
步骤 3: 点击搜索按钮
代码:
await ws.send(json.dumps({
'id': 1,
'method': 'Runtime.evaluate',
'params': {
'expression': 'document.querySelector("#su").click();',
'returnByValue': True
}
}))关键要点:
- 百度搜索按钮选择器:
#su是百度"百度一下"按钮的 ID - 使用
Runtime.evaluate执行点击脚本即可 - 点击后等待页面跳转和加载
等待时间: 点击后等待 4-5 秒确保搜索结果页加载完成
ping -n 5 127.0.0.1 > nul---
步骤 4: 抓取搜索结果数据
核心代码:
# 提取搜索结果
await ws.send(json.dumps({
'id': 1,
'method': 'Runtime.evaluate',
'params': {
'expression': '''
(() => {
const results = [];
// 使用多个选择器匹配不同结构的搜索结果
const items = document.querySelectorAll('.result, .c-container');
items.forEach((item, index) => {
// 标题选择器: 尝试多种可能的结构
const titleEl = item.querySelector('h3 a, .t a, [data-click*="title"]');
// 摘要选择器
const abstractEl = item.querySelector('.c-abstract, .content-right_8Zs40, [class*="abstract"]');
// 链接选择器
const linkEl = item.querySelector('h3 a, .t a');
if (titleEl) {
results.push({
index: index + 1,
title: titleEl.innerText?.trim() || '',
abstract: abstractEl?.innerText?.trim() || '',
link: linkEl?.href || ''
});
}
});
return results.slice(0, 10); // 只取前10条
})()
''',
'returnByValue': True
}
}))数据提取技巧:
1. 多选择器匹配: 百度搜索结果可能有不同结构,使用逗号分隔多个选择器
.result- 普通搜索结果.c-container- 另一种结果容器
2. 标题提取:
h3 a- 最常见的标题结构.t a- 另一种标题结构
3. 摘要提取:
.c-abstract- 普通摘要.content-right_8Zs40- 某些特殊结果的摘要
4. 使用可选链操作符: ?. 防止元素不存在时报错
5. 限制结果数量: 使用 .slice(0, 10) 只取前10条,避免数据过多
---
完整执行流程总结
1. 启动 Chrome(调试模式)
↓
2. 等待 3-5 秒确保加载完成
↓
3. 前台模式输入关键词
- 启用 Input 域
- 聚焦输入框 (#kw)
- 清空输入框
- 逐字输入(间隔 0.15 秒)
↓
4. 点击搜索按钮 (#su)
↓
5. 等待 4-5 秒确保结果页加载
↓
6. 提取搜索结果数据
↓
7. 格式化输出结果---
踩坑记录与解决方案
坑 1: 输入框没有聚焦导致输入无效
现象: 发送了键盘事件但输入框没有文字
原因: 没有先执行 focus() 聚焦输入框
解决: 必须先执行 document.querySelector("#kw").focus()
---
坑 2: 打字速度太快导致漏字
现象: 输入的文字不完整,少了几个字
原因: 打字间隔太短,浏览器来不及处理
解决: 将间隔从 0.05 秒调整为 0.15 秒
---
坑 3: 页面还没加载完就提取数据
现象: 提取不到搜索结果或结果为空
原因: 搜索结果页是异步加载的,需要时间渲染
解决: 点击搜索后等待至少 4-5 秒再提取数据
---
坑 4: Windows 终端中文输出乱码
现象: 抓取的中文内容显示为乱码
原因: Windows 终端默认使用 GBK 编码
解决: 在 Python 脚本开头添加编码配置
import sys
sys.stdout.reconfigure(encoding='utf-8')---
关键选择器参考
| 元素 | 选择器 | 说明 |
|---|---|---|
| 搜索框 | #kw | 百度输入框 ID |
| 搜索按钮 | #su | "百度一下"按钮 ID |
| 搜索结果容器 | .result, .c-container | 两种常见结构 |
| 标题 | h3 a, .t a | 标题链接 |
| 摘要 | .c-abstract | 结果摘要文本 |
---
最佳实践建议
1. 使用固定 user-data-dir: 保持一致的 Chrome 配置 2. 适当的等待时间: 每一步操作后都要有等待时间 3. 多选择器匹配: 提高数据提取的兼容性 4. 错误处理: 使用可选链操作符防止空值报错 5. 限制结果数量: 避免返回过多数据 6. 前台模式仅用于需要可视化的场景: 纯数据抓取建议用后台模式
---
代码模板
前台模式搜索模板
import asyncio
import websockets
import json
import urllib.request
import sys
sys.stdout.reconfigure(encoding='utf-8')
async def baidu_search_frontend(keyword):
# 获取 WebSocket URL
req = urllib.request.Request('http://127.0.0.1:9222/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
ws_url = pages[0]['webSocketDebuggerUrl']
async with websockets.connect(ws_url) as ws:
# 启用 Input
await ws.send(json.dumps({'id': 1, 'method': 'Input.enable'}))
# 聚焦并清空输入框
await ws.send(json.dumps({
'id': 2,
'method': 'Runtime.evaluate',
'params': {'expression': 'document.querySelector("#kw").focus();'}
}))
await asyncio.sleep(0.5)
await ws.send(json.dumps({
'id': 3,
'method': 'Runtime.evaluate',
'params': {'expression': 'document.querySelector("#kw").value = "";'}
}))
# 逐字输入
for i, char in enumerate(keyword):
await ws.send(json.dumps({
'id': 10 + i,
'method': 'Input.dispatchKeyEvent',
'params': {'type': 'char', 'text': char}
}))
await asyncio.sleep(0.15)
# 点击搜索
await ws.send(json.dumps({
'id': 100,
'method': 'Runtime.evaluate',
'params': {'expression': 'document.querySelector("#su").click();'}
}))
# 等待加载
await asyncio.sleep(4)
# 提取结果
await ws.send(json.dumps({
'id': 200,
'method': 'Runtime.evaluate',
'params': {
'expression': '''
(() => {
const items = document.querySelectorAll('.result, .c-container');
return Array.from(items).map((item, i) => ({
index: i + 1,
title: item.querySelector('h3 a')?.innerText?.trim() || '',
link: item.querySelector('h3 a')?.href || ''
})).filter(r => r.title);
})()
''',
'returnByValue': True
}
}))
# 接收响应
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == 200:
return data.get('result', {}).get('result', {}).get('value', [])
# 使用
results = asyncio.run(baidu_search_frontend('你的关键词'))
for r in results:
print(f"[{r['index']}] {r['title']}")---
总结
这次成功的关键经验:
1. 严格按顺序执行: 启用 Input → 聚焦 → 清空 → 输入 → 点击 → 等待 → 提取 2. 适当的等待时间: 每个步骤之间都要有等待,特别是页面加载 3. 多选择器兼容: 百度搜索结果结构多样,使用多个选择器提高成功率 4. 前台模式的核心: 使用 Input.dispatchKeyEvent 而不是直接修改 value
希望这份指南能帮助后续使用者顺利完成类似任务!
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
百度搜索前台模式完整模板
基于成功实战经验编写
使用方法:
1. 先启动 Chrome: start chrome --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-devtools-profile" https://www.baidu.com
2. 等待 3-5 秒
3. 运行此脚本: python baidu-search-template.py
"""
import asyncio
import websockets
import json
import urllib.request
import sys
# Windows 中文输出编码设置
sys.stdout.reconfigure(encoding='utf-8')
async def get_ws_url():
"""获取 Chrome DevTools WebSocket URL"""
req = urllib.request.Request('http://127.0.0.1:9222/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
return pages[0]['webSocketDebuggerUrl'] if pages else None
async def baidu_search_frontend(keyword, max_results=10):
"""
使用前台模式(可视化)在百度上搜索关键词
Args:
keyword: 搜索关键词
max_results: 最大返回结果数量
Returns:
搜索结果列表
"""
ws_url = await get_ws_url()
if not ws_url:
raise Exception("无法连接到 Chrome DevTools,请确保 Chrome 已启动")
async with websockets.connect(ws_url) as ws:
print(f"开始搜索: {keyword}")
print("-" * 60)
# ========== 步骤 1: 启用 Input 域 ==========
print("[1/5] 启用 Input 域...")
await ws.send(json.dumps({
'id': 1,
'method': 'Input.enable'
}))
await asyncio.sleep(0.3)
# ========== 步骤 2: 聚焦并清空搜索框 ==========
print("[2/5] 聚焦搜索框...")
await ws.send(json.dumps({
'id': 2,
'method': 'Runtime.evaluate',
'params': {
'expression': 'document.querySelector("#kw").focus();',
'returnByValue': True
}
}))
await asyncio.sleep(0.5)
# 清空输入框
await ws.send(json.dumps({
'id': 3,
'method': 'Runtime.evaluate',
'params': {
'expression': 'document.querySelector("#kw").value = "";',
'returnByValue': True
}
}))
# ========== 步骤 3: 逐字输入关键词(前台模式) ==========
print(f"[3/5] 输入关键词(前台可视化模式)...")
for i, char in enumerate(keyword):
await ws.send(json.dumps({
'id': 10 + i,
'method': 'Input.dispatchKeyEvent',
'params': {
'type': 'char',
'text': char
}
}))
await asyncio.sleep(0.15) # 打字间隔,让用户能看到动画
print(f" 已输入: {keyword}")
# ========== 步骤 4: 点击搜索按钮 ==========
print("[4/5] 点击搜索按钮...")
await ws.send(json.dumps({
'id': 100,
'method': 'Runtime.evaluate',
'params': {
'expression': 'document.querySelector("#su").click();',
'returnByValue': True
}
}))
# 等待搜索结果页加载
print(" 等待页面加载...")
await asyncio.sleep(4)
# ========== 步骤 5: 提取搜索结果 ==========
print("[5/5] 提取搜索结果...")
await ws.send(json.dumps({
'id': 200,
'method': 'Runtime.evaluate',
'params': {
'expression': f'''
(() => {{
const results = [];
const items = document.querySelectorAll('.result, .c-container');
items.forEach((item, index) => {{
const titleEl = item.querySelector('h3 a, .t a');
const abstractEl = item.querySelector('.c-abstract, [class*="abstract"]');
const linkEl = item.querySelector('h3 a, .t a');
if (titleEl) {{
results.push({{
index: index + 1,
title: titleEl.innerText?.trim() || '',
abstract: abstractEl?.innerText?.trim() || '',
link: linkEl?.href || ''
}});
}}
}});
return results.slice(0, {max_results});
}})()
''',
'returnByValue': True
}
}))
# 接收响应
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == 200:
results = data.get('result', {}).get('result', {}).get('value', [])
return results
def print_results(results):
"""格式化输出搜索结果"""
print()
print("=" * 80)
print("搜索结果")
print("=" * 80)
print()
for item in results:
print(f"[{item['index']}] {item['title']}")
print(f" 链接: {item['link']}")
if item['abstract']:
abstract = item['abstract'][:120]
if len(item['abstract']) > 120:
abstract += "..."
print(f" 摘要: {abstract}")
print()
print("=" * 80)
print(f"共找到 {len(results)} 条结果")
print("=" * 80)
async def main():
"""主函数"""
# 搜索关键词
keyword = "球球大作战团队模式教程"
try:
results = await baidu_search_frontend(keyword, max_results=10)
print_results(results)
except Exception as e:
print(f"错误: {e}")
print()
print("请确保:")
print("1. Chrome 已启动并开启了远程调试端口")
print("2. 命令: start chrome --remote-debugging-port=9222 --user-data-dir=\"%TEMP%\\chrome-devtools-profile\"")
return 1
return 0
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)
Chrome DevTools 前台模式速查表
快速参考:如何使用 CDP 进行前台模式(可视化)操作
---
前台模式 vs 后台模式
| 特性 | 前台模式 | 后台模式 |
|---|---|---|
| 用户可见 | ✅ 能看到输入、点击过程 | ❌ 直接操作 DOM,无动画 |
| 速度 | 较慢(需要模拟真实操作) | 快(直接执行脚本) |
| 适用场景 | 演示、教学、需要可视化反馈 | 数据抓取、自动化测试 |
| 实现方式 | Input.dispatchKeyEvent | Runtime.evaluate 直接修改 |
---
核心方法
1. 启用 Input 域(必须!)
await ws.send(json.dumps({
'id': 1,
'method': 'Input.enable'
}))注意: 所有键盘/鼠标事件之前必须先启用 Input 域
---
2. 聚焦元素
await ws.send(json.dumps({
'id': 2,
'method': 'Runtime.evaluate',
'params': {
'expression': 'document.querySelector("#input-id").focus();'
}
}))
await asyncio.sleep(0.5) # 等待聚焦完成关键: 输入前必须先聚焦,否则键盘事件可能无效
---
3. 逐字输入文字
text = '要输入的文字'
for i, char in enumerate(text):
await ws.send(json.dumps({
'id': 10 + i,
'method': 'Input.dispatchKeyEvent',
'params': {
'type': 'char', # 输入字符
'text': char, # 字符内容
'unmodifiedText': char
}
}))
await asyncio.sleep(0.15) # 打字间隔参数说明:
type:'char'表示输入字符,'keyDown'/'keyUp'用于特殊按键text: 要输入的字符(支持中文)unmodifiedText: 未修饰的文本(可选)
打字间隔建议:
- 普通场景: 0.1-0.15 秒
- 需要更真实: 0.05-0.1 秒
- 避免漏字: 不小于 0.05 秒
---
4. 模拟按键(如 Enter、Tab)
# 按下 Enter
await ws.send(json.dumps({
'id': 1,
'method': 'Input.dispatchKeyEvent',
'params': {
'type': 'keyDown',
'key': 'Enter',
'code': 'Enter',
'keyCode': 13
}
}))
await ws.send(json.dumps({
'id': 2,
'method': 'Input.dispatchKeyEvent',
'params': {
'type': 'keyUp',
'key': 'Enter',
'code': 'Enter',
'keyCode': 13
}
}))常用按键代码:
| 按键 | key | code | keyCode |
|---|---|---|---|
| Enter | 'Enter' | 'Enter' | 13 |
| Tab | 'Tab' | 'Tab' | 9 |
| Escape | 'Escape' | 'Escape' | 27 |
| Backspace | 'Backspace' | 'Backspace' | 8 |
| 空格 | ' ' | 'Space' | 32 |
---
5. 模拟鼠标点击
# 方法 1: 使用 Runtime.evaluate 直接点击元素
await ws.send(json.dumps({
'id': 1,
'method': 'Runtime.evaluate',
'params': {
'expression': 'document.querySelector("#button").click();'
}
}))
# 方法 2: 使用 Input.dispatchMouseEvent 模拟真实点击(带坐标)
await ws.send(json.dumps({
'id': 1,
'method': 'Input.dispatchMouseEvent',
'params': {
'type': 'mousePressed',
'x': 100, # X 坐标
'y': 200, # Y 坐标
'button': 'left', # 左键
'clickCount': 1
}
}))
await ws.send(json.dumps({
'id': 2,
'method': 'Input.dispatchMouseEvent',
'params': {
'type': 'mouseReleased',
'x': 100,
'y': 200,
'button': 'left',
'clickCount': 1
}
}))---
常见网站选择器参考
百度搜索
| 元素 | 选择器 |
|---|---|
| 搜索框 | #kw |
| 搜索按钮 | #su |
| 搜索结果 | .result, .c-container |
| 结果标题 | h3 a, .t a |
| 结果摘要 | .c-abstract |
谷歌搜索
| 元素 | 选择器 |
|---|---|
| 搜索框 | textarea[name="q"] |
| 搜索按钮 | input[name="btnK"] |
| 搜索结果 | .g, [data-ved] |
| 结果标题 | h3 |
通用表单元素
| 元素类型 | 选择器示例 |
|---|---|
| 输入框 (by ID) | #username |
| 输入框 (by name) | input[name="email"] |
| 输入框 (by type) | input[type="password"] |
| 按钮 (by ID) | #submit-btn |
| 按钮 (by text) | button:contains("提交") |
| 链接 (by text) | a:contains("点击这里") |
---
完整示例:前台模式登录表单
import asyncio
import websockets
import json
import urllib.request
async def login_frontend(username, password):
# 获取 WebSocket URL
req = urllib.request.Request('http://127.0.0.1:9222/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
ws_url = pages[0]['webSocketDebuggerUrl']
async with websockets.connect(ws_url) as ws:
# 1. 启用 Input
await ws.send(json.dumps({'id': 1, 'method': 'Input.enable'}))
# 2. 输入用户名
await ws.send(json.dumps({
'id': 2,
'method': 'Runtime.evaluate',
'params': {'expression': 'document.querySelector("#username").focus();'}
}))
await asyncio.sleep(0.3)
for i, char in enumerate(username):
await ws.send(json.dumps({
'id': 10 + i,
'method': 'Input.dispatchKeyEvent',
'params': {'type': 'char', 'text': char}
}))
await asyncio.sleep(0.1)
# 3. 按 Tab 切换到密码框
await ws.send(json.dumps({
'id': 50,
'method': 'Input.dispatchKeyEvent',
'params': {'type': 'keyDown', 'key': 'Tab', 'keyCode': 9}
}))
await ws.send(json.dumps({
'id': 51,
'method': 'Input.dispatchKeyEvent',
'params': {'type': 'keyUp', 'key': 'Tab', 'keyCode': 9}
}))
await asyncio.sleep(0.3)
# 4. 输入密码
for i, char in enumerate(password):
await ws.send(json.dumps({
'id': 60 + i,
'method': 'Input.dispatchKeyEvent',
'params': {'type': 'char', 'text': char}
}))
await asyncio.sleep(0.1)
# 5. 按 Enter 提交
await ws.send(json.dumps({
'id': 100,
'method': 'Input.dispatchKeyEvent',
'params': {'type': 'keyDown', 'key': 'Enter', 'keyCode': 13}
}))
await ws.send(json.dumps({
'id': 101,
'method': 'Input.dispatchKeyEvent',
'params': {'type': 'keyUp', 'key': 'Enter', 'keyCode': 13}
}))
print('登录表单已提交')
# 使用
asyncio.run(login_frontend('myuser', 'mypass'))---
常见问题排查
问题 1: 键盘事件发送了但输入框没反应
检查清单: 1. ✅ 是否启用了 Input.enable? 2. ✅ 是否先执行了 focus()? 3. ✅ 等待时间是否足够(focus 后至少 0.3 秒)? 4. ✅ 选择器是否正确?
问题 2: 中文输入乱码
解决: 确保 Python 脚本开头设置编码
import sys
sys.stdout.reconfigure(encoding='utf-8')问题 3: 打字太快导致漏字
解决: 增加打字间隔到 0.15-0.2 秒
问题 4: 页面还没加载完就操作
解决: 增加等待时间,或使用 wait_for 等待特定元素
# 等待元素出现
await ws.send(json.dumps({
'id': 1,
'method': 'Runtime.evaluate',
'params': {
'expression': '''
new Promise((resolve) => {
const check = () => {
if (document.querySelector('.result')) {
resolve('loaded');
} else {
setTimeout(check, 100);
}
};
check();
})
''',
'awaitPromise': True
}
}))---
快速命令参考
启动 Chrome(Windows)
start chrome --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-devtools-profile" [URL]获取页面列表
curl -s http://127.0.0.1:9222/json/list | python -m json.tool等待命令(Windows)
ping -n 4 127.0.0.1 > nul # 等待约 3 秒---
最佳实践
1. 总是先启用 Input 域 2. 输入前总是先聚焦 3. 使用适当的等待时间 4. 使用多选择器提高兼容性 5. 前台模式仅用于需要可视化的场景 6. 后台模式更适合纯数据抓取
---
最后更新: 2026-02-17
Chrome DevTools Skill 参考资料
本目录包含 Chrome DevTools Skill 的实战经验和参考文档,帮助用户更好地使用此技能。
---
文档列表
| 文件名 | 说明 | 适用场景 |
|---|---|---|
| baidu-search-frontend-mode-guide.md | 百度搜索前台模式完整实战指南 | 需要可视化操作百度搜索 |
| cdp-frontend-mode-cheatsheet.md | 前台模式速查表 | 快速查阅 CDP 前台操作方法 |
| baidu-search-template.py | 百度搜索模板脚本 | 直接运行或修改使用 |
---
快速开始
场景 1: 我想了解如何使用前台模式
阅读 baidu-search-frontend-mode-guide.md,这是一份基于成功实战的详细指南。
场景 2: 我需要快速查阅某个方法
查看 cdp-frontend-mode-cheatsheet.md,包含常用方法和选择器参考。
场景 3: 我想直接运行百度搜索
1. 启动 Chrome:
start chrome --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-devtools-profile" https://www.baidu.com2. 等待 3-5 秒
3. 运行脚本:
python references/baidu-search-template.py---
什么是前台模式?
前台模式(可视化模式)是指:
- 用户能在浏览器中看到完整的操作过程
- 文字逐字输入、按钮被点击都有视觉反馈
- 使用 CDP 的
Input.dispatchKeyEvent模拟真实用户输入
适用场景:
- 操作演示
- 教学演示
- 需要用户观察操作过程的场景
不适用场景:
- 纯数据抓取(后台模式更快)
- 大批量自动化(前台模式较慢)
---
核心要点总结
前台模式操作顺序
1. 启用 Input 域 (Input.enable)
2. 聚焦目标元素 (focus())
3. 清空输入框 (可选)
4. 逐字输入 (Input.dispatchKeyEvent, 间隔 0.15s)
5. 点击按钮 或 按 Enter
6. 等待页面加载
7. 提取数据关键成功因素
1. 必须先启用 Input 域 2. 必须先聚焦输入框 3. 适当的等待时间(每步之间) 4. 打字间隔不要太快(0.1-0.2 秒)
---
常见问题
Q: 前台模式和后台模式有什么区别?
A: 前台模式用户能看到操作过程(逐字输入动画),后台模式直接操作 DOM 无动画。前台适合演示,后台适合数据抓取。
Q: 为什么我的输入没有反应?
A: 检查清单: 1. 是否启用了 Input.enable? 2. 是否先执行了 focus()? 3. 等待时间是否足够?
Q: 打字太快导致漏字怎么办?
A: 增加打字间隔到 0.15-0.2 秒。
Q: Windows 终端中文乱码?
A: 在 Python 脚本开头添加:
import sys
sys.stdout.reconfigure(encoding='utf-8')---
贡献
如果你有新的实战经验或改进建议,欢迎更新这些文档。
---
最后更新: 2026-02-17
"""
使用 Chrome DevTools Protocol 搜索百度并提取结果的完整示例
工作流程:
1. 启动 Chrome 远程调试模式(固定用户数据目录)
2. 通过 WebSocket 连接到 Chrome DevTools
3. 导航到百度搜索页面
4. 执行搜索
5. 提取搜索结果
"""
import asyncio
import websockets
import json
import urllib.request
import ssl
import sys
# 忽略 SSL 验证
ssl._create_default_https_context = ssl._create_unverified_context
# 固定的用户数据目录(重要:保持一致性)
CHROME_USER_DATA_DIR = "%TEMP%\\chrome-devtools-profile"
CDP_HTTP_URL = "http://127.0.0.1:9222"
async def get_page_ws_url():
"""获取第一个页面的 WebSocket URL"""
req = urllib.request.Request(f'{CDP_HTTP_URL}/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
if pages:
return pages[0]['webSocketDebuggerUrl']
return None
async def search_baidu(keyword):
"""
在百度上搜索关键词并提取结果
Args:
keyword: 搜索关键词
Returns:
搜索结果列表
"""
# 获取页面 WebSocket URL
ws_url = await get_page_ws_url()
if not ws_url:
print("未找到可用的 Chrome 页面")
return []
print(f"连接到: {ws_url}")
async with websockets.connect(ws_url) as ws:
# 启用 Runtime 域
await ws.send(json.dumps({"id": 1, "method": "Runtime.enable"}))
# 等待启用响应
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == 1:
break
# 导航到百度搜索页面
navigate_script = f'''
window.location.href = "https://www.baidu.com/s?wd={keyword}";
'''
await ws.send(json.dumps({
"id": 2,
"method": "Runtime.evaluate",
"params": {"expression": navigate_script}
}))
# 等待导航完成
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == 2:
break
# 等待页面加载
await asyncio.sleep(3)
# 提取搜索结果
extract_script = """
(function() {
var results = [];
var containers = document.querySelectorAll('.result, .c-container');
containers.forEach(function(item, index) {
var titleEl = item.querySelector('h3 a, .t a');
if (!titleEl) return;
var title = titleEl.innerText.trim();
var link = titleEl.href;
var abstractEl = item.querySelector('.c-abstract, .abstract');
var abstract = abstractEl ? abstractEl.innerText.trim() : '';
if (title) {
results.push({
index: index + 1,
title: title,
link: link,
abstract: abstract.substring(0, 200)
});
}
});
return results;
})()
"""
await ws.send(json.dumps({
"id": 3,
"method": "Runtime.evaluate",
"params": {
"expression": extract_script,
"returnByValue": True
}
}))
# 获取响应
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == 3:
result_value = data.get('result', {}).get('result', {}).get('value', [])
return result_value
return []
async def main():
"""主函数"""
# 设置 UTF-8 编码输出
sys.stdout.reconfigure(encoding='utf-8')
# 搜索关键词
keyword = "美女"
print(f"正在搜索: {keyword}\n")
results = await search_baidu(keyword)
print(f"\n=== 百度搜索 '{keyword}' 结果 ===")
print(f"共找到 {len(results)} 条结果\n")
for item in results:
print(f"[{item['index']}] {item['title']}")
print(f" 链接: {item['link'][:60]}...")
if item['abstract']:
print(f" 摘要: {item['abstract'][:100]}...")
print()
return results
if __name__ == "__main__":
results = asyncio.run(main())
"""
百度搜索可视化操作示例(前台模式)
此脚本演示如何在浏览器中显示操作过程:
- 用户能看到文字逐字输入到搜索框
- 用户能看到搜索按钮被点击
- 适合演示、教学等需要可视化反馈的场景
工作流程:
1. 启动 Chrome 远程调试模式
2. 通过 WebSocket 连接到 Chrome DevTools
3. 使用 Input 域模拟真实用户输入(可视化)
4. 使用 Input 域模拟鼠标点击(可视化)
5. 等待页面加载并提取结果
"""
import asyncio
import websockets
import json
import urllib.request
import ssl
import sys
# 修复 Windows 终端中文输出乱码
sys.stdout.reconfigure(encoding='utf-8')
# 忽略 SSL 验证
ssl._create_default_https_context = ssl._create_unverified_context
CDP_HTTP_URL = "http://127.0.0.1:9222"
async def get_page_ws_url():
"""获取第一个页面的 WebSocket URL"""
req = urllib.request.Request(f'{CDP_HTTP_URL}/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
if pages:
return pages[0]['webSocketDebuggerUrl']
return None
async def search_baidu_visual(keyword):
"""
可视化搜索百度 - 用户能在浏览器中看到操作过程
Args:
keyword: 搜索关键词
Returns:
搜索结果列表
"""
ws_url = await get_page_ws_url()
if not ws_url:
print("未找到可用的 Chrome 页面")
return []
print(f"连接到: {ws_url}")
print(f"\n=== 开始可视化操作 ===")
print(f"请在浏览器中观察操作过程...\n")
async with websockets.connect(ws_url) as ws:
msg_id = 0
def send_msg(method, params=None):
nonlocal msg_id
msg_id += 1
msg = {"id": msg_id, "method": method}
if params:
msg["params"] = params
return msg_id, json.dumps(msg)
# 1. 启用 Runtime 和 Input 域
await ws.send(json.dumps({"id": 1, "method": "Runtime.enable"}))
await ws.send(json.dumps({"id": 2, "method": "Input.enable"}))
await asyncio.sleep(0.5)
# 2. 聚焦到搜索框
print("步骤 1: 聚焦搜索框...")
mid, msg = send_msg("Runtime.evaluate", {
"expression": "document.querySelector('#kw').focus();",
"returnByValue": True
})
await ws.send(msg)
await asyncio.sleep(0.5)
# 3. 逐字输入关键词(用户能看到输入过程)
print(f"步骤 2: 输入关键词 '{keyword}'...")
for char in keyword:
mid, msg = send_msg("Input.dispatchKeyEvent", {
"type": "char",
"text": char
})
await ws.send(msg)
await asyncio.sleep(0.1) # 模拟真实打字间隔
await asyncio.sleep(0.5)
# 4. 获取搜索按钮位置并点击
print("步骤 3: 点击搜索按钮...")
# 先获取按钮位置
mid, msg = send_msg("Runtime.evaluate", {
"expression": """
(function() {
var btn = document.querySelector('#su');
var rect = btn.getBoundingClientRect();
return {x: rect.left + rect.width/2, y: rect.top + rect.height/2};
})()
""",
"returnByValue": True
})
await ws.send(msg)
# 接收按钮位置
btn_pos = None
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == mid:
btn_pos = data.get('result', {}).get('result', {}).get('value')
break
if btn_pos:
# 模拟鼠标按下
mid, msg = send_msg("Input.dispatchMouseEvent", {
"type": "mousePressed",
"x": btn_pos['x'],
"y": btn_pos['y'],
"button": "left",
"clickCount": 1
})
await ws.send(msg)
# 模拟鼠标释放
mid, msg = send_msg("Input.dispatchMouseEvent", {
"type": "mouseReleased",
"x": btn_pos['x'],
"y": btn_pos['y'],
"button": "left",
"clickCount": 1
})
await ws.send(msg)
print("步骤 4: 等待页面加载...")
await asyncio.sleep(4)
# 5. 提取搜索结果
print("步骤 5: 提取搜索结果...\n")
mid, msg = send_msg("Runtime.evaluate", {
"expression": """
(function() {
var results = [];
var containers = document.querySelectorAll('.result, .c-container');
containers.forEach(function(item, index) {
var titleEl = item.querySelector('h3 a, .t a');
if (!titleEl) return;
var title = titleEl.innerText.trim();
var link = titleEl.href;
var abstractEl = item.querySelector('.c-abstract, .abstract');
var abstract = abstractEl ? abstractEl.innerText.trim() : '';
if (title) {
results.push({
index: index + 1,
title: title,
link: link,
abstract: abstract.substring(0, 200)
});
}
});
return results;
})()
""",
"returnByValue": True
})
await ws.send(msg)
# 获取结果
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == mid:
return data.get('result', {}).get('result', {}).get('value', [])
return []
async def main():
"""主函数"""
keyword = "美女"
print(f"准备搜索: {keyword}")
print("请确保 Chrome 浏览器已打开并处于前台\n")
results = await search_baidu_visual(keyword)
print(f"=== 百度搜索 '{keyword}' 结果 ===")
print(f"共找到 {len(results)} 条结果\n")
for item in results:
print(f"[{item['index']}] {item['title']}")
print(f" 链接: {item['link'][:60]}...")
if item['abstract']:
print(f" 摘要: {item['abstract'][:100]}...")
print()
return results
if __name__ == "__main__":
results = asyncio.run(main())
"""
Chrome DevTools Protocol (CDP) 助手类
封装了常用的 CDP 操作,简化 WebSocket 通信
"""
import asyncio
import websockets
import json
import urllib.request
import ssl
import sys
from typing import Optional, Any, Dict, List
# 修复 Windows 终端中文输出乱码问题
sys.stdout.reconfigure(encoding='utf-8')
ssl._create_default_https_context = ssl._create_unverified_context
class CDPClient:
"""Chrome DevTools Protocol 客户端"""
def __init__(self, debugger_url: str = "http://127.0.0.1:9222"):
self.debugger_url = debugger_url
self.ws = None
self.message_id = 0
self.pending_messages: Dict[int, asyncio.Future] = {}
async def connect(self, page_id: Optional[str] = None) -> bool:
"""
连接到 Chrome DevTools
Args:
page_id: 页面 ID,如果不指定则连接第一个页面
Returns:
是否连接成功
"""
try:
if page_id:
ws_url = f"ws://127.0.0.1:9222/devtools/page/{page_id}"
else:
# 获取第一个页面
pages = self.get_pages()
if not pages:
print("未找到可用的页面")
return False
ws_url = pages[0]['webSocketDebuggerUrl']
self.ws = await websockets.connect(ws_url)
# 启动消息接收循环
asyncio.create_task(self._receive_loop())
# 启用 Runtime
await self.send_command("Runtime.enable")
return True
except Exception as e:
print(f"连接失败: {e}")
return False
def get_pages(self) -> List[Dict]:
"""获取所有页面列表"""
req = urllib.request.Request(f'{self.debugger_url}/json/list')
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode())
async def _receive_loop(self):
"""接收消息循环"""
try:
async for message in self.ws:
data = json.loads(message)
# 处理带 id 的响应
if 'id' in data and data['id'] in self.pending_messages:
future = self.pending_messages.pop(data['id'])
future.set_result(data)
# 可以在这里处理事件(如 console.log)
elif 'method' in data:
pass # 忽略事件
except websockets.exceptions.ConnectionClosed:
print("WebSocket 连接已关闭")
async def send_command(self, method: str, params: Optional[Dict] = None) -> Dict:
"""
发送 CDP 命令
Args:
method: CDP 方法名
params: 方法参数
Returns:
响应数据
"""
self.message_id += 1
msg_id = self.message_id
command = {
"id": msg_id,
"method": method
}
if params:
command["params"] = params
# 创建 Future 等待响应
future = asyncio.Future()
self.pending_messages[msg_id] = future
# 发送命令
await self.ws.send(json.dumps(command))
# 等待响应
return await future
async def evaluate(self, script: str, return_by_value: bool = True) -> Any:
"""
在页面中执行 JavaScript
Args:
script: JavaScript 代码
return_by_value: 是否返回值
Returns:
执行结果
"""
response = await self.send_command("Runtime.evaluate", {
"expression": script,
"returnByValue": return_by_value
})
result = response.get('result', {}).get('result', {})
if 'value' in result:
return result['value']
elif 'description' in result:
return result['description']
else:
return None
async def navigate(self, url: str) -> bool:
"""
导航到指定 URL
Args:
url: 目标 URL
Returns:
是否导航成功
"""
try:
await self.evaluate(f'window.location.href = "{url}";')
await asyncio.sleep(2) # 等待页面加载
return True
except Exception as e:
print(f"导航失败: {e}")
return False
async def screenshot(self, output_path: str = "screenshot.png") -> bool:
"""
截取页面截图
Args:
output_path: 截图保存路径
Returns:
是否截图成功
"""
try:
import base64
response = await self.send_command("Page.captureScreenshot")
data = response.get('result', {}).get('data', '')
if data:
with open(output_path, 'wb') as f:
f.write(base64.b64decode(data))
print(f"截图已保存: {output_path}")
return True
except Exception as e:
print(f"截图失败: {e}")
return False
async def close(self):
"""关闭连接"""
if self.ws:
await self.ws.close()
# 使用示例
async def example():
"""使用 CDPClient 的示例"""
client = CDPClient()
# 连接 Chrome
if not await client.connect():
return
try:
# 导航到百度
await client.navigate("https://www.baidu.com")
# 执行搜索
await client.evaluate('''
document.querySelector('#kw').value = 'Python';
document.querySelector('#su').click();
''')
await asyncio.sleep(3)
# 提取搜索结果
results = await client.evaluate('''
(function() {
var items = document.querySelectorAll('.result');
var data = [];
items.forEach(function(item, i) {
var title = item.querySelector('h3 a');
if (title) {
data.push({
index: i + 1,
title: title.innerText
});
}
});
return data;
})()
''')
print("搜索结果:", results)
# 截图
await client.screenshot("baidu_python.png")
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(example())
Chrome DevTools Skill 参考脚本
这里存放使用 Chrome DevTools Protocol 的示例脚本和工具类。
文件说明
| 文件 | 说明 | 适用场景 |
|---|---|---|
baidu_search_example.py | 后台模式百度搜索示例 | 数据抓取、批量处理(推荐) |
baidu_search_visual.py | 前台模式百度搜索示例 | 演示、教学(需要可视化) |
cdp_helper.py | CDP 客户端封装类 | 简化 WebSocket 通信 |
搜索类任务指南
选择哪种方式?
后台方式(推荐):
- ✅ 直接构造搜索URL,简单可靠
- ✅ 速度快,无需等待输入动画
- ✅ 代码简洁,易于维护
- ❌ 用户看不到操作过程
前台方式:
- ✅ 可视化操作,用户能看到过程
- ✅ 适合演示和教学
- ❌ 实现复杂,需要处理坐标和时序
- ❌ 速度较慢,需要等待输入动画
快速选择
用户说"搜索XXX并抓取数据" → 使用后台方式
用户说"用可视化/前台方式搜索XXX" → 使用前台方式快速开始
1. 启动 Chrome 远程调试
# Windows
start chrome --remote-debugging-port=9222 --user-data-dir="%TEMP%\chrome-devtools-profile"
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir="$TMPDIR/chrome-devtools-profile"
# Linux
google-chrome --remote-debugging-port=9222 --user-data-dir="/tmp/chrome-devtools-profile"2. 运行示例
python baidu_search_example.py常见问题
1. WebSocket 响应匹配
必须按 id 匹配请求和响应,因为会收到各种事件消息:
# 正确做法
await ws.send(json.dumps({"id": 1, "method": "Runtime.evaluate", ...}))
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get('id') == 1: # 匹配请求 id
return data2. 中文编码问题
Windows 终端需要设置 UTF-8 编码:
import sys
sys.stdout.reconfigure(encoding='utf-8')3. 页面加载等待
执行导航后需要等待页面加载:
await asyncio.sleep(2) # 简单等待
# 或等待特定元素4. 页面 ID 动态获取
页面 ID 是动态生成的,不要硬编码,应该动态获取:
import urllib.request
import json
def get_ws_url():
req = urllib.request.Request('http://127.0.0.1:9222/json/list')
with urllib.request.urlopen(req) as response:
pages = json.loads(response.read().decode())
return pages[0]['webSocketDebuggerUrl'] if pages else None5. 动态内容提取技巧
如果提取的内容不完整(如摘要为空):
// 使用多个选择器匹配
const abstract = item.querySelector('.c-abstract, .abstract, [class*="abstract"]')?.innerText;
// 滚动页面加载更多内容
window.scrollTo(0, document.body.scrollHeight);6. 前台可视化操作
如果需要在浏览器中显示操作过程(如输入文字、点击按钮),使用 Input 域:
# 模拟键盘输入(用户能看到逐字输入)
for char in "美女":
await ws.send(json.dumps({
"id": 10,
"method": "Input.dispatchKeyEvent",
"params": {
"type": "char",
"text": char
}
}))
await asyncio.sleep(0.1) # 打字间隔
# 模拟鼠标点击
await ws.send(json.dumps({
"id": 11,
"method": "Input.dispatchMouseEvent",
"params": {
"type": "mousePressed",
"x": 100, # 点击位置 X 坐标
"y": 200, # 点击位置 Y 坐标
"button": "left"
}
}))后台 vs 前台对比:
| 方式 | 方法 | 用户可见 | 适用场景 |
|---|---|---|---|
| 后台 | Runtime.evaluate | ❌ 不可见 | 数据抓取、自动化测试 |
| 前台 | Input.dispatchKeyEvent/MouseEvent | ✅ 可见 | 演示、教学、需要可视化反馈 |