强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Chromium / ChromeDriver 完全指南 / 10 - 浏览器扩展

10 - 浏览器扩展

在自动化脚本中加载、测试和调试浏览器扩展,掌握 Manifest V3 扩展的自动化能力。


10.1 浏览器扩展基础

浏览器扩展 (Browser Extension) 是运行在浏览器中的小程序,可以修改网页行为、添加 UI 元素、拦截网络请求等。

Manifest 版本

版本 Chrome 支持 说明
Manifest V2 (MV2) 2024 年起逐步淘汰 Service Worker 替代 Background Page
Manifest V3 (MV3) Chrome 88+ (推荐) 安全性更高,权限更细,性能更好

扩展结构

my-extension/
├── manifest.json          # 扩展配置 (必需)
├── background.js          # Service Worker (MV3) / Background Page (MV2)
├── content.js             # Content Script (注入到网页)
├── popup.html             # 弹出窗口 UI
├── popup.js
├── options.html           # 设置页面
├── icons/
│   ├── icon16.png
│   ├── icon48.png
│   └── icon128.png
└── _locales/              # 国际化
    ├── zh_CN/
    │   └── messages.json
    └── en/
        └── messages.json

MV3 manifest.json 示例

{
  "manifest_version": 3,
  "name": "我的扩展",
  "version": "1.0.0",
  "description": "一个示例扩展",
  "permissions": ["activeTab", "storage", "scripting"],
  "host_permissions": ["https://*.example.com/*"],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content.js"],
      "css": ["content.css"],
      "run_at": "document_end"
    }
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png"
    }
  }
}

10.2 Selenium 加载扩展

加载 CRX 文件

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()

# 加载 .crx 扩展文件
options.add_extension("/path/to/extension.crx")

# 加载多个扩展
options.add_extension("/path/to/ext1.crx")
options.add_extension("/path/to/ext2.crx")

# 注意: --headless (旧版) 不支持扩展加载
# --headless=new 支持扩展加载
options.add_argument("--headless=new")

driver = webdriver.Chrome(options=options)
driver.get("https://example.com")

加载解压扩展目录

options = Options()

# 加载解压的扩展目录 (开发阶段常用)
options.add_argument("--load-extension=/path/to/my-extension")

# 加载多个扩展
options.add_argument(
    "--load-extension=/path/to/ext1,/path/to/ext2"
)

# 禁用其他扩展 (排除干扰)
options.add_argument("--disable-extensions-except=/path/to/my-extension")

driver = webdriver.Chrome(options=options)

等待扩展加载

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def wait_for_extension(driver, extension_id, timeout=10):
    """等待扩展加载完成"""
    # 扩展页面可通过 chrome-extension://ID/ 访问
    driver.get(f"chrome-extension://{extension_id}/popup.html")
    WebDriverWait(driver, timeout).until(
        EC.presence_of_element_located((By.TAG_NAME, "body"))
    )

# 获取扩展 ID
# 方法 1: 手动查看 chrome://extensions
# 方法 2: 通过 CDP 获取
extensions_info = driver.execute_cdp_cmd("Management.getAll", {})
for ext in extensions_info:
    print(f"ID: {ext['id']}, Name: {ext['name']}")

10.3 Playwright 加载扩展

const { chromium } = require('playwright');
const path = require('path');

(async () => {
  const extensionPath = path.resolve(__dirname, './my-extension');

  const context = await chromium.launchPersistentContext('', {
    headless: false,  // Playwright 扩展加载需要有头模式
    args: [
      `--disable-extensions-except=${extensionPath}`,
      `--load-extension=${extensionPath}`,
    ],
  });

  // 等待扩展背景页面
  const background = context.serviceWorkers()[0] ||
                     context.waitForEvent('serviceworker');

  const page = await context.newPage();
  await page.goto('https://example.com');

  // ... 测试

  await context.close();
})();

Playwright — 测试扩展弹出窗口

test('扩展弹出窗口工作正常', async ({ context }) => {
  // 获取扩展 ID
  let [background] = context.serviceWorkers();
  if (!background) {
    background = await context.waitForEvent('serviceworker');
  }
  const extensionId = background.url().split('/')[2];

  // 打开扩展弹出页面
  const page = await context.newPage();
  await page.goto(`chrome-extension://${extensionId}/popup.html`);

  // 测试弹出窗口内容
  await expect(page.locator('h1')).toHaveText('我的扩展');
  await page.getByRole('button', { name: '启用' }).click();
  await expect(page.locator('.status')).toHaveText('已启用');
});

