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

Chromium / ChromeDriver 完全指南 / 12 - 最佳实践

12 - 最佳实践

综合前 11 章内容,提炼浏览器自动化在稳定性、性能、反检测、错误处理、维护等方面的关键最佳实践。


12.1 稳定性

12.1.1 元素等待策略

策略推荐度说明
自动等待 (Playwright)⭐⭐⭐⭐⭐最稳定,无需手动处理
显式等待 (Selenium)⭐⭐⭐⭐需要为每个关键操作添加等待
隐式等待 (Selenium)⭐⭐粒度粗,不推荐混用
time.sleep()最不稳定,仅用于调试
# ✅ 推荐: 显式等待 + 精确条件
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

element = WebDriverWait(driver, 15).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, ".submit-btn"))
)

# ❌ 避免: 硬编码等待
import time
time.sleep(3)
// ✅ 推荐: Playwright 自动等待
await page.click('#submit-btn');

// ⚠️ 仅在特殊场景使用
await page.waitForSelector('#content', { state: 'visible', timeout: 15000 });

12.1.2 StaleElementReferenceException 处理

from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver.support.ui import WebDriverWait

def safe_click(driver, locator, max_retries=3):
    """安全点击,自动重试 stale element"""
    for attempt in range(max_retries):
        try:
            element = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable(locator)
            )
            element.click()
            return True
        except StaleElementReferenceException:
            if attempt == max_retries - 1:
                raise
    return False

12.1.3 页面加载可靠性

def wait_for_page_ready(driver, timeout=30):
    """等待页面完全就绪"""
    # 等待 DOM 加载完成
    WebDriverWait(driver, timeout).until(
        lambda d: d.execute_script("return document.readyState") == "complete"
    )
    # 等待 jQuery AJAX 完成
    try:
        WebDriverWait(driver, timeout).until(
            lambda d: d.execute_script(
                "return (typeof jQuery === 'undefined') || jQuery.active === 0"
            )
        )
    except:
        pass
    # 等待加载动画消失
    try:
        WebDriverWait(driver, 5).until(
            EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading, .spinner"))
        )
    except:
        pass

12.1.4 浏览器实例管理

# ✅ 使用 context manager 确保浏览器关闭
from contextlib import contextmanager

@contextmanager
def managed_chrome(options=None):
    driver = webdriver.Chrome(options=options)
    try:
        yield driver
    finally:
        driver.quit()

# 使用
with managed_chrome(options) as driver:
    driver.get("https://example.com")
    # driver.quit() 会在退出时自动调用
# ✅ 使用 fixture (pytest)
@pytest.fixture
def driver():
    d = webdriver.Chrome(options=chrome_options())
    d.implicitly_wait(5)
    yield d
    d.quit()  # 测试结束后自动清理

12.2 性能优化

12.2.1 资源加载优化

# 禁用不必要资源 (Selenium + CDP)
def optimize_resource_loading(driver):
    driver.execute_cdp_cmd("Network.enable", {})
    driver.execute_cdp_cmd("Network.setBlockedURLs", {
        "urls": [
            "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp",
            "*.woff", "*.woff2", "*.ttf", "*.eot",
            "*google-analytics.com*", "*doubleclick.net*",
            "*facebook.com/tr*", "*hotjar.com*",
        ]
    })
// Playwright 路由拦截
await page.route('**/*.{png,jpg,jpeg,gif,webp,svg,woff,woff2}', route => route.abort());
await page.route('**/google-analytics.com/**', route => route.abort());
await page.route('**/doubleclick.net/**', route => route.abort());

12.2.2 页面加载策略

# Selenium — 使用 eager 策略
options = Options()
options.page_load_strategy = 'eager'  # DOM 就绪即可操作

# 页面加载超时
driver.set_page_load_timeout(30)     # 页面加载超时 30 秒
driver.set_script_timeout(15)        # JS 执行超时 15 秒

12.2.3 浏览器实例复用

# ✅ 复用浏览器上下文 (避免重复启动)
class BrowserPool:
    def __init__(self, max_instances=3):
        self.pool = []
        self.max_instances = max_instances
        self.lock = threading.Lock()

    def get_driver(self):
        with self.lock:
            if self.pool:
                return self.pool.pop()
            if len(self.pool) < self.max_instances:
                return self._create_driver()
        return None

    def release_driver(self, driver):
        driver.delete_all_cookies()
        with self.lock:
            self.pool.append(driver)

    def _create_driver(self):
        options = Options()
        options.add_argument("--headless=new")
        options.add_argument("--no-sandbox")
        return webdriver.Chrome(options=options)

    def shutdown(self):
        for driver in self.pool:
            driver.quit()
        self.pool.clear()

