CDP 浏览器操控原理:让脚本接管你的浏览器
上一篇讲了 WorkBuddy Skill 的基本概念,这篇我们深入技术核心------如何用 Chrome DevTools Protocol + Playwright 实现真正的浏览器自动化。文中的所有代码都来自一个实际跑通的自动化项目(每天自动浏览 96 页课程、批改 62 份作业)。
1. 为什么不用 Selenium
做过 Web 自动化的同学第一反应通常是 Selenium。但它有几个致命问题:
| 问题 | Selenium | CDP + Playwright |
|---|---|---|
| 被检测 | WebDriver 特征明显 | 直接连真实浏览器,无特征 |
| 登录态 | 需要写登录流程 | 复用已有登录态 |
| 验证码 | 每次都要过 | 手动登录一次,Cookie 复用 |
| 维护成本 | 登录流程一变就挂 | 不受登录流程影响 |
核心区别:Selenium 是"开一个受控的新浏览器",CDP 是"接管你已经登录好的浏览器"。
对于需要短信验证码双因子认证的平台,CDP 几乎是唯一可行的方案。
2. CDP 协议简述
Chrome DevTools Protocol 是 Chromium 内核浏览器暴露的调试协议。你按 F12 打开开发者工具时,浏览器内部走的就是 CDP。
CDP 的通信基于 WebSocket,默认调试端口是 9222。连接后你可以做几乎任何事:
- 导航到指定 URL
- 执行 JavaScript(包括获取/修改 Angular scope)
- 获取 DOM 节点、点击、输入
- 监听网络请求
- 截图
Playwright 封装了 CDP 协议,提供了更友好的 Python API。
3. 启动浏览器调试模式
Windows(Edge)
batch
:: start_edge_debug.bat
taskkill /F /IM msedge.exe 2>nul
start msedge.exe ^
--remote-debugging-port=9222 ^
--user-data-dir="C:\Users\%USERNAME%\edge-debug-profile" ^
--no-first-run ^
--no-default-browser-check
关键参数
| 参数 | 作用 |
|---|---|
--remote-debugging-port=9222 |
暴露 CDP 调试端口 |
--user-data-dir=... |
独立用户目录,不会干扰你日常用的浏览器 |
--no-first-run |
跳过首次运行向导 |
⚠️ 必须先启动调试浏览器,再运行 Python 脚本。
4. Playwright 连接调试浏览器
python
from playwright.sync_api import sync_playwright
CDP_URL = "http://localhost:9222"
def connect_browser():
"""连接到已打开的调试 Edge"""
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(CDP_URL)
context = browser.contexts[0] # 使用已有上下文
page = context.pages[0] # 使用已有标签页
return p, browser, context, page
连接后,page 对象就是你在调试浏览器里看到的那个标签页。
5. Cookie 管理:一次登录,长期使用
这是自动化工程中最关键的一环:
scss
首次运行
↓
[没有 Cookie 文件]
↓
需要用户手动登录
(含短信验证码)
↓
脚本检测登录成功
↓
save_storage_state() → cookies.json
↓
┌──────────────────────────┐
│ 后续每次运行 │
│ load cookies.json │
│ ↓ │
│ 访问平台首页 │
│ ├─ 正常 → 继续执行 │
│ └─ 重定向 → Cookie失效 │
│ ↓ │
│ 等用户手动登录 │
└──────────────────────────┘
保存 Cookie
python
import json
def save_cookies(context, filepath="cookies.json"):
state = context.storage_state()
with open(filepath, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False)
加载与检测
python
def load_cookies(browser, filepath="cookies.json"):
if not os.path.exists(filepath):
return None, None
with open(filepath, "r") as f:
state = json.load(f)
context = browser.new_context(storage_state=state)
return context, context.new_page()
def check_login(page, platform_url):
page.goto(platform_url, wait_until="domcontentloaded")
page.wait_for_timeout(3000)
if "login" in page.url.lower():
return False
return True
实际经验:某教育平台的 Cookie 约 7-10 天过期。设好失效检测,别让脚本静默失败。
6. Angular 页面操控进阶
现代 Web 应用大量使用 Angular/React/Vue,直接改 DOM 的值往往无效------框架有自己的状态管理。
问题场景
你看到一个 <select> 元素,用 Playwright 选了选项,但 Angular 控制器不知道,提交时还是旧值。
解决方案:Scope 注入
javascript
// 获取 Angular scope
const scope = angular.element(
document.querySelector('[ng-controller]')
).scope();
// 修改数据
scope.conditions.role = ['assistant_instructor'];
// 通知 Angular 更新
scope.$apply();
Python 侧封装
python
def set_angular_scope(page, selector, expression):
return page.evaluate(f"""
(() => {{
const el = document.querySelector('{selector}');
if (!el) return 'not found';
const scope = angular.element(el).scope();
{expression}
scope.$apply();
return 'ok';
}})()
""")
实战:触发隐藏弹窗
python
def trigger_modal(page):
page.evaluate("""
(() => {
const modal = document.querySelector('#give-score');
if (modal) {
modal.style.display = 'block';
modal.style.visibility = 'visible';
}
})()
""")
7. Select2 / MultiSelect 组件处理
这是自动化中最容易翻车的地方。jQuery UI MultiSelect 或 Select2 组件不能通过简单设 value 来选值。
❌ 错误做法
python
page.select_option("select.course-role", "assistant_instructor")
✅ 正确做法
python
page.evaluate("""
(() => {
const scope = angular.element(
document.querySelector('[ng-controller="CourseListController"]')
).scope();
scope.conditions.role = ['assistant_instructor'];
scope.search(); // 触发后端筛选
})()
""")
核心原则:找框架组件的控制器 scope,改 scope 上的值,调 scope 的方法------不碰 DOM。
8. 完整连接流程模板
python
import json
from playwright.sync_api import sync_playwright
CDP_URL = "http://localhost:9222"
PLATFORM_URL = "https://your-platform.com"
def main():
with sync_playwright() as p:
# 1. 连接调试浏览器
browser = p.chromium.connect_over_cdp(CDP_URL)
# 2. 加载 Cookie
context, page = load_cookies(browser)
if not context:
context = browser.contexts[0]
page = context.pages[0]
# 3. 检测登录态
if not check_login(page, PLATFORM_URL):
print("请在浏览器中手动登录,完成后按回车...")
input()
save_cookies(context)
# 4. 执行任务
do_tasks(page)
# 5. 保存 Cookie
save_cookies(context)
9. 常见踩坑
| 坑 | 现象 | 解决 |
|---|---|---|
| 连不上 9222 | Connection refused | 确认调试浏览器已启动 |
| 页面空白 | 内容不可见 | Angular 初始化慢,等 10 秒以上 |
| scope undefined | 取不到 scope | 确认元素有 ng-controller,等待足够久 |
| Cookie 写入失败 | 登录后仍需重登 | storage_state() 需包含 localStorage |
| 端口占用 | 启动失败 | taskkill /F /IM msedge.exe |
10. 小结
| 学会的 | 要点 |
|---|---|
| CDP vs Selenium | CDP 复用已登录态,绕过验证码 |
| 启动调试浏览器 | --remote-debugging-port=9222 + 独立 user-data-dir |
| Playwright 连接 | connect_over_cdp() |
| Cookie 管理 | storage_state() 保存/加载 + 失效检测 |
| Angular 操控 | scope 注入 + $apply(),别直接改 DOM |
| Select2 组件 | 走 scope 不走 DOM |
下一篇预告
第 3 节:登录态持久化与双因子认证------Cookie 生命周期管理、失败自动恢复、多终端同步策略。敬请关注!
本文首发于稀土掘金,作者实战运行自动化脚本超百次,代码全部可运行。欢迎关注后续连载。