10.4 常用扩展自动化场景

10.4.1 广告拦截器测试

def test_adblock():
    options = Options()
    options.add_extension("/path/to/adblock.crx")
    options.add_argument("--headless=new")

    driver = webdriver.Chrome(options=options)
    driver.get("https://example-news-site.com")

    # 验证广告被拦截
    ads = driver.find_elements(By.CSS_SELECTOR, ".ad-banner, .ad-container")
    assert len(ads) == 0, f"仍有 {len(ads)} 个广告元素未被拦截"

    # 检查网络请求 (通过 CDP)
    logs = driver.get_log("performance")
    ad_requests = [
        log for log in logs
        if "doubleclick.net" in str(log) or "googleads" in str(log)
    ]
    assert len(ad_requests) == 0, "仍有广告网络请求"

    driver.quit()

10.4.2 VPN/代理扩展

def test_with_vpn_extension():
    options = Options()
    options.add_extension("/path/to/vpn-extension.crx")
    options.add_argument("--headless=new")

    driver = webdriver.Chrome(options=options)

    # 等待 VPN 连接
    time.sleep(5)

    # 验证 IP 已改变
    driver.get("https://api.ipify.org?format=json")
    ip_data = json.loads(driver.find_element(By.TAG_NAME, "body").text)
    print(f"当前 IP: {ip_data['ip']}")

    driver.quit()

10.4.3 密码管理器扩展

def test_with_password_manager():
    options = Options()
    options.add_argument("--load-extension=/path/to/password-manager")
    options.add_argument("--headless=new")
    # 加载已有的用户配置 (包含已保存的密码)
    options.add_argument("--user-data-dir=/path/to/profile")

    driver = webdriver.Chrome(options=options)
    driver.get("https://example.com/login")

    # 密码管理器可能自动填充
    time.sleep(3)
    username = driver.find_element(By.ID, "username")
    if username.get_attribute("value"):
        print("密码管理器已自动填充用户名")

    driver.quit()

10.4.4 自定义扩展测试

def test_custom_extension():
    """测试自定义开发的扩展"""
    options = Options()
    options.add_argument("--load-extension=./my-extension")
    options.add_argument("--disable-extensions-except=./my-extension")
    options.add_argument("--headless=new")

    driver = webdriver.Chrome(options=options)
    driver.get("https://target-website.com")

    # 验证扩展 Content Script 注入
    result = driver.execute_script(
        "return document.querySelector('#my-extension-injected-element') !== null"
    )
    assert result, "扩展未正确注入 Content Script"

    # 验证扩展修改了页面样式
    bg_color = driver.execute_script(
        "return window.getComputedStyle(document.body).backgroundColor"
    )
    assert bg_color == "rgb(255, 255, 255)", f"扩展未正确设置背景色: {bg_color}"

    driver.quit()

10.5 扩展开发与调试

连接到扩展的 Background Script

# 启动 Chrome 并开启远程调试
options = Options()
options.add_argument("--load-extension=./my-extension")
options.add_argument("--remote-debugging-port=9222")
options.add_argument("--headless=new")

driver = webdriver.Chrome(options=options)

# 通过 CDP 连接到扩展的 background context
# 在 DevTools 中访问 chrome://inspect 可查看扩展

调试 Content Script

// 在 content.js 中添加调试日志
console.log('[MyExtension] Content script loaded on:', window.location.href);

// 使用 debugger 语句 (DevTools 打开时会暂停)
// debugger;

// 监听来自 background 的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('[MyExtension] Received message:', message);
  if (message.action === 'getData') {
    sendResponse({ data: document.title });
  }
});

使用 Selenium 测试扩展通信

def test_extension_messaging():
    """测试扩展内部通信"""
    options = Options()
    options.add_argument("--load-extension=./my-extension")
    options.add_argument("--headless=new")

    driver = webdriver.Chrome(options=options)
    driver.get("https://example.com")

    # 通过执行脚本触发扩展逻辑
    result = driver.execute_script("""
        return new Promise((resolve) => {
            chrome.runtime.sendMessage(
                { action: 'getStatus' },
                (response) => resolve(response)
            );
        });
    """)
    assert result["active"] == True

    driver.quit()

10.6 管理扩展权限

常用权限

