CDP 浏览器操控原理:让脚本接管你的浏览器

0 阅读5分钟

CDP 浏览器操控原理:让脚本接管你的浏览器

上一篇讲了 WorkBuddy Skill 的基本概念,这篇我们深入技术核心——如何用 Chrome DevTools Protocol + Playwright 实现真正的浏览器自动化。文中的所有代码都来自一个实际跑通的自动化项目(每天自动浏览 96 页课程、批改 62 份作业)。


1. 为什么不用 Selenium

做过 Web 自动化的同学第一反应通常是 Selenium。但它有几个致命问题:

问题SeleniumCDP + 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)

:: 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 连接调试浏览器

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 管理:一次登录,长期使用

这是自动化工程中最关键的一环:

                    首次运行
                       ↓
              [没有 Cookie 文件]
                       ↓
              需要用户手动登录
              (含短信验证码)
                       ↓
              脚本检测登录成功
                       ↓
        save_storage_state() → cookies.json
                       ↓
            ┌──────────────────────────┐
            │    后续每次运行            │
            │  load cookies.json       │
            │       ↓                  │
            │  访问平台首页             │
            │   ├─ 正常 → 继续执行      │
            │   └─ 重定向 → Cookie失效  │
            │              ↓           │
            │        等用户手动登录      │
            └──────────────────────────┘

保存 Cookie

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)

加载与检测

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 注入

// 获取 Angular scope
const scope = angular.element(
  document.querySelector('[ng-controller]')
).scope();

// 修改数据
scope.conditions.role = ['assistant_instructor'];

// 通知 Angular 更新
scope.$apply();

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';
        }})()
    """)

实战:触发隐藏弹窗

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 来选值

❌ 错误做法

page.select_option("select.course-role", "assistant_instructor")

✅ 正确做法

page.evaluate("""
    (() => {
        const scope = angular.element(
            document.querySelector('[ng-controller="CourseListController"]')
        ).scope();
        scope.conditions.role = ['assistant_instructor'];
        scope.search();  // 触发后端筛选
    })()
""")

核心原则:找框架组件的控制器 scope,改 scope 上的值,调 scope 的方法——不碰 DOM。


8. 完整连接流程模板

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. 常见踩坑

现象解决
连不上 9222Connection refused确认调试浏览器已启动
页面空白内容不可见Angular 初始化慢,等 10 秒以上
scope undefined取不到 scope确认元素有 ng-controller,等待足够久
Cookie 写入失败登录后仍需重登storage_state() 需包含 localStorage
端口占用启动失败taskkill /F /IM msedge.exe

10. 小结

学会的要点
CDP vs SeleniumCDP 复用已登录态,绕过验证码
启动调试浏览器--remote-debugging-port=9222 + 独立 user-data-dir
Playwright 连接connect_over_cdp()
Cookie 管理storage_state() 保存/加载 + 失效检测
Angular 操控scope 注入 + $apply(),别直接改 DOM
Select2 组件走 scope 不走 DOM

下一篇预告

第 3 节:登录态持久化与双因子认证——Cookie 生命周期管理、失败自动恢复、多终端同步策略。敬请关注!


上一篇:第1节:WorkBuddy Skill 入门

本文首发于稀土掘金,作者实战运行自动化脚本超百次,代码全部可运行。欢迎关注后续连载。