Python Selenium 4 实战——现代化使用姿势与 Playwright 对比选型
Python Selenium 4 实战——现代化使用姿势与 Playwright 对比选型
适读人群:仍在使用 Selenium 的开发者、想做技术选型的工程师 | 阅读时长:约15分钟 | 核心价值:吃透 Selenium 4 新特性,做出务实的工具选型
那个坚持用 Selenium 的同事
我有个朋友叫老吴,在一家保险公司做自动化测试,他们的测试套件有将近800个用例,全是 Selenium 写的,跑了快五年了。
前年 Playwright 火起来之后,他们团队里有个刚毕业的新人小李,每天跟他说要把测试框架迁移到 Playwright,说 Selenium 太老了,以后会被淘汰。老吴跟我说这件事,语气有点无奈:"他说得有道理,但我们800个用例迁移成本太高了,而且 Selenium 4 发布之后其实改进了很多,他说的那些问题很多都已经解决了。"
这让我思考一个问题:在 Playwright 普及的今天,Selenium 4 还值得学吗?老项目要不要迁移?
我的答案是:Selenium 4 的新特性值得认真掌握,老项目不必强行迁移,新项目优先选 Playwright。 但关键在于,你要真的懂 Selenium 4 到底新在哪里,不然就只是用新版本跑着老写法,白白浪费了升级的价值。
一、Selenium 4 的核心新特性
Selenium 4 相较 Selenium 3 有几个重要升级,如果你还在用老写法,你等于在开新车走老路。
1. WebDriver Manager——告别手动管理 Driver
这是 Selenium 4 最受欢迎的改进之一。
pip install selenium webdriver-manager# 老写法(Selenium 3 时代):手动下载 chromedriver,指定路径
from selenium import webdriver
driver = webdriver.Chrome(executable_path="/path/to/chromedriver")
# Selenium 4 新写法:自动管理,永不为版本不匹配发愁
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)2. 相对定位器——更语义化的元素定位
from selenium.webdriver.common.by import By
from selenium.webdriver.support.relative_locator import locate_with
# 找到"密码"标签旁边的输入框
password_label = driver.find_element(By.XPATH, "//label[text()='密码']")
password_input = driver.find_element(
locate_with(By.TAG_NAME, "input").to_right_of(password_label)
)
# 找到提交按钮上方的复选框
submit_btn = driver.find_element(By.ID, "submit")
checkbox = driver.find_element(
locate_with(By.TAG_NAME, "input").above(submit_btn)
)3. Chrome DevTools Protocol (CDP) 直接访问
Selenium 4 直接支持 CDP,这让它获得了很多 Playwright 才有的能力:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def create_stealth_driver():
"""创建反检测 Chrome Driver"""
options = Options()
options.add_argument("--headless=new") # 新版无头模式
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=options
)
# 通过 CDP 注入反检测脚本
driver.execute_cdp_cmd(
"Page.addScriptToEvaluateOnNewDocument",
{
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3]
});
"""
}
)
return driver二、Selenium 4 完整实战示例
下面是一个工程化的登录爬取示例,包含等待、异常处理、截图记录:
import time
import logging
from pathlib import Path
from contextlib import contextmanager
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import (
TimeoutException,
NoSuchElementException,
StaleElementReferenceException,
)
from webdriver_manager.chrome import ChromeDriverManager
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
SCREENSHOT_DIR = Path("screenshots")
SCREENSHOT_DIR.mkdir(exist_ok=True)
@contextmanager
def create_driver(headless: bool = True):
"""Driver 上下文管理器,确保资源释放"""
options = Options()
if headless:
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--window-size=1920,1080")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=options,
)
driver.implicitly_wait(0) # 关闭隐式等待,用显式等待替代
try:
yield driver
finally:
driver.quit()
def safe_find(driver, by, value, timeout=10):
"""安全查找元素,自动等待"""
try:
return WebDriverWait(driver, timeout).until(
EC.presence_of_element_located((by, value))
)
except TimeoutException:
logger.warning(f"元素未找到: {by}={value}")
return None
def safe_click(driver, by, value, timeout=10):
"""安全点击元素,等待可点击"""
try:
element = WebDriverWait(driver, timeout).until(
EC.element_to_be_clickable((by, value))
)
element.click()
return True
except (TimeoutException, StaleElementReferenceException) as e:
logger.warning(f"点击失败: {e}")
return False
class WebCrawler:
def __init__(self, base_url: str):
self.base_url = base_url
def login(self, driver, username: str, password: str) -> bool:
"""执行登录"""
try:
driver.get(f"{self.base_url}/login")
# 等待登录表单出现
username_input = safe_find(driver, By.ID, "username")
if not username_input:
driver.save_screenshot(str(SCREENSHOT_DIR / "login_fail.png"))
return False
username_input.clear()
username_input.send_keys(username)
password_input = safe_find(driver, By.ID, "password")
password_input.clear()
password_input.send_keys(password)
safe_click(driver, By.ID, "login-btn")
# 等待跳转到首页
WebDriverWait(driver, 15).until(
EC.url_contains("/dashboard")
)
logger.info("登录成功")
return True
except TimeoutException:
driver.save_screenshot(str(SCREENSHOT_DIR / "login_timeout.png"))
logger.error("登录超时")
return False
def crawl_list(self, driver, max_pages: int = 10) -> list[dict]:
"""爬取列表页"""
results = []
page = 1
while page <= max_pages:
try:
driver.get(f"{self.base_url}/list?page={page}")
# 等待列表加载
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, ".list-item")
)
)
items = driver.find_elements(By.CSS_SELECTOR, ".list-item")
if not items:
logger.info(f"第{page}页无数据,停止爬取")
break
for item in items:
try:
results.append({
"title": item.find_element(
By.CSS_SELECTOR, ".item-title"
).text,
"date": item.find_element(
By.CSS_SELECTOR, ".item-date"
).text,
})
except NoSuchElementException:
continue
logger.info(f"第{page}页: 获取{len(items)}条")
page += 1
# 随机延迟
time.sleep(1.5 + (page % 3) * 0.5)
except TimeoutException:
logger.warning(f"第{page}页加载超时,截图记录")
driver.save_screenshot(
str(SCREENSHOT_DIR / f"timeout_page{page}.png")
)
break
return results
# 使用示例
def main():
crawler = WebCrawler("https://example.com")
with create_driver(headless=True) as driver:
if crawler.login(driver, "username", "password"):
data = crawler.crawl_list(driver, max_pages=5)
print(f"共爬取 {len(data)} 条数据")
if __name__ == "__main__":
main()三、踩坑实录
踩坑实录1:StaleElementReferenceException 频繁出现
现象:爬取列表时,偶发 StaleElementReferenceException,脚本崩溃。
原因:页面在 find 和 use 之间发生了重新渲染(比如异步更新),元素引用失效。
解法:不缓存元素引用,每次使用前重新查找;或者用 try/except 重试。
from selenium.common.exceptions import StaleElementReferenceException
def retry_on_stale(func, retries=3):
"""遇到 StaleElementReferenceException 自动重试"""
for attempt in range(retries):
try:
return func()
except StaleElementReferenceException:
if attempt == retries - 1:
raise
time.sleep(0.5)踩坑实录2:implicitlyWait 和 WebDriverWait 混用导致等待时间翻倍
现象:明明只设了5秒等待,实际要等10秒才超时。
原因:同时开启了隐式等待(3秒)和显式等待(5秒),两者叠加,行为不可预期。
解法:只用显式等待,彻底关闭隐式等待(driver.implicitly_wait(0))。
踩坑实录3:headless 模式下某些元素点击失效
现象:有头模式能正常点击,headless 模式失败。
原因:默认 headless 窗口尺寸较小(800×600),某些元素被遮挡。
解法:设置 --window-size=1920,1080,或改用 --headless=new(Selenium 4.8+ 支持的新 headless 模式,行为更接近有头模式)。
四、Selenium 4 vs Playwright 选型指南
| 考量因素 | 选 Selenium 4 | 选 Playwright |
|---|---|---|
| 现有代码库 | 大量 Selenium 代码 | 新项目 |
| 团队背景 | 测试团队,熟悉 Selenium | 没有历史包袱 |
| 语言 | Java/C#/Python 均需要 | 主要 Python/Node.js |
| 稳定性要求 | 成熟稳定,坑都已知 | 新框架,部分边缘情况还在完善 |
| 网络拦截 | 有限支持(通过 CDP) | 原生支持,更强大 |
| 异步支持 | 较弱 | 原生 async |
| 文档质量 | 非常完善(多年积累) | 优秀且更新快 |
我的建议是:不要为了新而新,也不要守旧而不升级。 老吴的团队最终的决策很务实——Selenium 4 新特性全面应用到存量用例里,新的功能开发全部用 Playwright,两套共存,互不干扰。这比强行全量迁移理智得多。
