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.webdriver | JS 覆盖为 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 reachable | Chrome 崩溃或被杀 | 检查内存、增加 /dev/shm |
SessionNotCreatedException | 版本不匹配 | 更新 ChromeDriver |
ElementClickInterceptedException | 元素被遮挡 | 滚动到元素或关闭遮挡层 |
TimeoutException | 页面加载慢或元素不存在 | 增加超时或检查定位器 |
StaleElementReferenceException | DOM 刷新 | 重新定位元素 |
| 测试本地通过 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 扩展阅读