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

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 支持扩展旧版无头不支持
需要 launchPersistentContextPlaywright 加载扩展需要持久化上下文
权限审计很重要高权限扩展可能带来安全风险
性能影响需测量扩展会增加页面加载时间

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 APIhttps://developer.chrome.com/docs/extensions/reference/