权限 说明 风险等级
activeTab 访问当前活动标签
storage 使用 chrome.storage API
tabs 访问标签页信息
cookies 读写 Cookies
webRequest 拦截/修改网络请求
scripting 注入脚本
notifications 显示桌面通知
<all_urls> 访问所有网站 最高

安全测试清单

def audit_extension_permissions(extension_path):
    """审计扩展权限"""
    manifest_path = f"{extension_path}/manifest.json"
    with open(manifest_path) as f:
        manifest = json.load(f)

    permissions = manifest.get("permissions", [])
    host_permissions = manifest.get("host_permissions", [])

    high_risk = ["cookies", "webRequest", "webRequestBlocking", "<all_urls>"]

    print("=== 扩展权限审计 ===")
    print(f"扩展名: {manifest.get('name')}")
    print(f"版本: {manifest.get('version')}")
    print(f"Manifest 版本: {manifest.get('manifest_version')}")
    print(f"\n权限 ({len(permissions)}):")
    for p in permissions:
        risk = "⚠️ 高" if p in high_risk else "✅ 低"
        print(f"  {risk} - {p}")
    print(f"\n主机权限 ({len(host_permissions)}):")
    for h in host_permissions:
        print(f"  🔗 {h}")

    return permissions, host_permissions

10.7 扩展性能测试

def test_extension_performance():
    """测试扩展对页面性能的影响"""
    # 无扩展基准测试
    options_no_ext = Options()
    options_no_ext.add_argument("--headless=new")
    driver = webdriver.Chrome(options=options_no_ext)
    driver.get("https://example.com")
    baseline = driver.execute_script("""
        const perf = performance.getEntriesByType('navigation')[0];
        return {
            domContentLoaded: perf.domContentLoadedEventEnd - perf.startTime,
            loadComplete: perf.loadEventEnd - perf.startTime,
        };
    """)
    driver.quit()

    # 有扩展对比测试
    options_with_ext = Options()
    options_with_ext.add_argument("--load-extension=./my-extension")
    options_with_ext.add_argument("--headless=new")
    driver = webdriver.Chrome(options_with_ext)
    driver.get("https://example.com")
    with_ext = driver.execute_script("""
        const perf = performance.getEntriesByType('navigation')[0];
        return {
            domContentLoaded: perf.domContentLoadedEventEnd - perf.startTime,
            loadComplete: perf.loadEventEnd - perf.startTime,
        };
    """)
    driver.quit()

    # 比较
    dom_overhead = with_ext["domContentLoaded"] - baseline["domContentLoaded"]
    load_overhead = with_ext["loadComplete"] - baseline["loadComplete"]

    print(f"DOM ContentLoaded 开销: {dom_overhead:.0f}ms")
    print(f"页面完全加载开销: {load_overhead:.0f}ms")

    # 断言扩展对性能的影响不超过 500ms
    assert load_overhead < 500, f"扩展性能开销过大: {load_overhead:.0f}ms"

10.8 要点回顾

要点 说明
MV3 是标准 新扩展应使用 Manifest V3
--load-extension 加载开发扩展 不需要打包为 CRX
--headless=new 支持扩展 旧版无头不支持
需要 launchPersistentContext Playwright 加载扩展需要持久化上下文
权限审计很重要 高权限扩展可能带来安全风险
性能影响需测量 扩展会增加页面加载时间

10.9 注意事项

⚠️ MV2 淘汰: Chrome 正在逐步淘汰 Manifest V2,新扩展开发必须使用 MV3。

⚠️ 有头模式要求: 某些扩展功能(如弹出窗口、通知)在无头模式下可能不可用,调试时使用有头模式。

⚠️ 扩展 ID 不固定: 解压扩展的 ID 在每次加载时可能变化,开发阶段使用 --load-extension + --disable-extensions-except 确保 ID 稳定。

⚠️ CDP 与扩展共存: 使用 CDP 命令时注意不要与扩展的 Content Script 产生冲突。


10.10 扩展阅读

资源 链接
Chrome 扩展开发文档 https://developer.chrome.com/docs/extensions/
Manifest V3 迁移指南 https://developer.chrome.com/docs/extensions/migrating/
Playwright 扩展测试 https://playwright.dev/docs/chrome-extensions
Selenium 扩展加载 https://www.selenium.dev/documentation/webdriver/browsers/chrome/
Chrome Extensions API https://developer.chrome.com/docs/extensions/reference/