12.2.4 性能基准

优化措施效果说明
禁用图片-30-60% 加载时间爬虫场景推荐
禁用 CSS/字体-10-30% 加载时间爬虫可考虑
eager 加载策略-20-40% 等待时间不等图片加载
浏览器复用-80% 启动时间避免重复启动
并发实例N 倍吞吐量受限于 CPU/内存
Headless 模式-30-50% 内存服务器环境必须

12.3 反检测

12.3.1 基础反检测清单

def apply_stealth_measures(driver):
    """应用基础反检测措施"""

    # 1. 移除 webdriver 标志
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            });
        """
    })

    # 2. 伪造插件
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            Object.defineProperty(navigator, 'plugins', {
                get: () => [1, 2, 3, 4, 5]
            });
        """
    })

    # 3. 伪造语言
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            Object.defineProperty(navigator, 'languages', {
                get: () => ['zh-CN', 'zh', 'en-US', 'en']
            });
        """
    })

    # 4. 伪造 chrome 对象
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            window.chrome = {
                runtime: {},
                loadTimes: function() {},
                csi: function() {},
                app: {}
            };
        """
    })

    # 5. 修改 permissions API
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            const originalQuery = window.navigator.permissions.query;
            window.navigator.permissions.query = (parameters) => (
                parameters.name === 'notifications'
                    ? Promise.resolve({ state: Notification.permission })
                    : originalQuery(parameters)
            );
        """
    })

12.3.2 高级反检测

检测手段应对方法
UserAgent 分析使用真实浏览器 UA,--headless=new 已修复
navigator.webdriverJS 覆盖为 undefined
Canvas 指纹--headless=new 与有头模式一致
WebGL 渲染器修改 WEBGL_debug_renderer_info
TLS/JA3 指纹使用 curl_cffi 或代理
行为分析模拟人类行为(随机延迟、鼠标轨迹)
IP 信誉使用住宅代理
Cookie 检查保持真实 Cookie

12.3.3 人类行为模拟

import random
import time

def human_type(element, text, min_delay=50, max_delay=150):
    """模拟人类输入"""
    for char in text:
        element.send_keys(char)
        time.sleep(random.uniform(min_delay, max_delay) / 1000)

def human_scroll(driver, distance, steps=None):
    """模拟人类滚动"""
    if steps is None:
        steps = random.randint(3, 8)
    step_distance = distance // steps

    for i in range(steps):
        driver.execute_script(f"window.scrollBy(0, {step_distance})")
        time.sleep(random.uniform(0.1, 0.3))

def human_mouse_move(actions, target_element):
    """模拟人类鼠标移动 (贝塞尔曲线)"""
    # 简化版本:分段移动
    current = actions._driver.execute_script(
        "return {x: 0, y: 0}"
    )
    for i in range(random.randint(3, 6)):
        offset_x = random.randint(-50, 50)
        offset_y = random.randint(-50, 50)
        actions.move_by_offset(offset_x, offset_y)
        actions.pause(random.uniform(0.05, 0.15))
    actions.move_to_element(target_element)
    actions.pause(random.uniform(0.1, 0.3))
    actions.click()
    actions.perform()

12.4 错误处理

12.4.1 全局异常处理

from selenium.common.exceptions import (
    WebDriverException,
    TimeoutException,
    NoSuchElementException,
    StaleElementReferenceException,
    ElementClickInterceptedException,
    ElementNotInteractableException,
    SessionNotCreatedException,
)

def safe_execute(driver, func, default=None, retries=3):
    """安全执行操作,带重试和异常处理"""
    for attempt in range(retries):
        try:
            return func(driver)
        except TimeoutException:
            print(f"超时 (尝试 {attempt + 1}/{retries})")
            driver.save_screenshot(f"/tmp/timeout_{attempt}.png")
        except NoSuchElementException:
            print(f"元素未找到 (尝试 {attempt + 1}/{retries})")
        except StaleElementReferenceException:
            print(f"元素已过期 (尝试 {attempt + 1}/{retries})")
        except ElementClickInterceptedException:
            print(f"元素被遮挡 (尝试 {attempt + 1}/{retries})")
            # 尝试关闭遮挡层
            try:
                driver.execute_script("""
                    document.querySelectorAll('.modal-backdrop, .overlay')
                        .forEach(el => el.remove());
                """)
            except:
                pass
        except WebDriverException as e:
            print(f"WebDriver 错误: {e}")
            driver.save_screenshot(f"/tmp/error_{attempt}.png")
        except Exception as e:
            print(f"未知错误: {e}")
            break

        if attempt < retries - 1:
            time.sleep(2 ** attempt)  # 指数退避

    return default

12.4.2 Playwright 错误处理

import { test, expect } from '@playwright/test';

test('带错误处理的测试', async ({ page }) => {
  // 全局超时设置
  page.setDefaultTimeout(15000);

  try {
    await page.goto('https://example.com');

    // 操作带自定义超时
    await page.click('#button', { timeout: 10000 });

  } catch (error) {
    // 截图保存现场
    await page.screenshot({ path: 'error-screenshot.png' });

    // 记录页面 URL 和内容
    console.log('失败 URL:', page.url());
    console.log('错误:', error.message);

    throw error; // 重新抛出
  }
});

12.4.3 截图与报告

import datetime

class TestContext:
    def __init__(self, driver, output_dir="/tmp/test-screenshots"):
        self.driver = driver
        self.output_dir = output_dir
        os.makedirs(output_dir, exist_ok=True)

    def take_screenshot(self, name=None):
        if name is None:
            name = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        path = f"{self.output_dir}/{name}.png"
        self.driver.save_screenshot(path)
        return path

    def save_page_source(self, name=None):
        if name is None:
            name = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        path = f"{self.output_dir}/{name}.html"
        with open(path, "w", encoding="utf-8") as f:
            f.write(self.driver.page_source)
        return path

    def log_error(self, error, context=""):
        timestamp = datetime.datetime.now().isoformat()
        screenshot = self.take_screenshot()
        source = self.save_page_source()

        log_entry = {
            "timestamp": timestamp,
            "error": str(error),
            "context": context,
            "url": self.driver.current_url,
            "screenshot": screenshot,
            "source": source,
        }

        with open(f"{self.output_dir}/error_log.json", "a") as f:
            f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")

        return log_entry

12.5 维护与代码组织

12.5.1 项目结构

automation-project/
├── tests/                    # 测试用例
│   ├── test_login.py
│   ├── test_search.py
│   └── test_checkout.py
├── pages/                    # Page Objects
│   ├── base_page.py
│   ├── login_page.py
│   ├── search_page.py
│   └── checkout_page.py
├── utils/                    # 工具类
│   ├── driver_factory.py
│   ├── wait_helpers.py
│   ├── screenshot.py
│   └── data_loader.py
├── data/                     # 测试数据
│   ├── users.json
│   └── products.json
├── config/                   # 配置
│   ├── settings.py
│   └── environments.yml
├── reports/                  # 测试报告
├── conftest.py               # pytest fixtures
├── pytest.ini               # pytest 配置
├── requirements.txt
└── README.md

12.5.2 配置管理

# config/settings.py
import os
from dataclasses import dataclass

@dataclass
class BrowserConfig:
    name: str = "chrome"
    headless: bool = True
    window_width: int = 1920
    window_height: int = 1080
    timeout: int = 15
    implicit_wait: int = 0

@dataclass
class EnvironmentConfig:
    base_url: str = os.getenv("BASE_URL", "http://localhost:3000")
    selenium_hub: str = os.getenv("SELENIUM_HUB", "")
    browser: BrowserConfig = BrowserConfig()

    @classmethod
    def for_ci(cls):
        return cls(
            base_url=os.getenv("CI_BASE_URL", "http://web-app:3000"),
            selenium_hub="http://selenium-hub:4444/wd/hub",
            browser=BrowserConfig(headless=True)
        )

    @classmethod
    def for_dev(cls):
        return cls(
            base_url="http://localhost:3000",
            browser=BrowserConfig(headless=False, timeout=30)
        )

12.5.3 Driver 工厂

# utils/driver_factory.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

class DriverFactory:
    @staticmethod
    def create(config):
        options = Options()

        if config.browser.headless:
            options.add_argument("--headless=new")

        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        options.add_argument("--disable-gpu")
        options.add_argument(
            f"--window-size={config.browser.window_width},{config.browser.window_height}"
        )
        options.add_argument("--disable-blink-features=AutomationControlled")

        if config.selenium_hub:
            driver = webdriver.Remote(
                command_executor=config.selenium_hub,
                options=options
            )
        else:
            service = Service(ChromeDriverManager().install())
            driver = webdriver.Chrome(service=service, options=options)

        driver.set_page_load_timeout(config.browser.timeout)
        return driver

12.6 安全实践

实践说明
不在代码中存储凭据使用环境变量或密钥管理服务
使用 HTTPS自动化脚本也应使用安全连接
避免 --ignore-certificate-errors生产环境不要禁用证书验证
最小权限原则浏览器只授予必要的权限
定期更新保持浏览器、驱动、库版本最新
清理敏感数据测试结束后清除 cookies 和本地存储
隔离网络测试环境与生产环境网络隔离

12.7 监控与可观测性

12.7.1 关键指标

指标说明警戒值
测试成功率通过/总测试数< 95% 需关注
平均执行时间单测试平均耗时> 30s 需优化
Flaky 率非确定性失败比例> 5% 需处理
浏览器崩溃率浏览器进程异常退出> 1% 需排查
资源使用率CPU/内存占用> 80% 需扩容

12.7.2 测试报告

# 集成 Allure 报告 (pytest)
import allure

@allure.feature("用户登录")
@allure.story("正常登录")
def test_login_success(driver):
    with allure.step("打开登录页"):
        driver.get(f"{base_url}/login")
        allure.attach(driver.get_screenshot_as_png(), name="登录页", attachment_type=allure.attachment_type.PNG)

    with allure.step("输入凭据"):
        # ...
        pass

    with allure.step("验证登录成功"):
        assert "/dashboard" in driver.current_url
# 安装 Allure
pip install allure-pytest

# 运行测试并生成报告
pytest tests/ --alluredir=allure-results

# 查看报告
allure serve allure-results

12.8 技术栈选型决策树

开始 → 需要跨浏览器测试吗?
  ├─ 是 → 团队主要使用什么语言?
  │   ├─ JavaScript/TypeScript → Playwright
  │   ├─ Python → Playwright 或 Selenium
  │   ├─ Java → Selenium 或 Playwright
  │   └─ C# → Playwright 或 Selenium
  └─ 否 → 只需要 Chrome?
      ├─ 是 → 需要 CDP 底层能力吗?
      │   ├─ 是 (请求拦截/Trace/PDF) → Puppeteer
      │   └─ 否 → 团队语言?
      │       ├─ JS/TS → Puppeteer 或 Playwright
      │       └─ 其他 → Selenium 或 Playwright
      └─ 否 → 已有 Selenium 项目?
          ├─ 是 → 继续使用 Selenium
          └─ 否 → 新项目选择 Playwright

12.9 常见问题速查表

问题原因解决方案
Chrome not reachableChrome 崩溃或被杀检查内存、增加 /dev/shm
SessionNotCreatedException版本不匹配更新 ChromeDriver
ElementClickInterceptedException元素被遮挡滚动到元素或关闭遮挡层
TimeoutException页面加载慢或元素不存在增加超时或检查定位器
StaleElementReferenceExceptionDOM 刷新重新定位元素
测试本地通过 CI 失败环境差异锁定版本、使用 Docker
截图是空白无头模式渲染问题使用 --headless=new
PDF 中文乱码缺少字体安装 fonts-noto-cjk
扩展不加载无头模式不支持使用 --headless=new
内存泄漏浏览器实例未关闭使用 context manager

12.10 要点回顾

要点说明
稳定性优先使用自动等待、显式等待,避免 sleep
性能需要度量禁用不必要资源、复用浏览器实例
反检测持续对抗技术不断演进,需持续关注
错误处理要完善截图保存现场、重试机制、日志记录
代码组织清晰POM 模式、配置分离、数据驱动
技术栈选择根据团队、场景、浏览器需求综合考虑
持续维护定期更新依赖、清理 flaky tests

12.11 注意事项

⚠️ 技术选型不要追新: 根据团队实际情况选择技术栈,已有 Selenium 项目不必强迁 Playwright。

E2E 测试不是银弹: E2E 测试维护成本高,应与单元测试、集成测试配合使用。

⚠️ 依赖版本锁定: 生产环境中的 Chrome、ChromeDriver、Selenium/Playwright 版本都应锁定。

⚠️ 持续关注安全: Chrome 和 ChromeDriver 定期发布安全更新,及时跟进。


12.12 扩展阅读

资源链接
Selenium 最佳实践https://www.selenium.dev/documentation/test_practices/
Playwright 最佳实践https://playwright.dev/docs/best-practices
Google Testing Bloghttps://testing.googleblog.com/
Martin Fowler - E2E Testinghttps://martinfowler.com/bliki/EndToEndTest.html
Test Automation Patternshttps://testautomationpatterns.org/
Chrome Release Noteshttps://chromereleases.googleblog.com/
WebDriver BiDihttps://w3c.github.io/webdriver-bidi/