Selenium、playwright
目录
一、Selenium 简介二、环境搭建三、元素定位(核心)四、元素常用操作五、等待机制(重要)六、弹窗处理七、窗口切换八、iframe 处理九、爬取动态加载数据(重点)十、其他高级功能十一、实战案例十二、面试题十三、Playwright 详解十四、Playwright 核心功能十五、Playwright 与 Selenium 对比十六、Playwright 实战案例十七、Playwright 面试题
一、Selenium 简介
1.1 什么是 Selenium?
Selenium 是一个用于 Web 应用程序自动化测试的开源工具集。它支持多种浏览器(Chrome、Firefox、Safari、Edge 等)和多种编程语言(Python、Java、C#、JavaScript 等)。
1.2 Selenium 的组成
Selenium WebDriver:核心组件,用于控制浏览器Selenium Grid:分布式测试,可以在多台机器上并行执行测试Selenium IDE:浏览器插件,用于录制和回放测试脚本
1.3 Selenium 的优势
跨浏览器支持:支持主流浏览器跨平台支持:Windows、Linux、macOS多语言支持:Python、Java、C#、JavaScript 等开源免费:完全免费,社区活跃功能强大:支持复杂的 Web 应用自动化
1.4 Selenium 的应用场景
Web 自动化测试:功能测试、回归测试Web 爬虫:爬取动态加载的数据UI 自动化:重复性操作自动化数据采集:批量数据采集和处理
二、环境搭建
2.1 Python 环境安装
# 安装 Selenium
pip install selenium
# 安装其他常用库
pip install webdriver-manager # 自动管理浏览器驱动
pip install beautifulsoup4 # HTML 解析
pip install pandas # 数据处理
2.2 浏览器驱动安装
方式一:手动下载驱动(推荐用于生产环境)
Chrome 驱动:
查看 Chrome 版本:下载对应版本的 ChromeDriver:https://chromedriver.chromium.org/将驱动放到 PATH 环境变量中
chrome://version/
Firefox 驱动(GeckoDriver):
下载 GeckoDriver:https://github.com/mozilla/geckodriver/releases将驱动放到 PATH 环境变量中
方式二:使用 webdriver-manager(推荐用于开发环境)
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
# 自动下载和管理 Chrome 驱动
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
2.3 第一个 Selenium 脚本
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)
# 打开网页
driver.get("https://www.baidu.com")
# 打印页面标题
print(driver.title)
# 关闭浏览器
driver.quit()
三、元素定位(核心)
元素定位是 Selenium 的核心,只有准确定位到元素,才能进行操作。
3.1 8 种定位方式
3.1.1 ID 定位(最优先使用)
特点:ID 在页面中唯一,定位最快最稳定
from selenium.webdriver.common.by import By
# 方式一:使用 By.ID
element = driver.find_element(By.ID, "username")
# 方式二:使用 find_element_by_id(已废弃,不推荐)
element = driver.find_element_by_id("username")
HTML 示例:
<input id="username" type="text" placeholder="请输入用户名">
3.1.2 Name 定位
特点:Name 属性可能不唯一,但比 ID 更常用
element = driver.find_element(By.NAME, "username")
HTML 示例:
<input name="username" type="text">
3.1.3 Class Name 定位
特点:Class 通常不唯一,需要谨慎使用
# 定位单个元素
element = driver.find_element(By.CLASS_NAME, "login-btn")
# 定位多个元素(返回列表)
elements = driver.find_elements(By.CLASS_NAME, "btn")
HTML 示例:
<button class="login-btn">登录</button>
3.1.4 Tag Name 定位
特点:通过标签名定位,通常用于定位多个相同标签
# 定位所有 input 标签
inputs = driver.find_elements(By.TAG_NAME, "input")
# 定位所有链接
links = driver.find_elements(By.TAG_NAME, "a")
3.1.5 Link Text 定位(完整链接文本)
特点:用于定位链接,必须完全匹配链接文本
# 定位链接文本为"登录"的链接
link = driver.find_element(By.LINK_TEXT, "登录")
link.click()
HTML 示例:
<a href="/login">登录</a>
3.1.6 Partial Link Text 定位(部分链接文本)
特点:用于定位链接,只需要部分匹配
# 定位包含"登录"的链接
link = driver.find_element(By.PARTIAL_LINK_TEXT, "登录")
3.1.7 XPath 定位(最强大)
特点:功能最强大,可以定位任何元素,但性能相对较慢
绝对路径(不推荐,太脆弱):
element = driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/form/input[1]")
相对路径(推荐):
# 通过属性定位
element = driver.find_element(By.XPATH, "//input[@id='username']")
element = driver.find_element(By.XPATH, "//input[@name='username']")
element = driver.find_element(By.XPATH, "//input[@type='text']")
# 通过文本定位
element = driver.find_element(By.XPATH, "//button[text()='登录']")
element = driver.find_element(By.XPATH, "//a[contains(text(),'登录')]")
# 通过层级关系定位
element = driver.find_element(By.XPATH, "//div[@class='form']//input[@id='username']")
element = driver.find_element(By.XPATH, "//div[@id='parent']/input[1]")
# 通过逻辑运算符定位
element = driver.find_element(By.XPATH, "//input[@id='username' and @type='text']")
element = driver.find_element(By.XPATH, "//input[@id='username' or @name='username']")
# 通过轴定位
element = driver.find_element(By.XPATH, "//input[@id='username']/following-sibling::input")
element = driver.find_element(By.XPATH, "//input[@id='username']/parent::div")
element = driver.find_element(By.XPATH, "//div[@id='parent']/descendant::input")
常用 XPath 函数:
# contains():包含
element = driver.find_element(By.XPATH, "//a[contains(@href,'login')]")
# starts-with():以...开始
element = driver.find_element(By.XPATH, "//input[starts-with(@id,'user')]")
# text():文本内容
element = driver.find_element(By.XPATH, "//button[text()='提交']")
# normalize-space():去除首尾空格
element = driver.find_element(By.XPATH, "//div[normalize-space(text())='登录']")
3.1.8 CSS Selector 定位(性能最好)
特点:性能比 XPath 好,语法简洁,但功能不如 XPath 强大
# 通过 ID 定位
element = driver.find_element(By.CSS_SELECTOR, "#username")
# 通过 Class 定位
element = driver.find_element(By.CSS_SELECTOR, ".login-btn")
# 通过标签定位
element = driver.find_element(By.CSS_SELECTOR, "input")
# 通过属性定位
element = driver.find_element(By.CSS_SELECTOR, "input[name='username']")
element = driver.find_element(By.CSS_SELECTOR, "input[type='text']")
# 通过层级关系定位
element = driver.find_element(By.CSS_SELECTOR, "div.form input#username")
element = driver.find_element(By.CSS_SELECTOR, "div#parent > input")
# 通过伪类定位
element = driver.find_element(By.CSS_SELECTOR, "input:first-child")
element = driver.find_element(By.CSS_SELECTOR, "a:nth-child(2)")
# 通过文本内容定位(CSS 不支持,需要用 XPath)
3.2 定位策略优先级
ID:最优先,唯一且快速Name:次优先,如果 ID 不可用CSS Selector:性能好,语法简洁XPath:功能强大,但性能相对较慢其他:根据实际情况选择
3.3 定位多个元素
# find_elements 返回列表,即使只有一个元素
elements = driver.find_elements(By.CLASS_NAME, "btn")
# 遍历元素
for element in elements:
print(element.text)
# 获取第一个元素
first_element = elements[0]
3.4 定位技巧
技巧 1:使用浏览器开发者工具
右键点击元素 → 检查在 Elements 面板中查看元素的属性右键元素 → Copy → Copy XPath / Copy selector
技巧 2:验证定位表达式
# 在浏览器控制台验证 XPath
# $x("//input[@id='username']")
# 在浏览器控制台验证 CSS Selector
# document.querySelector("#username")
技巧 3:处理动态 ID
# 如果 ID 是动态的,使用其他属性或文本定位
# 错误:driver.find_element(By.ID, "user_123456") # ID 会变化
# 正确:使用稳定的属性
element = driver.find_element(By.XPATH, "//input[@name='username']")
element = driver.find_element(By.XPATH, "//input[contains(@class,'username-input')]")
四、元素常用操作
4.1 输入操作
from selenium.webdriver.common.keys import Keys
# 定位输入框
input_element = driver.find_element(By.ID, "username")
# 清空输入框
input_element.clear()
# 输入文本
input_element.send_keys("testuser")
# 输入特殊键
input_element.send_keys(Keys.ENTER) # 回车
input_element.send_keys(Keys.TAB) # Tab
input_element.send_keys(Keys.BACKSPACE) # 退格
input_element.send_keys(Keys.CONTROL, 'a') # Ctrl+A(全选)
# 组合键
input_element.send_keys(Keys.CONTROL, 'a') # 全选
input_element.send_keys(Keys.CONTROL, 'c') # 复制
input_element.send_keys(Keys.CONTROL, 'v') # 粘贴
4.2 点击操作
# 普通点击
button = driver.find_element(By.ID, "submit-btn")
button.click()
# 双击
from selenium.webdriver.common.action_chains import ActionChains
action = ActionChains(driver)
action.double_click(button).perform()
# 右键点击
action.context_click(button).perform()
# 悬停
action.move_to_element(button).perform()
4.3 获取元素信息
element = driver.find_element(By.ID, "username")
# 获取文本内容
text = element.text
print(f"文本内容:{text}")
# 获取属性值
value = element.get_attribute("value")
id_value = element.get_attribute("id")
class_value = element.get_attribute("class")
href = element.get_attribute("href")
# 获取标签名
tag_name = element.tag_name
# 判断元素是否显示
is_displayed = element.is_displayed()
# 判断元素是否可用
is_enabled = element.is_enabled()
# 判断元素是否被选中(用于复选框、单选框)
is_selected = element.is_selected()
# 获取元素大小和位置
size = element.size
location = element.location
rect = element.rect # 包含 x, y, width, height
4.4 下拉框操作
from selenium.webdriver.support.ui import Select
# 定位下拉框
select_element = driver.find_element(By.ID, "country")
# 创建 Select 对象
select = Select(select_element)
# 通过可见文本选择
select.select_by_visible_text("中国")
# 通过 value 选择
select.select_by_value("CN")
# 通过索引选择(从 0 开始)
select.select_by_index(0)
# 获取所有选项
options = select.options
for option in options:
print(option.text)
# 获取当前选中的选项
selected_option = select.first_selected_option
print(f"当前选中:{selected_option.text}")
# 取消选择(仅适用于多选下拉框)
select.deselect_all()
select.deselect_by_visible_text("中国")
4.5 文件上传
# 方式一:使用 send_keys(推荐)
file_input = driver.find_element(By.ID, "file-upload")
file_input.send_keys("/path/to/file.txt")
# 方式二:使用 AutoIt 或 PyAutoGUI(复杂场景)
# 适用于需要点击"浏览"按钮的场景
4.6 滚动操作
from selenium.webdriver.common.keys import Keys
# 方式一:使用 JavaScript
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # 滚动到底部
driver.execute_script("window.scrollTo(0, 0);") # 滚动到顶部
# 滚动到指定元素
element = driver.find_element(By.ID, "target-element")
driver.execute_script("arguments[0].scrollIntoView();", element)
# 方式二:使用 Keys
body = driver.find_element(By.TAG_NAME, "body")
body.send_keys(Keys.END) # 滚动到底部
body.send_keys(Keys.HOME) # 滚动到顶部
body.send_keys(Keys.PAGE_DOWN) # 向下翻页
body.send_keys(Keys.PAGE_UP) # 向上翻页
4.7 拖拽操作
from selenium.webdriver.common.action_chains import ActionChains
# 定位源元素和目标元素
source = driver.find_element(By.ID, "source")
target = driver.find_element(By.ID, "target")
# 执行拖拽
action = ActionChains(driver)
action.drag_and_drop(source, target).perform()
# 或者指定偏移量拖拽
action.drag_and_drop_by_offset(source, 100, 200).perform()
五、等待机制(重要)
等待机制是 Selenium 中非常重要的概念,用于处理页面加载和元素出现的异步问题。
5.1 为什么需要等待?
问题场景:
页面元素需要时间加载JavaScript 动态生成内容网络延迟导致元素出现慢如果不等待,会抛出
NoSuchElementException
5.2 三种等待方式
5.2.1 强制等待(time.sleep)
特点:固定等待时间,无论元素是否加载完成
import time
# 等待 3 秒
time.sleep(3)
# 缺点:浪费时间,如果元素提前加载完成也要等
# 优点:简单直接
使用场景:调试代码、固定延迟需求
5.2.2 隐式等待(Implicit Wait)
特点:设置全局等待时间,对所有元素查找都生效
# 设置隐式等待 10 秒
driver.implicitly_wait(10)
# 之后所有的 find_element 操作都会等待最多 10 秒
element = driver.find_element(By.ID, "username")
# 如果元素在 10 秒内出现,立即返回
# 如果 10 秒后还没出现,抛出 NoSuchElementException
工作原理:
在设置的时间内,Selenium 会轮询查找元素如果元素找到,立即返回如果超时,抛出异常
优点:设置一次,全局生效
缺点:
只能用于元素查找,不能用于其他条件如果元素不存在,必须等到超时可能影响测试执行速度
5.2.3 显式等待(Explicit Wait)(推荐)
特点:针对特定条件等待,更灵活、更高效
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 创建显式等待对象(最多等待 10 秒,每 0.5 秒检查一次)
wait = WebDriverWait(driver, 10, poll_frequency=0.5)
# 等待元素出现
element = wait.until(EC.presence_of_element_located((By.ID, "username")))
# 等待元素可见
element = wait.until(EC.visibility_of_element_located((By.ID, "username")))
# 等待元素可点击
element = wait.until(EC.element_to_be_clickable((By.ID, "submit-btn")))
# 等待元素消失
wait.until(EC.invisibility_of_element_located((By.ID, "loading")))
# 等待文本出现
wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "成功"))
5.3 常用 Expected Conditions
from selenium.webdriver.support import expected_conditions as EC
# 元素存在(DOM 中存在,不一定可见)
EC.presence_of_element_located((By.ID, "username"))
# 元素可见(存在且可见)
EC.visibility_of_element_located((By.ID, "username"))
# 元素可点击
EC.element_to_be_clickable((By.ID, "submit-btn"))
# 元素不可见
EC.invisibility_of_element_located((By.ID, "loading"))
# 元素被选中
EC.element_to_be_selected((By.ID, "checkbox"))
# 文本出现在元素中
EC.text_to_be_present_in_element((By.ID, "status"), "成功")
# 标题包含文本
EC.title_contains("百度")
# URL 包含文本
EC.url_contains("login")
# 元素数量
EC.number_of_elements_to_be((By.CLASS_NAME, "item"), 10)
# 元素属性包含值
EC.element_attribute_to_include((By.ID, "link"), "href", "login")
# 框架可用并切换
EC.frame_to_be_available_and_switch_to_it((By.ID, "iframe"))
5.4 自定义等待条件
from selenium.webdriver.support.ui import WebDriverWait
# 自定义等待条件
class element_has_text:
def __init__(self, locator, text):
self.locator = locator
self.text = text
def __call__(self, driver):
element = driver.find_element(*self.locator)
if self.text in element.text:
return element
return False
# 使用自定义等待条件
wait = WebDriverWait(driver, 10)
element = wait.until(element_has_text((By.ID, "status"), "成功"))
5.5 等待机制最佳实践
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 1. 设置隐式等待(作为兜底)
driver.implicitly_wait(5)
# 2. 关键操作使用显式等待
wait = WebDriverWait(driver, 10)
# 等待页面加载完成
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
# 等待登录按钮可点击
login_btn = wait.until(EC.element_to_be_clickable((By.ID, "login-btn")))
# 3. 等待 AJAX 请求完成(通过检查加载指示器)
wait.until(EC.invisibility_of_element_located((By.ID, "loading")))
# 4. 等待特定条件满足后再操作
wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "加载完成"))
5.6 常见等待问题解决
问题 1:元素定位超时
# 解决方案:增加等待时间,检查定位表达式
try:
element = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.ID, "username"))
)
except TimeoutException:
print("元素未找到,检查定位表达式或页面是否加载完成")
问题 2:元素存在但不可见
# 解决方案:使用 visibility_of_element_located 而不是 presence_of_element_located
element = wait.until(EC.visibility_of_element_located((By.ID, "username")))
问题 3:元素可点击但点击无效
# 解决方案:滚动到元素位置再点击
element = wait.until(EC.element_to_be_clickable((By.ID, "submit-btn")))
driver.execute_script("arguments[0].scrollIntoView();", element)
element.click()
六、弹窗处理
6.1 Alert 弹窗
特点:浏览器原生弹窗,只有一个”确定”按钮
from selenium.webdriver.common.alert import Alert
# 等待弹窗出现
wait = WebDriverWait(driver, 10)
alert = wait.until(EC.alert_is_present())
# 或者直接获取
alert = driver.switch_to.alert
# 获取弹窗文本
text = alert.text
print(f"弹窗内容:{text}")
# 点击确定
alert.accept()
# 点击取消(如果有)
# alert.dismiss() # Alert 弹窗没有取消按钮,只有 Confirm 和 Prompt 有
6.2 Confirm 弹窗
特点:有”确定”和”取消”两个按钮
# 等待 Confirm 弹窗
alert = wait.until(EC.alert_is_present())
# 获取文本
text = alert.text
# 点击确定
alert.accept()
# 点击取消
alert.dismiss()
6.3 Prompt 弹窗
特点:可以输入文本的弹窗
# 等待 Prompt 弹窗
alert = wait.until(EC.alert_is_present())
# 输入文本
alert.send_keys("输入的内容")
# 点击确定
alert.accept()
# 或者点击取消
alert.dismiss()
6.4 自定义弹窗(Modal)
特点:HTML/CSS/JavaScript 实现的弹窗,不是浏览器原生弹窗
# 定位弹窗元素
modal = driver.find_element(By.ID, "modal")
# 等待弹窗出现
wait.until(EC.visibility_of_element_located((By.ID, "modal")))
# 操作弹窗内的元素
close_btn = modal.find_element(By.CLASS_NAME, "close-btn")
close_btn.click()
# 或者点击遮罩层关闭
overlay = driver.find_element(By.CLASS_NAME, "modal-overlay")
overlay.click()
6.5 弹窗处理最佳实践
def handle_alert(driver, accept=True):
"""处理弹窗的通用方法"""
try:
wait = WebDriverWait(driver, 5)
alert = wait.until(EC.alert_is_present())
text = alert.text
if accept:
alert.accept()
else:
alert.dismiss()
return text
except TimeoutException:
print("没有弹窗出现")
return None
# 使用示例
alert_text = handle_alert(driver, accept=True)
七、窗口切换
7.1 获取窗口句柄
# 获取当前窗口句柄
current_window = driver.current_window_handle
print(f"当前窗口:{current_window}")
# 获取所有窗口句柄
all_windows = driver.window_handles
print(f"所有窗口:{all_windows}")
# 窗口句柄是一个唯一的字符串标识符
7.2 切换窗口
# 方式一:切换到指定窗口
driver.switch_to.window(window_handle)
# 方式二:切换到最后一个窗口(通常是新打开的窗口)
driver.switch_to.window(driver.window_handles[-1])
# 方式三:切换到第一个窗口
driver.switch_to.window(driver.window_handles[0])
# 完整示例
# 1. 记录当前窗口
main_window = driver.current_window_handle
# 2. 点击打开新窗口的链接
link = driver.find_element(By.LINK_TEXT, "新窗口")
link.click()
# 3. 等待新窗口打开
wait = WebDriverWait(driver, 10)
wait.until(lambda d: len(d.window_handles) > 1)
# 4. 切换到新窗口
new_window = [w for w in driver.window_handles if w != main_window][0]
driver.switch_to.window(new_window)
# 5. 在新窗口操作
print(f"新窗口标题:{driver.title}")
# 6. 关闭新窗口
driver.close()
# 7. 切换回主窗口
driver.switch_to.window(main_window)
7.3 窗口切换最佳实践
class WindowManager:
def __init__(self, driver):
self.driver = driver
self.main_window = None
def save_main_window(self):
"""保存主窗口句柄"""
self.main_window = self.driver.current_window_handle
def switch_to_new_window(self):
"""切换到新打开的窗口"""
self.save_main_window()
wait = WebDriverWait(self.driver, 10)
wait.until(lambda d: len(d.window_handles) > len([self.main_window]))
new_window = [w for w in self.driver.window_handles if w != self.main_window][0]
self.driver.switch_to.window(new_window)
return new_window
def switch_to_main_window(self):
"""切换回主窗口"""
if self.main_window:
self.driver.switch_to.window(self.main_window)
def close_current_window(self):
"""关闭当前窗口并切换回主窗口"""
self.driver.close()
self.switch_to_main_window()
# 使用示例
wm = WindowManager(driver)
wm.save_main_window()
# 打开新窗口
link.click()
wm.switch_to_new_window()
# 在新窗口操作
# ...
# 关闭新窗口并返回主窗口
wm.close_current_window()
八、iframe 处理
8.1 什么是 iframe?
iframe(内联框架)是 HTML 中用于嵌入另一个 HTML 页面的元素。Selenium 无法直接操作 iframe 内的元素,必须先切换到 iframe。
8.2 切换到 iframe
# 方式一:通过索引切换(不推荐,不稳定)
driver.switch_to.frame(0) # 切换到第一个 iframe
driver.switch_to.frame(1) # 切换到第二个 iframe
# 方式二:通过 name 或 id 切换(推荐)
driver.switch_to.frame("iframe-name")
driver.switch_to.frame("iframe-id")
# 方式三:通过 WebElement 切换(最推荐)
iframe = driver.find_element(By.ID, "iframe-id")
driver.switch_to.frame(iframe)
# 方式四:使用显式等待(最佳实践)
wait = WebDriverWait(driver, 10)
wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "iframe-id")))
8.3 切换回主页面
# 切换回主页面(最外层)
driver.switch_to.default_content()
# 切换回父级 iframe(如果有多层 iframe)
driver.switch_to.parent_frame()
8.4 多层 iframe 处理
# 假设结构:主页面 → iframe1 → iframe2
# 1. 切换到第一层 iframe
driver.switch_to.frame("iframe1")
# 2. 切换到第二层 iframe
driver.switch_to.frame("iframe2")
# 3. 操作 iframe2 中的元素
element = driver.find_element(By.ID, "element-in-iframe2")
# 4. 切换回 iframe1
driver.switch_to.parent_frame()
# 5. 切换回主页面
driver.switch_to.default_content()
8.5 iframe 处理最佳实践
class IframeManager:
def __init__(self, driver):
self.driver = driver
self.iframe_stack = []
def switch_to_iframe(self, locator):
"""切换到 iframe 并记录"""
wait = WebDriverWait(self.driver, 10)
wait.until(EC.frame_to_be_available_and_switch_to_it(locator))
self.iframe_stack.append(locator)
def switch_back(self):
"""切换回上一级"""
if self.iframe_stack:
self.driver.switch_to.parent_frame()
self.iframe_stack.pop()
def switch_to_default(self):
"""切换回主页面"""
self.driver.switch_to.default_content()
self.iframe_stack.clear()
# 使用示例
iframe_mgr = IframeManager(driver)
# 切换到 iframe
iframe_mgr.switch_to_iframe((By.ID, "iframe-id"))
# 操作 iframe 内的元素
element = driver.find_element(By.ID, "element")
# 切换回主页面
iframe_mgr.switch_to_default()
8.6 常见 iframe 问题
问题 1:找不到 iframe 内的元素
# 原因:没有切换到 iframe
# 解决:先切换再定位
# 错误示例
element = driver.find_element(By.ID, "element") # 在主页面查找,找不到
# 正确示例
driver.switch_to.frame("iframe-id")
element = driver.find_element(By.ID, "element") # 在 iframe 内查找
问题 2:iframe 加载慢
# 解决:使用显式等待
wait = WebDriverWait(driver, 10)
wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "iframe-id")))
问题 3:多层 iframe 混乱
# 解决:使用上下文管理器
from contextlib import contextmanager
@contextmanager
def iframe_context(driver, locator):
"""iframe 上下文管理器"""
driver.switch_to.frame(locator)
try:
yield
finally:
driver.switch_to.default_content()
# 使用
with iframe_context(driver, (By.ID, "iframe-id")):
element = driver.find_element(By.ID, "element")
element.click()
# 自动切换回主页面
九、爬取动态加载数据(重点)
动态加载数据是 Web 爬虫中最常见的场景,Selenium 是处理动态内容的强大工具。
9.1 什么是动态加载数据?
静态内容:HTML 中直接存在的元素
<div id="content">这是静态内容</div>
动态内容:通过 JavaScript、AJAX 异步加载的内容
// JavaScript 异步加载数据
fetch('/api/data').then(response => response.json()).then(data => {
document.getElementById('content').innerHTML = data.content;
});
9.2 识别动态加载内容
特征:
页面初始加载时元素不存在需要滚动才能加载更多内容(无限滚动)点击”加载更多”按钮加载内容通过 AJAX 请求获取数据使用框架如 React、Vue 动态渲染
9.3 等待动态内容加载
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
# 方法一:等待元素出现
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.CLASS_NAME, "dynamic-content")))
# 方法二:等待元素可见
element = wait.until(EC.visibility_of_element_located((By.CLASS_NAME, "dynamic-content")))
# 方法三:等待特定文本出现
wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "加载完成"))
# 方法四:等待 AJAX 加载完成(通过检查加载指示器)
wait.until(EC.invisibility_of_element_located((By.ID, "loading")))
9.4 无限滚动加载
场景:页面滚动到底部时自动加载更多内容
def scroll_and_load_all(driver, max_scrolls=10):
"""滚动页面直到加载所有内容"""
last_height = driver.execute_script("return document.body.scrollHeight")
scroll_count = 0
while scroll_count < max_scrolls:
# 滚动到底部
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 等待新内容加载
time.sleep(2)
# 获取新的页面高度
new_height = driver.execute_script("return document.body.scrollHeight")
# 如果高度没有变化,说明已经加载完所有内容
if new_height == last_height:
break
last_height = new_height
scroll_count += 1
return scroll_count
# 使用示例
driver.get("https://example.com/infinite-scroll")
scroll_and_load_all(driver)
# 现在可以获取所有内容
elements = driver.find_elements(By.CLASS_NAME, "item")
9.5 点击”加载更多”按钮
def click_load_more_until_no_more(driver):
"""点击"加载更多"按钮直到没有更多内容"""
while True:
try:
# 查找"加载更多"按钮
load_more_btn = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable((By.CLASS_NAME, "load-more-btn"))
)
# 滚动到按钮位置
driver.execute_script("arguments[0].scrollIntoView();", load_more_btn)
# 点击按钮
load_more_btn.click()
# 等待新内容加载
WebDriverWait(driver, 10).until(
EC.invisibility_of_element_located((By.CLASS_NAME, "loading"))
)
time.sleep(1) # 等待内容渲染
except TimeoutException:
# 没有"加载更多"按钮或已加载完所有内容
print("已加载所有内容")
break
# 使用示例
driver.get("https://example.com/load-more")
click_load_more_until_no_more(driver)
9.6 处理 AJAX 请求
def wait_for_ajax_complete(driver, timeout=10):
"""等待所有 AJAX 请求完成"""
wait = WebDriverWait(driver, timeout)
# 方法一:检查 jQuery AJAX(如果页面使用 jQuery)
try:
wait.until(lambda d: d.execute_script("return jQuery.active == 0"))
except:
pass
# 方法二:检查加载指示器
try:
wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, "loading")))
except:
pass
# 方法三:等待特定元素出现(表示数据已加载)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "data-item")))
# 使用示例
driver.get("https://example.com/ajax-page")
driver.find_element(By.ID, "load-data-btn").click()
wait_for_ajax_complete(driver)
# 现在可以获取 AJAX 加载的数据
data_items = driver.find_elements(By.CLASS_NAME, "data-item")
9.7 爬取动态表格数据
def scrape_dynamic_table(driver, url):
"""爬取动态加载的表格数据"""
driver.get(url)
# 等待表格加载
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.TAG_NAME, "table")))
# 如果表格有分页,需要处理分页
all_data = []
page = 1
while True:
# 等待当前页数据加载
wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "table tbody tr")))
# 获取当前页数据
rows = driver.find_elements(By.CSS_SELECTOR, "table tbody tr")
for row in rows:
cells = row.find_elements(By.TAG_NAME, "td")
row_data = [cell.text for cell in cells]
all_data.append(row_data)
# 尝试点击下一页
try:
next_btn = driver.find_element(By.CLASS_NAME, "next-page")
if "disabled" in next_btn.get_attribute("class"):
break # 没有下一页了
next_btn.click()
page += 1
time.sleep(2) # 等待页面加载
except:
break # 没有分页按钮或已到最后一页
return all_data
# 使用示例
data = scrape_dynamic_table(driver, "https://example.com/table")
9.8 处理单页应用(SPA)
场景:React、Vue 等框架构建的单页应用
def scrape_spa_content(driver, url):
"""爬取单页应用的内容"""
driver.get(url)
# 等待框架加载完成
wait = WebDriverWait(driver, 10)
# 等待 React 应用加载(检查根元素)
wait.until(EC.presence_of_element_located((By.ID, "root")))
# 等待 Vue 应用加载(检查特定类名)
# wait.until(EC.presence_of_element_located((By.CLASS_NAME, "vue-app")))
# 等待数据加载完成(通过检查加载指示器或特定元素)
wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, "loading")))
# 如果内容是通过路由动态加载的,需要触发路由
# 例如:点击导航链接
nav_link = driver.find_element(By.LINK_TEXT, "数据页面")
nav_link.click()
# 等待新路由的内容加载
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "data-content")))
# 现在可以获取内容
content = driver.find_elements(By.CLASS_NAME, "data-item")
return content
9.9 处理 WebSocket 实时数据
def wait_for_websocket_data(driver, element_locator, timeout=30):
"""等待 WebSocket 数据更新"""
wait = WebDriverWait(driver, timeout)
# 获取初始内容
initial_text = driver.find_element(*element_locator).text
# 等待内容变化(表示 WebSocket 数据已更新)
wait.until(
lambda d: d.find_element(*element_locator).text != initial_text
)
# 返回更新后的内容
return driver.find_element(*element_locator).text
# 使用示例
driver.get("https://example.com/websocket-page")
updated_data = wait_for_websocket_data(driver, (By.ID, "realtime-data"))
9.10 完整爬虫示例:爬取动态商品列表
from selenium import webdriver
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.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time
import json
def scrape_dynamic_products(url):
"""爬取动态加载的商品列表"""
# 创建驱动
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
try:
driver.get(url)
# 等待页面加载
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "product-list")))
products = []
page = 1
while True:
print(f"正在爬取第 {page} 页...")
# 等待商品加载
wait.until(EC.presence_of_all_elements_located((By.CLASS_NAME, "product-item")))
# 滚动页面确保所有商品都加载
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
# 获取当前页所有商品
product_items = driver.find_elements(By.CLASS_NAME, "product-item")
for item in product_items:
try:
# 提取商品信息
name = item.find_element(By.CLASS_NAME, "product-name").text
price = item.find_element(By.CLASS_NAME, "product-price").text
image = item.find_element(By.TAG_NAME, "img").get_attribute("src")
product = {
"name": name,
"price": price,
"image": image
}
products.append(product)
except Exception as e:
print(f"提取商品信息失败:{e}")
continue
# 尝试点击"下一页"
try:
next_btn = driver.find_element(By.CLASS_NAME, "next-page")
if "disabled" in next_btn.get_attribute("class"):
break
# 滚动到按钮位置
driver.execute_script("arguments[0].scrollIntoView();", next_btn)
next_btn.click()
# 等待新页面加载
wait.until(EC.staleness_of(product_items[0]))
page += 1
time.sleep(2)
except:
break
return products
finally:
driver.quit()
# 使用示例
if __name__ == "__main__":
url = "https://example.com/products"
products = scrape_dynamic_products(url)
# 保存数据
with open("products.json", "w", encoding="utf-8") as f:
json.dump(products, f, ensure_ascii=False, indent=2)
print(f"共爬取 {len(products)} 个商品")
9.11 动态数据爬取最佳实践
总是使用显式等待:不要使用 ,使用
time.sleep()检查元素状态:确保元素可见、可点击后再操作处理加载指示器:等待加载完成后再提取数据处理异常:使用 try-except 处理元素不存在的情况优化性能:减少不必要的等待,合理设置超时时间处理反爬虫:添加 User-Agent、使用代理、控制请求频率
WebDriverWait
十、其他高级功能
10.1 Cookie 操作
# 获取所有 Cookie
cookies = driver.get_cookies()
for cookie in cookies:
print(f"{cookie['name']}: {cookie['value']}")
# 获取指定 Cookie
cookie = driver.get_cookie("session_id")
print(cookie)
# 添加 Cookie
driver.add_cookie({
"name": "session_id",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"expires": None # None 表示会话 Cookie
})
# 删除 Cookie
driver.delete_cookie("session_id")
# 删除所有 Cookie
driver.delete_all_cookies()
10.2 JavaScript 执行
# 执行 JavaScript 代码
driver.execute_script("alert('Hello Selenium');")
# 执行 JavaScript 并返回值
result = driver.execute_script("return document.title;")
print(result)
# 传递参数
element = driver.find_element(By.ID, "username")
driver.execute_script("arguments[0].value = 'testuser';", element)
# 滚动到元素
driver.execute_script("arguments[0].scrollIntoView();", element)
# 修改元素属性
driver.execute_script("arguments[0].setAttribute('style', 'display: none;');", element)
# 获取页面信息
page_height = driver.execute_script("return document.body.scrollHeight")
window_height = driver.execute_script("return window.innerHeight")
10.3 截图功能
# 截取整个页面
driver.save_screenshot("screenshot.png")
# 截取指定元素
element = driver.find_element(By.ID, "content")
element.screenshot("element_screenshot.png")
# 获取截图(Base64)
screenshot_base64 = driver.get_screenshot_as_base64()
# 获取截图(二进制)
screenshot_binary = driver.get_screenshot_as_png()
10.4 页面信息获取
# 获取页面标题
title = driver.title
print(f"页面标题:{title}")
# 获取当前 URL
current_url = driver.current_url
print(f"当前 URL:{current_url}")
# 获取页面源码
page_source = driver.page_source
# 获取窗口大小
window_size = driver.get_window_size()
print(f"窗口大小:{window_size}")
# 设置窗口大小
driver.set_window_size(1920, 1080)
# 最大化窗口
driver.maximize_window()
# 最小化窗口
driver.minimize_window()
10.5 浏览器操作
# 前进
driver.forward()
# 后退
driver.back()
# 刷新
driver.refresh()
# 关闭当前窗口
driver.close()
# 关闭所有窗口并退出驱动
driver.quit()
10.6 键盘操作
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
# 发送组合键
element = driver.find_element(By.ID, "input")
element.send_keys(Keys.CONTROL, 'a') # Ctrl+A
element.send_keys(Keys.CONTROL, 'c') # Ctrl+C
element.send_keys(Keys.CONTROL, 'v') # Ctrl+V
# 使用 ActionChains 发送组合键
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL).send_keys('a').key_up(Keys.CONTROL).perform()
10.7 鼠标操作
from selenium.webdriver.common.action_chains import ActionChains
element = driver.find_element(By.ID, "button")
# 创建 ActionChains 对象
actions = ActionChains(driver)
# 悬停
actions.move_to_element(element).perform()
# 右键点击
actions.context_click(element).perform()
# 双击
actions.double_click(element).perform()
# 拖拽
source = driver.find_element(By.ID, "source")
target = driver.find_element(By.ID, "target")
actions.drag_and_drop(source, target).perform()
# 链式操作
actions.move_to_element(element).click().perform()
10.8 页面加载策略
from selenium.webdriver.chrome.options import Options
options = Options()
# 设置页面加载策略
# normal: 等待所有资源加载完成(默认)
# eager: 等待 DOM 加载完成,不等待图片、样式表等
# none: 不等待任何资源加载
options.page_load_strategy = "eager"
driver = webdriver.Chrome(options=options)
10.9 无头模式(Headless)
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--headless") # 无头模式
options.add_argument("--disable-gpu") # 禁用 GPU
options.add_argument("--no-sandbox") # 禁用沙箱
driver = webdriver.Chrome(options=options)
10.10 禁用图片和 CSS 加载(提高速度)
from selenium.webdriver.chrome.options import Options
prefs = {
"profile.managed_default_content_settings.images": 2, # 禁用图片
"profile.managed_default_content_settings.stylesheets": 2 # 禁用 CSS
}
options = Options()
options.add_experimental_option("prefs", prefs)
driver = webdriver.Chrome(options=options)
十一、实战案例
11.1 案例一:自动化登录并获取数据
from selenium import webdriver
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.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
def auto_login_and_get_data(username, password):
"""自动化登录并获取数据"""
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
try:
# 1. 打开登录页面
driver.get("https://example.com/login")
# 2. 等待页面加载
wait = WebDriverWait(driver, 10)
# 3. 输入用户名
username_input = wait.until(
EC.presence_of_element_located((By.ID, "username"))
)
username_input.clear()
username_input.send_keys(username)
# 4. 输入密码
password_input = driver.find_element(By.ID, "password")
password_input.clear()
password_input.send_keys(password)
# 5. 点击登录按钮
login_btn = driver.find_element(By.ID, "login-btn")
login_btn.click()
# 6. 等待登录成功(检查 URL 或特定元素)
wait.until(EC.url_contains("dashboard"))
# 7. 获取数据
data_elements = wait.until(
EC.presence_of_all_elements_located((By.CLASS_NAME, "data-item"))
)
data = []
for element in data_elements:
data.append(element.text)
return data
finally:
driver.quit()
# 使用示例
data = auto_login_and_get_data("testuser", "testpass")
print(data)
11.2 案例二:批量处理表单
def batch_process_forms(driver, form_data_list):
"""批量处理表单"""
results = []
for form_data in form_data_list:
try:
# 填写表单
driver.find_element(By.ID, "name").clear()
driver.find_element(By.ID, "name").send_keys(form_data["name"])
driver.find_element(By.ID, "email").clear()
driver.find_element(By.ID, "email").send_keys(form_data["email"])
# 提交表单
driver.find_element(By.ID, "submit-btn").click()
# 等待提交成功
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "success-message")))
results.append({"status": "success", "data": form_data})
except Exception as e:
results.append({"status": "error", "data": form_data, "error": str(e)})
return results
11.3 案例三:爬取电商网站商品信息
def scrape_ecommerce_products(driver, category_url):
"""爬取电商网站商品信息"""
driver.get(category_url)
wait = WebDriverWait(driver, 10)
products = []
# 等待商品列表加载
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "product-list")))
# 滚动加载所有商品
scroll_and_load_all(driver)
# 获取所有商品
product_items = driver.find_elements(By.CLASS_NAME, "product-item")
for item in product_items:
try:
product = {
"name": item.find_element(By.CLASS_NAME, "product-name").text,
"price": item.find_element(By.CLASS_NAME, "product-price").text,
"rating": item.find_element(By.CLASS_NAME, "rating").get_attribute("data-rating"),
"image": item.find_element(By.TAG_NAME, "img").get_attribute("src"),
"link": item.find_element(By.TAG_NAME, "a").get_attribute("href")
}
products.append(product)
except Exception as e:
print(f"提取商品信息失败:{e}")
continue
return products
十二、面试题
面试题 1:Selenium 的工作原理是什么?
答案要点:
WebDriver 协议:Selenium 使用 WebDriver 协议与浏览器通信浏览器驱动:每个浏览器都有对应的驱动(ChromeDriver、GeckoDriver 等)JSON Wire Protocol:Selenium 通过 HTTP 请求发送 JSON 格式的命令给浏览器驱动浏览器执行:浏览器驱动接收命令后,控制浏览器执行相应操作返回结果:浏览器执行完成后,将结果返回给 Selenium
流程图:
Selenium 脚本 → WebDriver API → JSON Wire Protocol → 浏览器驱动 → 浏览器
面试题 2:Selenium 中元素定位的方式有哪些?优先级如何?
答案要点:
8 种定位方式:ID、Name、Class Name、Tag Name、Link Text、Partial Link Text、XPath、CSS Selector优先级:
ID:最优先,唯一且快速Name:次优先,如果 ID 不可用CSS Selector:性能好,语法简洁XPath:功能强大,但性能相对较慢其他:根据实际情况选择
面试题 3:显式等待和隐式等待的区别?
答案要点:
| 特性 | 隐式等待 | 显式等待 |
|---|---|---|
| 设置方式 | |
|
| 作用范围 | 全局,对所有元素查找生效 | 局部,针对特定条件 |
| 等待条件 | 只能等待元素存在 | 可以等待多种条件(可见、可点击等) |
| 灵活性 | 较低 | 较高 |
| 性能 | 如果元素不存在,必须等到超时 | 条件满足立即返回 |
| 推荐使用 | 作为兜底 | 关键操作使用 |
面试题 4:如何处理动态加载的元素?
答案要点:
使用显式等待: +
WebDriverWait等待元素出现:
expected_conditions等待元素可见:
EC.presence_of_element_located()等待 AJAX 完成:检查加载指示器消失等待特定条件:文本出现、元素可点击等避免使用
EC.visibility_of_element_located():效率低且不稳定
time.sleep()
示例代码:
wait = WebDriverWait(driver, 10)
element = wait.until(EC.visibility_of_element_located((By.ID, "dynamic-element")))
面试题 5:如何处理 iframe?
答案要点:
切换到 iframe:切换方式:
driver.switch_to.frame()
通过索引:通过 name/id:
driver.switch_to.frame(0)通过 WebElement:
driver.switch_to.frame("iframe-id") 切换回主页面:
driver.switch_to.frame(iframe_element)多层 iframe:使用
driver.switch_to.default_content() 逐层返回最佳实践:使用显式等待确保 iframe 加载完成
parent_frame()
面试题 6:如何处理弹窗?
答案要点:
Alert 弹窗:Confirm 弹窗:
driver.switch_to.alert.accept() 或
alert.accept()Prompt 弹窗:
alert.dismiss() +
alert.send_keys()自定义弹窗:当作普通元素处理最佳实践:使用显式等待确保弹窗出现
alert.accept()
面试题 7:如何切换窗口?
答案要点:
获取窗口句柄:、
driver.current_window_handle切换窗口:
driver.window_handles切换到最后打开的窗口:
driver.switch_to.window(window_handle)关闭窗口:
driver.switch_to.window(driver.window_handles[-1])(关闭当前窗口)最佳实践:先保存主窗口句柄,操作完新窗口后切换回去
driver.close()
面试题 8:Selenium 如何爬取动态加载的数据?
答案要点:
识别动态内容:通过浏览器开发者工具检查网络请求等待内容加载:使用显式等待等待元素出现处理无限滚动:滚动到底部触发加载处理”加载更多”按钮:循环点击直到没有更多内容处理 AJAX 请求:等待加载指示器消失处理单页应用:等待框架加载完成,触发路由
面试题 9:Selenium 和 BeautifulSoup 的区别?
答案要点:
| 特性 | Selenium | BeautifulSoup |
|---|---|---|
| 功能 | 浏览器自动化 | HTML 解析 |
| JavaScript | 支持执行 JavaScript | 不支持 |
| 动态内容 | 可以处理动态加载的内容 | 只能解析静态 HTML |
| 性能 | 较慢(需要启动浏览器) | 较快(纯解析) |
| 使用场景 | 需要交互、动态内容 | 静态页面解析 |
| 配合使用 | 可以先用 Selenium 获取页面,再用 BeautifulSoup 解析 | – |
面试题 10:如何提高 Selenium 的执行速度?
答案要点:
使用无头模式:禁用图片和 CSS:减少资源加载设置页面加载策略:
--headless使用显式等待而非强制等待:避免不必要的等待合理设置超时时间:不要设置过长的等待时间使用 CSS Selector 而非 XPath:CSS Selector 性能更好并行执行:使用 Selenium Grid 或 pytest-xdist
page_load_strategy = "eager"
面试题 11:如何处理反爬虫机制?
答案要点:
设置 User-Agent:模拟真实浏览器使用代理:轮换 IP 地址控制请求频率:添加延迟,避免请求过快处理验证码:使用 OCR 或第三方服务使用 Cookie:保持会话状态模拟人类行为:随机延迟、鼠标移动等
示例代码:
options = Options()
options.add_argument("--user-agent=Mozilla/5.0...")
driver = webdriver.Chrome(options=options)
面试题 12:Selenium 中如何处理下拉框?
答案要点:
使用 Select 类:创建 Select 对象:
from selenium.webdriver.support.ui import Select选择方式:
select = Select(element)
:通过可见文本
select_by_visible_text():通过 value 值
select_by_value():通过索引 获取选项:
select_by_index()、
select.options取消选择:
select.first_selected_option(仅多选下拉框)
deselect_all()
面试题 13:如何处理文件上传?
答案要点:
方式一:直接使用 发送文件路径(推荐)
send_keys()
file_input = driver.find_element(By.ID, "file-upload")
file_input.send_keys("/path/to/file.txt")
方式二:使用 AutoIt 或 PyAutoGUI(需要点击”浏览”按钮的场景)注意事项:文件路径必须是绝对路径
面试题 14:Selenium 中如何执行 JavaScript?
答案要点:
使用 :执行 JavaScript 代码返回值:可以获取 JavaScript 执行的结果传递参数:通过
execute_script() 传递参数常见用途:
arguments
滚动页面修改元素属性获取页面信息执行复杂操作
示例代码:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
result = driver.execute_script("return document.title;")
driver.execute_script("arguments[0].click();", element)
面试题 15:Selenium Grid 是什么?如何使用?
答案要点:
定义:Selenium Grid 用于分布式测试,可以在多台机器上并行执行测试组成:
Hub:中心节点,接收测试请求并分发Node:执行节点,运行实际的测试 优势:
并行执行,提高效率支持多浏览器、多平台测试资源利用率高 使用场景:大规模测试、多浏览器兼容性测试
面试题 16:如何处理 StaleElementReferenceException?
答案要点:
原因:元素在 DOM 中被移除或重新创建解决方案:
重新定位元素使用显式等待确保元素稳定使用 try-except 捕获异常并重试 最佳实践:在操作元素前重新定位
示例代码:
try:
element.click()
except StaleElementReferenceException:
element = driver.find_element(By.ID, "element-id")
element.click()
面试题 17:Selenium 中如何实现数据驱动测试?
答案要点:
使用参数化:pytest 的 从文件读取数据:CSV、JSON、Excel 等使用 Fixture:pytest 的 Fixture 提供测试数据数据库驱动:从数据库读取测试数据
@pytest.mark.parametrize
示例代码:
@pytest.mark.parametrize("username,password", [
("user1", "pass1"),
("user2", "pass2"),
])
def test_login(username, password):
# 测试逻辑
pass
面试题 18:如何实现 Page Object Model(POM)?
答案要点:
定义:将页面元素和操作封装成类优势:
代码复用易于维护测试逻辑清晰 实现:
创建页面类封装元素定位封装页面操作
示例代码:
class LoginPage:
def __init__(self, driver):
self.driver = driver
self.username_input = (By.ID, "username")
self.password_input = (By.ID, "password")
self.login_btn = (By.ID, "login-btn")
def login(self, username, password):
self.driver.find_element(*self.username_input).send_keys(username)
self.driver.find_element(*self.password_input).send_keys(password)
self.driver.find_element(*self.login_btn).click()
面试题 19:Selenium 中如何处理验证码?
答案要点:
OCR 识别:使用 Tesseract、pytesseract第三方服务:使用打码平台 API测试环境:设置验证码为固定值或跳过Cookie 绕过:登录后保存 Cookie,下次直接使用人工处理:暂停等待人工输入
面试题 20:Selenium 和 Appium 的区别?
答案要点:
| 特性 | Selenium | Appium |
|---|---|---|
| 测试对象 | Web 应用 | 移动应用(iOS、Android) |
| 协议 | WebDriver | WebDriver(扩展) |
| 支持平台 | 浏览器 | iOS、Android |
| 元素定位 | Web 元素 | 移动应用元素 |
| 使用场景 | Web 自动化测试 | 移动应用自动化测试 |
十三、Playwright 详解
13.1 什么是 Playwright?
Playwright 是 Microsoft 开发的现代化 Web 自动化测试框架,支持 Chromium、Firefox 和 WebKit 浏览器。它提供了更强大的 API、更好的性能和更丰富的功能。
13.2 Playwright 的特点
现代化设计:专为现代 Web 应用设计,支持单页应用(SPA)自动等待:内置智能等待机制,无需手动等待多浏览器支持:Chromium、Firefox、WebKit(Safari)多语言支持:Python、JavaScript、TypeScript、Java、C#强大的网络拦截:可以拦截和修改网络请求自动生成代码:Codegen 工具可以录制生成测试代码并行执行:原生支持并行测试执行移动端支持:支持移动设备模拟
13.3 Playwright 的优势
相比 Selenium:
性能更好:直接与浏览器通信,无需 WebDriver自动等待:内置智能等待,减少 flaky 测试更强大的 API:提供更多实用功能更好的调试:内置追踪和视频录制更快的执行速度:并行执行效率更高
13.4 Playwright 的应用场景
Web 自动化测试:E2E 测试、集成测试Web 爬虫:爬取动态内容UI 自动化:重复性操作自动化API 测试:网络请求拦截和验证性能测试:页面性能监控
十四、Playwright 核心功能
14.1 环境搭建
14.1.1 安装 Playwright
# 安装 Playwright Python 包
pip install playwright
# 安装浏览器驱动(Chromium、Firefox、WebKit)
playwright install
# 或者只安装特定浏览器
playwright install chromium
playwright install firefox
playwright install webkit
14.1.2 第一个 Playwright 脚本
from playwright.sync_api import sync_playwright
def run(playwright):
# 启动浏览器
browser = playwright.chromium.launch(headless=False)
# 创建页面上下文
page = browser.new_page()
# 访问网页
page.goto("https://www.baidu.com")
# 打印页面标题
print(page.title())
# 关闭浏览器
browser.close()
with sync_playwright() as playwright:
run(playwright)
14.2 元素定位
Playwright 提供了强大的定位器(Locator)API,支持多种定位方式。
14.2.1 基本定位方式
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
# 通过文本定位
button = page.get_by_text("登录")
# 通过角色定位(推荐)
button = page.get_by_role("button", name="登录")
link = page.get_by_role("link", name="首页")
# 通过标签和文本定位
heading = page.get_by_role("heading", name="欢迎")
# 通过占位符定位
input = page.get_by_placeholder("请输入用户名")
# 通过标签定位
input = page.locator("input")
# 通过 ID 定位
element = page.locator("#username")
# 通过 Class 定位
element = page.locator(".login-btn")
# 通过属性定位
element = page.locator("[name='username']")
# 通过 XPath 定位
element = page.locator("xpath=//button[@id='submit']")
# 通过 CSS Selector 定位
element = page.locator("css=button#submit")
browser.close()
14.2.2 定位器链式操作
# Playwright 支持链式定位
page.locator("div").filter(has_text="登录").click()
# 组合定位
page.locator("form").locator("input[name='username']").fill("testuser")
# 父级定位
page.locator("input").locator("..").click() # 点击 input 的父元素
14.2.3 定位器最佳实践
# ✅ 推荐:使用 get_by_role(最稳定)
button = page.get_by_role("button", name="提交")
# ✅ 推荐:使用 get_by_text
link = page.get_by_text("点击这里")
# ✅ 推荐:使用 get_by_placeholder
input = page.get_by_placeholder("请输入")
# ⚠️ 谨慎:使用 locator(如果上述方法不可用)
element = page.locator("#username")
# ❌ 避免:使用 XPath(除非必要)
element = page.locator("xpath=//div[@id='content']")
14.3 元素操作
14.3.1 输入操作
# 输入文本(自动等待元素可见)
page.get_by_placeholder("用户名").fill("testuser")
# 清空并输入
page.get_by_placeholder("用户名").clear()
page.get_by_placeholder("用户名").type("testuser", delay=100) # 模拟打字,每个字符延迟 100ms
# 输入特殊键
page.get_by_placeholder("搜索").press("Enter")
page.get_by_placeholder("搜索").press("Control+A") # Ctrl+A
14.3.2 点击操作
# 普通点击
page.get_by_role("button", name="登录").click()
# 双击
page.get_by_role("button", name="提交").dblclick()
# 右键点击
page.get_by_role("button", name="菜单").click(button="right")
# 强制点击(即使元素被遮挡)
page.get_by_role("button", name="提交").click(force=True)
# 点击坐标
page.click(100, 200)
14.3.3 获取元素信息
# 获取文本内容
text = page.get_by_role("heading").text_content()
# 获取内部 HTML
html = page.locator("div").inner_html()
# 获取属性值
value = page.locator("input").get_attribute("value")
href = page.locator("a").get_attribute("href")
# 判断元素是否可见
is_visible = page.get_by_role("button").is_visible()
# 判断元素是否启用
is_enabled = page.get_by_role("button").is_enabled()
# 判断元素是否被选中
is_checked = page.locator("input[type='checkbox']").is_checked()
# 获取元素数量
count = page.locator(".item").count()
14.4 自动等待机制(核心优势)
Playwright 的自动等待是其核心优势之一,无需手动等待。
14.4.1 自动等待的工作原理
# Playwright 在执行操作前会自动等待元素满足条件
page.get_by_role("button", name="提交").click()
# ↑ 自动等待按钮可见、可点击
page.get_by_placeholder("用户名").fill("testuser")
# ↑ 自动等待输入框可见、可编辑
page.get_by_text("成功").wait_for()
# ↑ 等待文本出现
14.4.2 显式等待
from playwright.sync_api import expect
# 等待元素可见
page.get_by_role("button", name="提交").wait_for(state="visible")
# 等待元素隐藏
page.get_by_id("loading").wait_for(state="hidden")
# 等待文本出现
page.get_by_text("加载完成").wait_for()
# 等待 URL 变化
page.wait_for_url("**/dashboard")
# 等待导航完成
page.wait_for_load_state("networkidle") # 等待网络空闲
page.wait_for_load_state("domcontentloaded") # 等待 DOM 加载完成
page.wait_for_load_state("load") # 等待所有资源加载完成
14.4.3 断言和期望
from playwright.sync_api import expect
# 断言元素可见
expect(page.get_by_role("button", name="提交")).to_be_visible()
# 断言文本内容
expect(page.get_by_role("heading")).to_have_text("欢迎")
# 断言属性值
expect(page.locator("input")).to_have_value("testuser")
# 断言 URL
expect(page).to_have_url("https://example.com/dashboard")
# 断言标题
expect(page).to_have_title("首页")
# 断言元素数量
expect(page.locator(".item")).to_have_count(10)
# 断言元素包含文本
expect(page.get_by_text("成功")).to_be_visible()
14.5 页面导航
# 访问页面
page.goto("https://example.com")
# 等待页面加载完成
page.goto("https://example.com", wait_until="networkidle")
# 前进
page.go_forward()
# 后退
page.go_back()
# 刷新
page.reload()
# 获取当前 URL
current_url = page.url
# 获取页面标题
title = page.title()
14.6 弹窗处理
# 监听弹窗(Alert、Confirm、Prompt)
page.on("dialog", lambda dialog: dialog.accept())
# 或者获取弹窗对象
def handle_dialog(dialog):
print(f"弹窗文本:{dialog.message}")
dialog.accept() # 或 dialog.dismiss()
page.on("dialog", handle_dialog)
# 处理自定义弹窗(Modal)
page.get_by_role("button", name="关闭").click()
14.7 窗口和标签页管理
# 获取当前页面
current_page = page
# 监听新页面打开
def handle_new_page(new_page):
new_page.wait_for_load_state()
print(f"新页面标题:{new_page.title()}")
new_page.close()
page.context.on("page", handle_new_page)
# 或者等待新页面
with page.context.expect_page() as new_page_info:
page.get_by_role("link", name="新窗口").click()
new_page = new_page_info.value
# 切换到新页面
new_page.bring_to_front() # 将页面置于前台
# 获取所有页面
all_pages = page.context.pages
14.8 iframe 处理
# 定位 iframe
iframe = page.frame_locator("iframe[name='myframe']")
# 在 iframe 内操作元素
iframe.get_by_role("button", name="提交").click()
# 或者通过 URL 定位
iframe = page.frame(url="**/iframe.html")
# 获取 iframe 内容
iframe_content = iframe.content()
# 切换回主页面(Playwright 自动处理,无需手动切换)
14.9 网络拦截和监控
# 拦截网络请求
def handle_route(route):
if "api/data" in route.request.url:
# 修改请求
route.continue_(url="https://mock-api.com/data")
else:
route.continue_()
page.route("**/*", handle_route)
# 拦截响应
def handle_response(response):
if "api/data" in response.url:
print(f"响应状态:{response.status}")
print(f"响应内容:{response.json()}")
page.on("response", handle_response)
# 等待网络请求完成
with page.expect_response("**/api/data") as response_info:
page.get_by_role("button", name="加载数据").click()
response = response_info.value
print(response.json())
14.10 文件操作
# 文件上传
page.get_by_label("选择文件").set_input_files("/path/to/file.txt")
# 多文件上传
page.get_by_label("选择文件").set_input_files([
"/path/to/file1.txt",
"/path/to/file2.txt"
])
# 文件下载
with page.expect_download() as download_info:
page.get_by_role("link", name="下载").click()
download = download_info.value
download.save_as("/path/to/save/file.txt")
14.11 截图和视频
# 页面截图
page.screenshot(path="screenshot.png")
# 元素截图
page.locator(".content").screenshot(path="element.png")
# 全屏截图
page.screenshot(path="fullscreen.png", full_page=True)
# 录制视频(需要在浏览器上下文中启用)
context = browser.new_context(record_video_dir="videos/")
page = context.new_page()
# ... 执行操作 ...
context.close() # 视频会自动保存
14.12 移动设备模拟
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
# 使用设备预设
iphone = playwright.devices["iPhone 12"]
browser = playwright.chromium.launch()
context = browser.new_context(**iphone)
page = context.new_page()
# 或者自定义设备
context = browser.new_context(
viewport={"width": 375, "height": 667},
user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)..."
)
page.goto("https://example.com")
browser.close()
14.13 代码生成(Codegen)
Playwright 提供了强大的代码生成工具,可以录制操作并生成代码。
# 启动代码生成器
playwright codegen https://example.com
# 指定浏览器
playwright codegen --browser firefox https://example.com
# 保存到文件
playwright codegen --target python -o test.py https://example.com
14.14 追踪和调试
# 启用追踪
context = browser.new_context()
context.tracing.start(screenshots=True, snapshots=True)
page = context.new_page()
# ... 执行操作 ...
# 停止追踪并保存
context.tracing.stop(path="trace.zip")
# 查看追踪(使用 Playwright Inspector)
# playwright show-trace trace.zip
14.15 异步 API(Async API)
Playwright 支持同步和异步两种 API,异步 API 性能更好,适合并发场景。
14.15.1 异步 API 基础
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as playwright:
browser = await playwright.chromium.launch()
page = await browser.new_page()
await page.goto("https://example.com")
print(await page.title())
await browser.close()
asyncio.run(main())
14.15.2 并发执行多个操作
async def scrape_multiple_pages():
async with async_playwright() as playwright:
browser = await playwright.chromium.launch()
# 并发访问多个页面
tasks = []
urls = ["https://example.com/page1", "https://example.com/page2", "https://example.com/page3"]
for url in urls:
async def scrape_page(url):
page = await browser.new_page()
await page.goto(url)
title = await page.title()
await page.close()
return title
tasks.append(scrape_page(url))
# 并发执行
titles = await asyncio.gather(*tasks)
await browser.close()
return titles
# 使用
titles = asyncio.run(scrape_multiple_pages())
14.15.3 异步等待
async def wait_for_content():
async with async_playwright() as playwright:
browser = await playwright.chromium.launch()
page = await browser.new_page()
await page.goto("https://example.com")
# 异步等待元素出现
await page.wait_for_selector(".content")
# 异步等待网络请求
async with page.expect_response("**/api/data") as response_info:
await page.click("button#load-data")
response = await response_info.value
data = await response.json()
await browser.close()
return data
14.16 浏览器上下文(Context)详解
浏览器上下文是 Playwright 的核心概念,提供了隔离的浏览器环境。
14.16.1 创建和管理上下文
# 创建上下文
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0...",
locale="zh-CN",
timezone_id="Asia/Shanghai"
)
# 创建页面
page = context.new_page()
# 获取所有页面
all_pages = context.pages
# 关闭上下文(会关闭所有页面)
context.close()
14.16.2 上下文配置选项
context = browser.new_context(
# 视口大小
viewport={"width": 1920, "height": 1080},
# 用户代理
user_agent="Mozilla/5.0...",
# 语言设置
locale="zh-CN",
# 时区
timezone_id="Asia/Shanghai",
# 地理位置
geolocation={"latitude": 39.9042, "longitude": 116.4074},
# 权限
permissions=["geolocation", "notifications"],
# 颜色方案
color_scheme="dark", # "light" 或 "dark"
# 减少动画
reduced_motion="reduce",
# 忽略 HTTPS 错误
ignore_https_errors=True,
# JavaScript 启用/禁用
java_script_enabled=True,
# 离线模式
offline=False,
# HTTP 凭据
http_credentials={"username": "user", "password": "pass"},
# 代理设置
proxy={"server": "http://proxy.example.com:8080"},
# Cookie
storage_state="cookies.json",
# 视频录制
record_video_dir="videos/",
record_video_size={"width": 1920, "height": 1080},
# 截图
screenshot="only-on-failure",
# 追踪
trace="on" # "on", "off", "on-first-retry"
)
14.16.3 上下文隔离
# 每个上下文都是隔离的,互不影响
context1 = browser.new_context()
context2 = browser.new_context()
page1 = context1.new_page()
page2 = context2.new_page()
# 两个页面的 Cookie、LocalStorage 等完全隔离
await page1.goto("https://example.com")
await page1.evaluate("localStorage.setItem('key', 'value1')")
await page2.goto("https://example.com")
await page2.evaluate("localStorage.setItem('key', 'value2')")
# 两个页面的 LocalStorage 互不影响
14.17 高级元素操作
14.17.1 拖拽操作
# 拖拽元素
source = page.locator("#source")
target = page.locator("#target")
source.drag_to(target)
# 或者指定坐标拖拽
source.drag_to(target, target_position={"x": 100, "y": 200})
# 拖拽到坐标
page.mouse.move(100, 100)
page.mouse.down()
page.mouse.move(200, 200)
page.mouse.up()
14.17.2 悬停操作
# 悬停在元素上
page.get_by_role("button", name="菜单").hover()
# 悬停并等待子菜单出现
page.get_by_role("button", name="菜单").hover()
page.get_by_text("子菜单项").click()
14.17.3 键盘操作
# 按下键
page.keyboard.press("Enter")
page.keyboard.press("Control+A")
# 输入文本
page.keyboard.type("Hello World", delay=100) # 每个字符延迟 100ms
# 按下并释放
page.keyboard.down("Shift")
page.keyboard.press("ArrowRight")
page.keyboard.up("Shift")
# 插入文本
page.keyboard.insert_text("插入的文本")
14.17.4 鼠标操作
# 移动鼠标
page.mouse.move(100, 200)
# 点击
page.mouse.click(100, 200)
# 双击
page.mouse.dblclick(100, 200)
# 按下
page.mouse.down()
# 释放
page.mouse.up()
# 滚轮
page.mouse.wheel(0, 500) # 向下滚动 500 像素
14.18 高级等待策略
14.18.1 等待页面加载状态
# 等待 DOM 内容加载完成
page.wait_for_load_state("domcontentloaded")
# 等待所有资源加载完成
page.wait_for_load_state("load")
# 等待网络空闲(500ms 内没有网络请求)
page.wait_for_load_state("networkidle")
# 等待直到特定条件
page.wait_for_function("document.readyState === 'complete'")
14.18.2 等待元素状态
# 等待元素可见
page.wait_for_selector(".content", state="visible")
# 等待元素隐藏
page.wait_for_selector(".loading", state="hidden")
# 等待元素附加到 DOM
page.wait_for_selector(".content", state="attached")
# 等待元素从 DOM 分离
page.wait_for_selector(".old-element", state="detached")
14.18.3 等待函数执行
# 等待 JavaScript 函数返回 true
page.wait_for_function("window.myFunction() === true")
# 等待函数,传递参数
page.wait_for_function("(count) => count > 10", arg=15)
# 等待元素满足条件
page.wait_for_function("""
() => {
const element = document.querySelector('.content');
return element && element.textContent.includes('加载完成');
}
""")
14.18.4 等待 URL 变化
# 等待 URL 匹配模式
page.wait_for_url("**/dashboard")
# 等待 URL 包含文本
page.wait_for_url("**/user/*/profile")
# 使用正则表达式
page.wait_for_url(re.compile(r".*/user/d+/profile"))
14.19 网络请求详解
14.19.1 请求拦截和修改
# 拦截并修改请求
def handle_route(route):
# 获取请求信息
request = route.request
print(f"请求 URL: {request.url}")
print(f"请求方法: {request.method}")
print(f"请求头: {request.headers}")
# 修改请求头
headers = request.headers.copy()
headers["X-Custom-Header"] = "custom-value"
# 继续请求(使用修改后的头)
route.continue_(headers=headers)
page.route("**/*", handle_route)
# 拦截并返回模拟响应
def mock_response(route):
if "api/data" in route.request.url:
route.fulfill(
status=200,
headers={"Content-Type": "application/json"},
body='{"data": "mock data"}'
)
else:
route.continue_()
page.route("**/api/**", mock_response)
# 中止请求
def abort_images(route):
if route.request.resource_type == "image":
route.abort()
else:
route.continue_()
page.route("**/*", abort_images)
14.19.2 响应监听和处理
# 监听所有响应
def handle_response(response):
print(f"响应 URL: {response.url}")
print(f"响应状态: {response.status}")
print(f"响应头: {response.headers}")
# 获取响应体
if "application/json" in response.headers.get("content-type", ""):
data = response.json()
print(f"响应数据: {data}")
page.on("response", handle_response)
# 等待特定响应
async with page.expect_response("**/api/data") as response_info:
page.click("button#load-data")
response = await response_info.value
data = await response.json()
# 等待响应满足条件
async with page.expect_response(
lambda response: response.url.endswith("/api/data") and response.status == 200
) as response_info:
page.click("button#load-data")
14.19.3 请求失败处理
# 监听请求失败
def handle_request_failed(request):
print(f"请求失败: {request.url}")
print(f"失败原因: {request.failure}")
page.on("requestfailed", handle_request_failed)
# 重试失败的请求
def retry_failed_request(route):
try:
route.continue_()
except Exception as e:
# 请求失败,可以重试
print(f"请求失败,重试: {e}")
route.continue_()
page.route("**/*", retry_failed_request)
14.20 高级选择器和定位技巧
14.20.1 组合选择器
# 通过文本和角色组合
page.get_by_role("button").filter(has_text="提交")
# 通过多个条件
page.locator("div").filter(has_text="内容").filter(has=page.locator("button"))
# 通过父元素
page.locator("form").locator("input[name='username']")
# 通过兄弟元素
page.locator("input").locator("..").locator("button")
14.20.2 链式定位
# 链式定位多个元素
page.locator("div.container").locator("ul.list").locator("li.item").first.click()
# 使用 nth() 选择第 n 个元素
page.locator("li.item").nth(2).click()
# 使用 first 和 last
page.locator("li.item").first.click()
page.locator("li.item").last.click()
# 使用 filter 过滤
page.locator("li.item").filter(has_text="特定文本").click()
14.20.3 定位器方法
# 获取所有匹配的元素
all_items = page.locator(".item").all()
for item in all_items:
print(item.text_content())
# 获取元素数量
count = page.locator(".item").count()
# 检查元素是否存在
if page.locator(".item").count() > 0:
page.locator(".item").first.click()
# 获取元素的边界框
bbox = page.locator(".item").bounding_box()
print(f"位置: ({bbox['x']}, {bbox['y']}), 大小: {bbox['width']}x{bbox['height']}")
14.21 页面评估(Evaluate)详解
14.21.1 执行 JavaScript
# 执行 JavaScript 并返回值
title = page.evaluate("document.title")
# 传递参数
result = page.evaluate("(arg1, arg2) => arg1 + arg2", 10, 20)
# 访问页面对象
result = page.evaluate("""
() => {
return {
title: document.title,
url: window.location.href,
userAgent: navigator.userAgent
};
}
""")
14.21.2 在元素上下文中执行
# 在元素上下文中执行 JavaScript
element = page.locator(".content")
text = element.evaluate("el => el.textContent")
# 修改元素
element.evaluate("el => el.style.color = 'red'")
# 获取元素属性
value = element.evaluate("el => el.getAttribute('data-value')")
14.21.3 处理复杂对象
# 传递复杂对象
data = {"name": "test", "value": 123}
result = page.evaluate("(data) => data.name + data.value", data)
# 返回复杂对象
result = page.evaluate("""
() => {
const items = document.querySelectorAll('.item');
return Array.from(items).map(item => ({
text: item.textContent,
href: item.href
}));
}
""")
14.22 Cookie 和存储管理
14.22.1 Cookie 操作
# 获取所有 Cookie
cookies = context.cookies()
for cookie in cookies:
print(f"{cookie['name']}: {cookie['value']}")
# 获取特定 URL 的 Cookie
cookies = context.cookies("https://example.com")
# 添加 Cookie
context.add_cookies([{
"name": "session_id",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"expires": -1, # -1 表示会话 Cookie
"httpOnly": True,
"secure": True,
"sameSite": "Lax"
}])
# 清除 Cookie
context.clear_cookies()
# 清除特定 URL 的 Cookie
context.clear_cookies("https://example.com")
14.22.2 LocalStorage 和 SessionStorage
# 设置 LocalStorage
page.evaluate("localStorage.setItem('key', 'value')")
# 获取 LocalStorage
value = page.evaluate("localStorage.getItem('key')")
# 清除 LocalStorage
page.evaluate("localStorage.clear()")
# SessionStorage 操作类似
page.evaluate("sessionStorage.setItem('key', 'value')")
value = page.evaluate("sessionStorage.getItem('key')")
14.22.3 保存和恢复状态
# 保存状态(包括 Cookie、LocalStorage 等)
context.storage_state(path="state.json")
# 从文件恢复状态
context = browser.new_context(storage_state="state.json")
# 在代码中使用状态
saved_state = context.storage_state()
new_context = browser.new_context(storage_state=saved_state)
14.23 性能监控和分析
14.23.1 性能指标
# 获取性能指标
performance_timing = page.evaluate("""
() => {
const perf = performance.timing;
return {
domContentLoaded: perf.domContentLoadedEventEnd - perf.navigationStart,
loadComplete: perf.loadEventEnd - perf.navigationStart,
firstPaint: performance.getEntriesByType('paint')[0].startTime
};
}
""")
print(f"DOM 加载时间: {performance_timing['domContentLoaded']}ms")
print(f"页面加载时间: {performance_timing['loadComplete']}ms")
14.23.2 网络性能
# 监听网络请求并计算性能
request_times = {}
def handle_request(request):
request_times[request.url] = {"start": time.time()}
def handle_response(response):
if response.url in request_times:
request_times[response.url]["end"] = time.time()
duration = request_times[response.url]["end"] - request_times[response.url]["start"]
print(f"{response.url}: {duration:.2f}s")
page.on("request", handle_request)
page.on("response", handle_response)
14.23.3 资源加载分析
# 分析资源加载
resources = page.evaluate("""
() => {
return performance.getEntriesByType('resource').map(resource => ({
name: resource.name,
duration: resource.duration,
size: resource.transferSize,
type: resource.initiatorType
}));
}
""")
for resource in resources:
print(f"{resource['name']}: {resource['duration']:.2f}ms, {resource['size']} bytes")
14.24 错误处理和重试机制
14.24.1 自动重试
from playwright.sync_api import TimeoutError
def click_with_retry(page, selector, max_retries=3):
"""带重试的点击操作"""
for attempt in range(max_retries):
try:
page.click(selector, timeout=5000)
return True
except TimeoutError:
if attempt == max_retries - 1:
raise
print(f"重试 {attempt + 1}/{max_retries}")
time.sleep(1)
return False
14.24.2 错误处理最佳实践
from playwright.sync_api import Page, TimeoutError
def safe_operation(page: Page, operation, *args, **kwargs):
"""安全的操作包装器"""
try:
return operation(page, *args, **kwargs)
except TimeoutError as e:
print(f"操作超时: {e}")
# 截图用于调试
page.screenshot(path=f"error_{int(time.time())}.png")
raise
except Exception as e:
print(f"操作失败: {e}")
raise
# 使用
safe_operation(page, lambda p: p.click("button#submit"))
14.25 测试框架集成
14.25.1 与 pytest 集成
import pytest
from playwright.sync_api import Page, expect
@pytest.fixture(scope="session")
def playwright():
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
yield playwright
@pytest.fixture(scope="session")
def browser(playwright):
browser = playwright.chromium.launch(headless=True)
yield browser
browser.close()
@pytest.fixture
def page(browser):
context = browser.new_context()
page = context.new_page()
yield page
context.close()
def test_login(page: Page):
page.goto("https://example.com/login")
page.get_by_placeholder("用户名").fill("testuser")
page.get_by_placeholder("密码").fill("testpass")
page.get_by_role("button", name="登录").click()
expect(page).to_have_url("**/dashboard")
14.25.2 pytest-playwright 插件
pip install pytest-playwright
# conftest.py
import pytest
@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args):
return {
**browser_type_launch_args,
"headless": False,
}
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
return {
**browser_context_args,
"viewport": {"width": 1920, "height": 1080},
}
# test_example.py
def test_example(page):
page.goto("https://example.com")
assert page.title() == "Example"
14.26 CI/CD 集成
14.26.1 GitHub Actions
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install -r requirements.txt
playwright install --with-deps
- name: Run tests
run: pytest
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
14.26.2 Jenkins 集成
pipeline {
agent any
stages {
stage('Install') {
steps {
sh 'pip install -r requirements.txt'
sh 'playwright install --with-deps'
}
}
stage('Test') {
steps {
sh 'pytest --html=report.html'
}
}
stage('Publish') {
steps {
publishHTML([
reportName: 'Playwright Report',
reportDir: 'playwright-report',
reportFiles: 'index.html',
keepAll: true
])
}
}
}
}
14.27 最佳实践和模式
14.27.1 Page Object Model (POM)
class LoginPage:
def __init__(self, page):
self.page = page
self.username_input = page.get_by_placeholder("用户名")
self.password_input = page.get_by_placeholder("密码")
self.login_button = page.get_by_role("button", name="登录")
self.error_message = page.locator(".error-message")
def goto(self):
self.page.goto("https://example.com/login")
def login(self, username: str, password: str):
self.username_input.fill(username)
self.password_input.fill(password)
self.login_button.click()
def get_error_message(self):
return self.error_message.text_content()
# 使用
def test_login(page):
login_page = LoginPage(page)
login_page.goto()
login_page.login("testuser", "testpass")
assert login_page.get_error_message() == ""
14.27.2 组件模式
class Button:
def __init__(self, page, locator):
self.page = page
self.locator = locator
def click(self):
self.locator.click()
def is_enabled(self):
return self.locator.is_enabled()
def get_text(self):
return self.locator.text_content()
class Form:
def __init__(self, page):
self.page = page
self.submit_button = Button(page, page.get_by_role("button", name="提交"))
def fill_field(self, name, value):
self.page.get_by_placeholder(name).fill(value)
def submit(self):
self.submit_button.click()
14.27.3 数据驱动测试
import pytest
import json
@pytest.fixture
def test_data():
with open("test_data.json") as f:
return json.load(f)
@pytest.mark.parametrize("username,password,expected", [
("user1", "pass1", True),
("user2", "wrong", False),
])
def test_login(page, username, password, expected):
page.goto("https://example.com/login")
page.get_by_placeholder("用户名").fill(username)
page.get_by_placeholder("密码").fill(password)
page.get_by_role("button", name="登录").click()
if expected:
expect(page).to_have_url("**/dashboard")
else:
expect(page.locator(".error")).to_be_visible()
14.28 常见问题和解决方案
14.28.1 元素定位失败
问题:元素定位超时
解决方案:
# 1. 增加超时时间
page.locator(".element").click(timeout=30000)
# 2. 使用更稳定的定位方式
# 避免使用 XPath,优先使用 get_by_role
# 3. 等待元素出现
page.wait_for_selector(".element", state="visible")
# 4. 检查元素是否在 iframe 中
iframe = page.frame_locator("iframe")
iframe.locator(".element").click()
14.28.2 点击被遮挡的元素
问题:元素被其他元素遮挡,无法点击
解决方案:
# 1. 使用 force 参数强制点击
page.locator(".element").click(force=True)
# 2. 滚动到元素位置
page.locator(".element").scroll_into_view_if_needed()
# 3. 使用 JavaScript 点击
page.locator(".element").evaluate("el => el.click()")
14.28.3 处理动态内容
问题:内容动态加载,定位不稳定
解决方案:
# 1. 等待内容加载完成
page.wait_for_load_state("networkidle")
# 2. 等待特定元素出现
page.wait_for_selector(".dynamic-content")
# 3. 使用文本定位
page.get_by_text("动态内容").wait_for()
# 4. 等待函数执行
page.wait_for_function("document.querySelector('.content') !== null")
14.28.4 处理反爬虫机制
问题:网站检测到自动化工具
解决方案:
# 1. 使用真实的 User-Agent
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..."
)
# 2. 添加随机延迟
import random
time.sleep(random.uniform(1, 3))
# 3. 模拟人类行为
page.mouse.move(random.randint(100, 200), random.randint(100, 200))
# 4. 使用代理
context = browser.new_context(
proxy={"server": "http://proxy.example.com:8080"}
)
# 5. 禁用自动化检测标志
context = browser.new_context(
# Playwright 会自动处理大部分检测
# 但某些网站可能需要额外配置
)
十五、Playwright 与 Selenium 全面对比
15.1 架构和设计理念对比
15.1.1 架构差异
Selenium 架构:
测试脚本 → WebDriver API → JSON Wire Protocol → 浏览器驱动(ChromeDriver/GeckoDriver) → 浏览器
Playwright 架构:
测试脚本 → Playwright API → Chrome DevTools Protocol / WebSocket → 浏览器(内置)
关键差异:
Selenium:需要单独的浏览器驱动,通过 HTTP 协议通信Playwright:直接与浏览器通信,使用浏览器原生协议(CDP)影响:Playwright 通信更快,延迟更低,但需要浏览器支持 CDP
15.1.2 设计理念
| 方面 | Selenium | Playwright |
|---|---|---|
| 设计目标 | 跨浏览器标准化 | 现代化 Web 应用测试 |
| 等待策略 | 手动配置(显式/隐式等待) | 自动等待(内置智能等待) |
| API 设计 | 命令式(命令-响应) | 声明式(期望-验证) |
| 错误处理 | 抛出异常,需要手动处理 | 自动重试,更好的错误信息 |
15.2 功能特性详细对比
15.2.1 核心功能对比表
| 功能 | Selenium | Playwright | 详细说明 |
|---|---|---|---|
| 自动等待 | ❌ 需要手动配置 | ✅ 内置自动等待 | Playwright 在执行操作前自动等待元素就绪 |
| 网络拦截 | ❌ 不支持 | ✅ 完整支持 | Playwright 可以拦截、修改、模拟网络请求 |
| 视频录制 | ❌ 需要第三方工具 | ✅ 内置支持 | Playwright 内置视频录制,无需额外配置 |
| 代码生成 | ⚠️ Selenium IDE(功能有限) | ✅ Codegen(功能强大) | Playwright Codegen 可以生成高质量代码 |
| 移动端模拟 | ⚠️ 需要额外配置 | ✅ 内置设备预设 | Playwright 内置 50+ 设备预设 |
| 并行执行 | ⚠️ 需要 Selenium Grid | ✅ 原生支持 | Playwright 原生支持多浏览器并行 |
| 调试工具 | ⚠️ 有限 | ✅ 强大的追踪和调试 | Playwright 提供完整的追踪和调试工具 |
| iframe 处理 | ⚠️ 需要手动切换 | ✅ 自动处理 | Playwright 自动处理 iframe,无需手动切换 |
| 多标签页 | ⚠️ 需要手动管理 | ✅ 自动管理 | Playwright 自动管理多个标签页 |
| 截图功能 | ✅ 基础支持 | ✅ 高级支持 | Playwright 支持全屏截图、元素截图 |
| Cookie 管理 | ✅ 支持 | ✅ 支持 | 两者都支持,Playwright API 更简洁 |
| JavaScript 执行 | ✅ 支持 | ✅ 支持 | 两者都支持,Playwright 性能更好 |
| 文件上传 | ✅ 支持 | ✅ 支持 | 两者都支持 |
| 文件下载 | ⚠️ 需要额外处理 | ✅ 内置支持 | Playwright 内置文件下载处理 |
| 拖拽操作 | ✅ 支持 | ✅ 支持 | 两者都支持 |
| 键盘操作 | ✅ 支持 | ✅ 支持 | 两者都支持 |
| 鼠标操作 | ✅ 支持 | ✅ 支持 | 两者都支持 |
15.2.2 高级功能对比
网络请求处理:
Selenium:
# Selenium 无法直接拦截网络请求
# 需要使用代理或其他工具
Playwright:
# Playwright 原生支持网络拦截
def handle_route(route):
if "api/data" in route.request.url:
route.fulfill(json={"data": "mock"})
else:
route.continue_()
page.route("**/*", handle_route)
视频录制:
Selenium:
# 需要使用第三方工具,如 ffmpeg、Monkey Recorder 等
# 配置复杂,需要额外依赖
Playwright:
# 内置支持,配置简单
context = browser.new_context(record_video_dir="videos/")
# 视频自动保存
追踪和调试:
Selenium:
# 有限的调试工具
# 主要依赖日志和截图
driver.save_screenshot("debug.png")
Playwright:
# 完整的追踪系统
context.tracing.start(screenshots=True, snapshots=True)
# ... 执行操作 ...
context.tracing.stop(path="trace.zip")
# 使用 Playwright Inspector 查看完整追踪
15.3 API 详细对比
15.3.1 元素定位对比
Selenium:
from selenium.webdriver.common.by import By
# 8 种定位方式
element = driver.find_element(By.ID, "username")
element = driver.find_element(By.NAME, "username")
element = driver.find_element(By.CLASS_NAME, "btn")
element = driver.find_element(By.TAG_NAME, "input")
element = driver.find_element(By.LINK_TEXT, "登录")
element = driver.find_element(By.PARTIAL_LINK_TEXT, "登")
element = driver.find_element(By.XPATH, "//input[@id='username']")
element = driver.find_element(By.CSS_SELECTOR, "#username")
# 定位多个元素
elements = driver.find_elements(By.CLASS_NAME, "item")
Playwright:
# 语义化定位(推荐)
element = page.get_by_role("textbox", name="用户名")
element = page.get_by_role("button", name="登录")
element = page.get_by_text("点击这里")
element = page.get_by_placeholder("请输入")
element = page.get_by_label("用户名")
# CSS Selector 和 XPath
element = page.locator("#username")
element = page.locator("xpath=//input[@id='username']")
# 定位多个元素
elements = page.locator(".item").all()
# 链式定位
page.locator("div").filter(has_text="登录").click()
对比分析:
Selenium:8 种定位方式,需要导入 类Playwright:语义化定位更稳定,API 更简洁推荐:Playwright 的语义化定位更不容易因页面变化而失效
By
15.3.2 等待机制对比
Selenium:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
# 强制等待(不推荐)
time.sleep(3)
# 隐式等待(全局)
driver.implicitly_wait(10)
# 显式等待(推荐)
wait = WebDriverWait(driver, 10)
element = wait.until(EC.visibility_of_element_located((By.ID, "username")))
# 等待多个条件
wait.until(EC.all_of(
EC.presence_of_element_located((By.ID, "username")),
EC.element_to_be_clickable((By.ID, "submit"))
))
Playwright:
from playwright.sync_api import expect
# 自动等待(内置)
page.get_by_role("textbox", name="用户名").fill("testuser")
# ↑ 自动等待元素可见、可编辑
# 显式等待
page.wait_for_selector(".content", state="visible")
page.wait_for_load_state("networkidle")
page.wait_for_url("**/dashboard")
# 断言等待
expect(page.get_by_role("button", name="提交")).to_be_visible()
expect(page).to_have_url("https://example.com/dashboard")
对比分析:
Selenium:需要手动配置等待,容易遗漏导致 flaky 测试Playwright:自动等待,减少 flaky 测试,代码更简洁优势:Playwright 的自动等待是最大的优势之一
15.3.3 元素操作对比
输入操作:
Selenium:
from selenium.webdriver.common.keys import Keys
element = driver.find_element(By.ID, "username")
element.clear()
element.send_keys("testuser")
element.send_keys(Keys.ENTER)
element.send_keys(Keys.CONTROL, 'a')
Playwright:
page.get_by_placeholder("用户名").fill("testuser")
page.get_by_placeholder("搜索").press("Enter")
page.get_by_placeholder("搜索").press("Control+A")
page.get_by_placeholder("用户名").type("testuser", delay=100) # 模拟打字
点击操作:
Selenium:
from selenium.webdriver.common.action_chains import ActionChains
element = driver.find_element(By.ID, "button")
element.click()
element.double_click() # 需要 ActionChains
ActionChains(driver).context_click(element).perform() # 右键
Playwright:
page.get_by_role("button", name="登录").click()
page.get_by_role("button", name="提交").dblclick()
page.get_by_role("button", name="菜单").click(button="right")
page.get_by_role("button", name="提交").click(force=True) # 强制点击
获取元素信息:
Selenium:
element = driver.find_element(By.ID, "content")
text = element.text
value = element.get_attribute("value")
is_displayed = element.is_displayed()
is_enabled = element.is_enabled()
is_selected = element.is_selected()
Playwright:
element = page.locator("#content")
text = element.text_content()
html = element.inner_html()
value = element.get_attribute("value")
is_visible = element.is_visible()
is_enabled = element.is_enabled()
is_checked = element.is_checked()
15.3.4 页面导航对比
Selenium:
driver.get("https://example.com")
driver.forward()
driver.back()
driver.refresh()
current_url = driver.current_url
title = driver.title
Playwright:
page.goto("https://example.com", wait_until="networkidle")
page.go_forward()
page.go_back()
page.reload()
current_url = page.url
title = page.title()
15.3.5 弹窗处理对比
Selenium:
from selenium.webdriver.common.alert import Alert
# 等待弹窗
wait = WebDriverWait(driver, 10)
alert = wait.until(EC.alert_is_present())
# 处理弹窗
alert_text = alert.text
alert.accept()
alert.dismiss()
alert.send_keys("input")
Playwright:
# 监听弹窗
def handle_dialog(dialog):
print(dialog.message)
dialog.accept()
page.on("dialog", handle_dialog)
# 或者等待弹窗
async with page.expect_dialog() as dialog_info:
page.click("button#trigger")
dialog = await dialog_info.value
dialog.accept()
15.3.6 窗口切换对比
Selenium:
# 获取窗口句柄
current_window = driver.current_window_handle
all_windows = driver.window_handles
# 切换窗口
driver.switch_to.window(window_handle)
# 切换到新窗口
new_window = [w for w in driver.window_handles if w != current_window][0]
driver.switch_to.window(new_window)
# 关闭窗口
driver.close()
driver.switch_to.window(current_window)
Playwright:
# 监听新页面
def handle_new_page(new_page):
new_page.wait_for_load_state()
print(new_page.title())
new_page.close()
page.context.on("page", handle_new_page)
# 或者等待新页面
async with page.context.expect_page() as page_info:
page.click("a[target='_blank']")
new_page = await page_info.value
# 切换到新页面
new_page.bring_to_front()
15.3.7 iframe 处理对比
Selenium:
# 切换到 iframe
driver.switch_to.frame("iframe-id")
driver.switch_to.frame(0) # 通过索引
iframe = driver.find_element(By.ID, "iframe-id")
driver.switch_to.frame(iframe)
# 操作 iframe 内元素
driver.find_element(By.ID, "element").click()
# 切换回主页面
driver.switch_to.default_content()
driver.switch_to.parent_frame() # 切换到父级 iframe
Playwright:
# 定位 iframe(自动处理切换)
iframe = page.frame_locator("iframe[name='myframe']")
# 在 iframe 内操作
iframe.get_by_role("button", name="提交").click()
# 或者通过 URL 定位
iframe = page.frame(url="**/iframe.html")
iframe.locator(".element").click()
# 无需手动切换回主页面
15.4 性能详细对比
15.4.1 执行速度对比
测试场景:打开页面、定位元素、执行操作、获取结果
| 操作 | Selenium | Playwright | 性能提升 | 说明 |
|---|---|---|---|---|
| 页面加载 | ~2-3秒 | ~1-2秒 | 30-50% | Playwright 直接通信,延迟更低 |
| 元素定位 | ~100-200ms | ~50-100ms | 50% | Playwright 定位算法更高效 |
| 元素操作 | ~50-100ms | ~30-50ms | 40% | Playwright 操作更直接 |
| 截图 | ~200-300ms | ~100-200ms | 50% | Playwright 截图更高效 |
| JavaScript 执行 | ~50-100ms | ~20-50ms | 60% | Playwright JS 执行更快 |
| 并行执行 | 需要 Grid,配置复杂 | 原生支持,配置简单 | 显著提升 | Playwright 并行效率更高 |
15.4.2 资源消耗对比
| 资源 | Selenium | Playwright | 说明 |
|---|---|---|---|
| 内存占用 | 较高(需要驱动) | 较低(直接通信) | Playwright 内存占用更少 |
| CPU 占用 | 较高 | 较低 | Playwright CPU 占用更少 |
| 启动时间 | ~2-3秒 | ~1-2秒 | Playwright 启动更快 |
| 稳定性 | 依赖驱动版本 | 内置浏览器,版本一致 | Playwright 更稳定 |
15.4.3 并发性能对比
Selenium:
# 需要 Selenium Grid
# 配置复杂,需要 Hub 和 Node
# 网络延迟影响性能
Playwright:
# 原生支持并发
# 配置简单,性能更好
async def test_concurrent():
async with async_playwright() as playwright:
browser = await playwright.chromium.launch()
tasks = []
for url in urls:
tasks.append(scrape_page(browser, url))
results = await asyncio.gather(*tasks)
性能数据(100 个页面并发访问):
Selenium Grid:~60-90秒Playwright:~20-30秒提升:3倍以上
15.5 稳定性和可靠性对比
15.5.1 Flaky 测试
Selenium:
❌ 需要手动配置等待,容易遗漏❌ 元素定位不稳定(XPath 容易失效)❌ 网络延迟导致超时❌ 浏览器驱动版本不匹配
Playwright:
✅ 自动等待,减少 flaky 测试✅ 语义化定位更稳定✅ 内置重试机制✅ 浏览器版本一致,更稳定
Flaky 测试率对比(基于实际项目数据):
Selenium:~5-10%Playwright:~1-2%改善:减少 80% 的 flaky 测试
15.5.2 错误处理
Selenium:
# 错误信息有限
try:
element = driver.find_element(By.ID, "username")
except NoSuchElementException as e:
print(e) # 错误信息不够详细
Playwright:
# 错误信息更详细
try:
element = page.locator("#username")
except TimeoutError as e:
print(e) # 包含详细的上下文信息
# 自动截图用于调试
15.6 开发体验对比
15.6.1 代码可读性
Selenium:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
wait = WebDriverWait(driver, 10)
username_input = wait.until(EC.visibility_of_element_located((By.ID, "username")))
username_input.clear()
username_input.send_keys("testuser")
password_input = wait.until(EC.visibility_of_element_located((By.ID, "password")))
password_input.clear()
password_input.send_keys("testpass")
submit_button = wait.until(EC.element_to_be_clickable((By.ID, "submit")))
submit_button.click()
Playwright:
from playwright.sync_api import expect
page.get_by_placeholder("用户名").fill("testuser")
page.get_by_placeholder("密码").fill("testpass")
page.get_by_role("button", name="登录").click()
expect(page).to_have_url("**/dashboard")
对比:Playwright 代码更简洁、可读性更好
15.6.2 调试工具
Selenium:
⚠️ 有限的调试工具⚠️ 主要依赖日志和截图⚠️ 需要手动添加调试代码
Playwright:
✅ 强大的追踪系统✅ Playwright Inspector(可视化调试)✅ 自动截图和视频录制✅ 详细的错误信息
15.6.3 代码生成
Selenium:
⚠️ Selenium IDE(功能有限)⚠️ 生成的代码质量一般⚠️ 需要大量手动修改
Playwright:
✅ Codegen(功能强大)✅ 生成高质量代码✅ 支持多种语言✅ 可以保存到文件
15.7 生态系统和社区对比
15.7.1 语言支持
| 语言 | Selenium | Playwright |
|---|---|---|
| Python | ✅ 成熟 | ✅ 成熟 |
| Java | ✅ 非常成熟 | ✅ 支持 |
| JavaScript/TypeScript | ✅ 成熟 | ✅ 原生支持(最佳) |
| C# | ✅ 成熟 | ✅ 支持 |
| Ruby | ✅ 成熟 | ✅ 支持 |
| Go | ❌ 不支持 | ✅ 支持 |
15.7.2 社区和文档
| 方面 | Selenium | Playwright |
|---|---|---|
| 社区规模 | 非常大(历史久) | 快速增长 |
| 文档质量 | 良好 | 优秀(更现代化) |
| 示例代码 | 丰富 | 丰富且更新 |
| Stack Overflow | 问题多,答案多 | 问题增长快 |
| GitHub Stars | ~25k | ~50k+(增长快) |
15.7.3 第三方工具和插件
Selenium:
✅ 丰富的第三方工具✅ 大量插件和扩展✅ 成熟的测试框架集成
Playwright:
✅ 官方工具完善⚠️ 第三方工具较少(但官方工具已足够)✅ 良好的测试框架集成
15.8 学习曲线对比
15.8.1 入门难度
Selenium:
⚠️ 需要理解 WebDriver 协议⚠️ 需要配置浏览器驱动⚠️ 需要学习等待机制⚠️ 需要处理各种异常
Playwright:
✅ API 更直观✅ 自动等待,减少学习成本✅ 更好的错误信息✅ 官方文档优秀
15.8.2 高级功能
Selenium:
⚠️ 高级功能需要额外学习⚠️ 网络拦截需要额外工具⚠️ 视频录制需要额外配置
Playwright:
✅ 高级功能内置✅ API 统一,学习成本低✅ 功能更强大
15.9 实际项目选择建议
15.9.1 选择 Selenium 的场景
✅ 适合选择 Selenium:
已有 Selenium 项目:迁移成本高,继续使用 Selenium需要支持旧版浏览器:Selenium 支持更多浏览器版本团队已有 Selenium 经验:学习成本低使用 Java/C#:Selenium 在这些语言的生态更成熟需要与现有工具集成:Selenium 集成更广泛企业级项目:Selenium 在企业级应用更广泛
❌ 不适合选择 Selenium:
新建项目,追求现代化方案需要网络拦截、视频录制等高级功能需要更好的性能和稳定性团队愿意学习新技术
15.9.2 选择 Playwright 的场景
✅ 适合选择 Playwright:
新建项目:追求现代化方案需要更好的性能:Playwright 性能更好需要高级功能:网络拦截、视频录制等使用 Python/JavaScript:Playwright 在这些语言支持最好需要移动端测试:Playwright 移动端支持更好需要减少 flaky 测试:Playwright 自动等待减少 flaky需要更好的调试工具:Playwright 调试工具更强大
❌ 不适合选择 Playwright:
需要支持非常旧的浏览器版本团队已有大量 Selenium 经验,迁移成本高使用 Java/C# 且团队不熟悉新工具
15.9.3 迁移建议
从 Selenium 迁移到 Playwright:
评估迁移成本:
测试用例数量代码复杂度团队学习成本
逐步迁移:
新功能使用 Playwright旧功能保持 Selenium逐步替换
迁移步骤:
# 1. 安装 Playwright
pip install playwright
playwright install
# 2. 创建 Playwright 版本的 Page Object
class LoginPagePW:
def __init__(self, page):
self.page = page
# ...
# 3. 并行运行,对比结果
# 4. 逐步替换
15.10 总结对比表
| 对比维度 | Selenium | Playwright | 胜者 |
|---|---|---|---|
| 性能 | 较慢 | 更快 | Playwright |
| 稳定性 | 一般 | 更好 | Playwright |
| 自动等待 | 需要手动配置 | 内置自动等待 | Playwright |
| 网络拦截 | 不支持 | 支持 | Playwright |
| 视频录制 | 需要第三方工具 | 内置支持 | Playwright |
| 代码可读性 | 一般 | 更好 | Playwright |
| 调试工具 | 有限 | 强大 | Playwright |
| 学习曲线 | 较陡 | 较平 | Playwright |
| 社区规模 | 非常大 | 快速增长 | Selenium(目前) |
| 浏览器支持 | 更多版本 | 现代浏览器 | Selenium(旧版) |
| 语言支持 | 更多语言 | 主要语言 | Selenium(Java/C#) |
| 企业采用 | 广泛 | 快速增长 | Selenium(目前) |
15.11 最终建议
对于新项目:
✅ 强烈推荐 Playwright:性能更好、功能更强、代码更简洁
对于现有项目:
⚠️ 评估迁移成本:如果成本不高,建议迁移到 Playwright⚠️ 如果成本高:继续使用 Selenium,新功能可以考虑 Playwright
对于团队:
✅ 如果团队愿意学习:选择 Playwright⚠️ 如果团队已有 Selenium 经验:可以继续使用 Selenium,或逐步迁移
结论:
Playwright 是未来的趋势,更适合现代化 Web 应用测试Selenium 仍然可靠,适合需要支持旧版浏览器或已有项目的场景两者可以共存,根据具体需求选择
十六、Playwright 实战案例
16.1 案例一:自动化登录并获取数据
from playwright.sync_api import sync_playwright, expect
def auto_login_and_get_data(username, password):
"""自动化登录并获取数据"""
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
try:
# 1. 访问登录页面
page.goto("https://example.com/login")
# 2. 输入用户名和密码(自动等待)
page.get_by_placeholder("用户名").fill(username)
page.get_by_placeholder("密码").fill(password)
# 3. 点击登录按钮
page.get_by_role("button", name="登录").click()
# 4. 等待登录成功(等待 URL 变化或特定元素出现)
page.wait_for_url("**/dashboard")
expect(page.get_by_text("欢迎")).to_be_visible()
# 5. 获取数据
data_elements = page.locator(".data-item").all()
data = []
for element in data_elements:
data.append({
"title": element.locator(".title").text_content(),
"content": element.locator(".content").text_content()
})
return data
finally:
browser.close()
# 使用示例
data = auto_login_and_get_data("testuser", "testpass")
print(data)
16.2 案例二:爬取动态加载的商品列表
from playwright.sync_api import sync_playwright
import json
def scrape_dynamic_products(url):
"""爬取动态加载的商品列表"""
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page()
try:
page.goto(url)
# 等待商品列表加载
page.wait_for_selector(".product-list")
products = []
page_num = 1
while True:
print(f"正在爬取第 {page_num} 页...")
# 等待商品加载完成
page.wait_for_load_state("networkidle")
# 滚动到底部触发加载
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
page.wait_for_timeout(2000) # 等待 2 秒
# 获取当前页所有商品
product_items = page.locator(".product-item").all()
for item in product_items:
try:
product = {
"name": item.locator(".product-name").text_content(),
"price": item.locator(".product-price").text_content(),
"image": item.locator("img").get_attribute("src"),
"link": item.locator("a").get_attribute("href")
}
products.append(product)
except Exception as e:
print(f"提取商品信息失败:{e}")
continue
# 尝试点击"下一页"
next_button = page.get_by_role("button", name="下一页")
if next_button.is_visible() and next_button.is_enabled():
next_button.click()
page.wait_for_load_state("networkidle")
page_num += 1
else:
break
return products
finally:
browser.close()
# 使用示例
products = scrape_dynamic_products("https://example.com/products")
with open("products.json", "w", encoding="utf-8") as f:
json.dump(products, f, ensure_ascii=False, indent=2)
16.3 案例三:网络请求拦截和验证
from playwright.sync_api import sync_playwright, expect
def test_api_interception():
"""测试 API 请求拦截和验证"""
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
page = browser.new_page()
# 拦截 API 请求
api_requests = []
def handle_request(request):
if "/api/" in request.url:
api_requests.append({
"url": request.url,
"method": request.method,
"headers": request.headers
})
request.continue_()
page.route("**/*", handle_request)
# 监听 API 响应
api_responses = []
def handle_response(response):
if "/api/" in response.url:
api_responses.append({
"url": response.url,
"status": response.status,
"body": response.json()
})
page.on("response", handle_response)
# 执行操作
page.goto("https://example.com")
page.get_by_role("button", name="加载数据").click()
# 等待 API 请求完成
page.wait_for_response("**/api/data")
# 验证请求和响应
assert len(api_requests) > 0
assert len(api_responses) > 0
assert api_responses[0]["status"] == 200
browser.close()
16.4 案例四:多浏览器并行测试
from playwright.sync_api import sync_playwright
def test_multiple_browsers():
"""多浏览器并行测试"""
with sync_playwright() as playwright:
browsers = [
playwright.chromium.launch(),
playwright.firefox.launch(),
playwright.webkit.launch()
]
pages = []
for browser in browsers:
page = browser.new_page()
pages.append(page)
# 并行访问页面
for page in pages:
page.goto("https://example.com")
assert "Example" in page.title()
# 关闭所有浏览器
for browser in browsers:
browser.close()
十七、Playwright 面试题
面试题 1:Playwright 和 Selenium 的主要区别是什么?
答案要点:
架构:
Selenium:WebDriver 协议 → 浏览器驱动 → 浏览器Playwright:直接与浏览器通信(Chrome DevTools Protocol) 自动等待:
Selenium:需要手动配置显式/隐式等待Playwright:内置自动等待,操作前自动等待元素就绪 性能:
Selenium:较慢(需要驱动中转)Playwright:更快(直接通信) 功能:
Playwright 提供网络拦截、视频录制、代码生成等 Selenium 没有的功能 稳定性:
Playwright 内置浏览器,版本一致,更稳定
面试题 2:Playwright 的自动等待机制是如何工作的?
答案要点:
工作原理:Playwright 在执行操作前会自动等待元素满足特定条件等待条件:
:等待元素可见、可点击
click():等待元素可见、可编辑
fill():等待元素附加到 DOM 超时设置:默认 30 秒,可通过
text_content() 参数修改优势:减少 flaky 测试,无需手动等待显式等待:仍可使用
timeout 进行显式等待
wait_for()
示例代码:
# 自动等待元素可见、可点击
page.get_by_role("button", name="提交").click()
# 自动等待元素可见、可编辑
page.get_by_placeholder("用户名").fill("testuser")
面试题 3:Playwright 支持哪些定位方式?
答案要点:
推荐方式(按优先级):
:通过角色定位(最稳定)
get_by_role():通过文本定位
get_by_text():通过占位符定位
get_by_placeholder():通过标签定位 备选方式:
get_by_label()
:CSS Selector、XPath 定位策略:优先使用语义化定位,避免使用 XPath
locator()
示例代码:
# 推荐:角色定位
page.get_by_role("button", name="登录")
# 推荐:文本定位
page.get_by_text("点击这里")
# 备选:CSS Selector
page.locator("#username")
面试题 4:Playwright 如何处理动态加载的内容?
答案要点:
自动等待:内置自动等待机制,自动等待元素加载完成显式等待:
:等待选择器出现
wait_for_selector():等待页面加载状态
wait_for_load_state():等待 URL 变化 网络等待:
wait_for_url() 等待网络空闲无限滚动:使用
wait_for_load_state("networkidle") 执行 JavaScript 滚动
evaluate()
示例代码:
# 自动等待元素出现
page.get_by_text("加载完成").click()
# 等待网络空闲
page.wait_for_load_state("networkidle")
# 等待 URL 变化
page.wait_for_url("**/dashboard")
面试题 5:Playwright 如何实现网络请求拦截?
答案要点:
拦截请求:使用 拦截网络请求修改请求:可以修改 URL、请求头、请求体模拟响应:可以返回模拟响应,不发送真实请求监听响应:使用
page.route() 监听响应应用场景:API 测试、Mock 数据、性能测试
page.on("response")
示例代码:
# 拦截请求
def handle_route(route):
if "api/data" in route.request.url:
route.fulfill(json={"data": "mock data"})
else:
route.continue_()
page.route("**/*", handle_route)
# 监听响应
page.on("response", lambda response: print(response.status))
面试题 6:Playwright 如何实现并行测试?
答案要点:
原生支持:Playwright 原生支持并行测试,无需额外配置多浏览器:可以同时启动多个浏览器实例多上下文:一个浏览器可以创建多个上下文(Context)多页面:一个上下文可以创建多个页面(Page)隔离性:每个上下文和页面都是隔离的
示例代码:
# 创建多个上下文并行测试
context1 = browser.new_context()
context2 = browser.new_context()
page1 = context1.new_page()
page2 = context2.new_page()
# 并行执行
page1.goto("https://example.com")
page2.goto("https://example.com")
面试题 7:Playwright 如何处理 iframe?
答案要点:
定位 iframe:使用 定位 iframe在 iframe 内操作:直接在 frame_locator 上操作元素自动切换:Playwright 自动处理 iframe 切换,无需手动切换多层 iframe:支持嵌套 iframe
page.frame_locator()
示例代码:
# 定位 iframe
iframe = page.frame_locator("iframe[name='myframe']")
# 在 iframe 内操作
iframe.get_by_role("button", name="提交").click()
面试题 8:Playwright 的 Codegen 工具如何使用?
答案要点:
启动方式:功能:录制操作并生成测试代码支持语言:Python、JavaScript、TypeScript、Java、C#输出格式:可以保存到文件优势:快速生成测试代码,学习 Playwright API
playwright codegen <url>
使用示例:
# 启动代码生成器
playwright codegen https://example.com
# 指定浏览器和输出文件
playwright codegen --browser firefox --target python -o test.py https://example.com
面试题 9:Playwright 如何实现移动端测试?
答案要点:
设备预设:使用 获取设备预设自定义设备:可以自定义视口、User-Agent 等触摸操作:支持触摸事件模拟地理位置:可以模拟地理位置权限:可以模拟权限请求
playwright.devices
示例代码:
# 使用设备预设
iphone = playwright.devices["iPhone 12"]
context = browser.new_context(**iphone)
# 自定义设备
context = browser.new_context(
viewport={"width": 375, "height": 667},
user_agent="Mozilla/5.0 (iPhone...)"
)
面试题 10:Playwright 的追踪(Tracing)功能是什么?
答案要点:
定义:记录测试执行过程,包括截图、网络请求、DOM 快照等启用方式:保存追踪:
context.tracing.start()查看追踪:使用
context.tracing.stop(path="trace.zip")应用场景:调试失败的测试、性能分析
playwright show-trace trace.zip
示例代码:
context.tracing.start(screenshots=True, snapshots=True)
# ... 执行测试 ...
context.tracing.stop(path="trace.zip")
面试题 11:Playwright 如何处理文件上传和下载?
答案要点:
文件上传:使用 方法多文件上传:传入文件列表文件下载:使用
set_input_files() 等待下载保存文件:使用
expect_download() 保存
download.save_as()
示例代码:
# 文件上传
page.get_by_label("选择文件").set_input_files("/path/to/file.txt")
# 文件下载
with page.expect_download() as download_info:
page.get_by_role("link", name="下载").click()
download = download_info.value
download.save_as("/path/to/save/file.txt")
面试题 12:Playwright 如何实现截图和视频录制?
答案要点:
页面截图:元素截图:
page.screenshot(path="screenshot.png")全屏截图:
element.screenshot(path="element.png") 参数视频录制:在浏览器上下文中启用
full_page=True自动保存:视频在上下文关闭时自动保存
record_video_dir
示例代码:
# 截图
page.screenshot(path="screenshot.png", full_page=True)
# 视频录制
context = browser.new_context(record_video_dir="videos/")
page = context.new_page()
# ... 执行操作 ...
context.close() # 视频自动保存
面试题 13:Playwright 的断言(Assertions)如何使用?
答案要点:
使用 :Playwright 提供
expect() API 进行断言自动等待:断言会自动等待条件满足常用断言:
expect
:元素可见
to_be_visible():文本内容
to_have_text():输入值
to_have_value():URL
to_have_url():标题 软断言:使用
to_have_title() 设置超时
expect().to_be_visible(timeout=5000)
示例代码:
from playwright.sync_api import expect
expect(page.get_by_role("button", name="提交")).to_be_visible()
expect(page.get_by_role("heading")).to_have_text("欢迎")
expect(page).to_have_url("https://example.com/dashboard")
面试题 14:Playwright 如何处理 Cookie?
答案要点:
获取 Cookie:添加 Cookie:
context.cookies()清除 Cookie:
context.add_cookies([cookie])应用场景:登录状态保持、测试不同用户角色
context.clear_cookies()
示例代码:
# 获取 Cookie
cookies = context.cookies()
# 添加 Cookie
context.add_cookies([{
"name": "session_id",
"value": "abc123",
"domain": ".example.com",
"path": "/"
}])
面试题 15:Playwright 和 Cypress 的区别?
答案要点:
| 特性 | Playwright | Cypress |
|---|---|---|
| 浏览器支持 | Chromium、Firefox、WebKit | 主要是 Chromium |
| 架构 | 外部控制浏览器 | 在浏览器内运行 |
| 并行执行 | 原生支持 | 需要额外配置 |
| 网络拦截 | 支持 | 支持 |
| 移动端 | 支持 | 有限支持 |
| 语言支持 | Python、JS、TS、Java、C# | 主要是 JavaScript |
面试题 16:Playwright 如何处理验证码?
答案要点:
OCR 识别:使用 Tesseract、pytesseract第三方服务:使用打码平台 API测试环境:设置验证码为固定值或跳过Cookie 绕过:登录后保存 Cookie,下次直接使用人工处理:暂停等待人工输入()
page.pause()
示例代码:
# 暂停等待人工输入
page.pause()
# 或使用 OCR
import pytesseract
from PIL import Image
captcha_image = page.locator("#captcha-image").screenshot()
text = pytesseract.image_to_string(Image.open(captcha_image))
面试题 17:Playwright 如何实现数据驱动测试?
答案要点:
使用参数化:pytest 的 从文件读取:CSV、JSON、Excel 等使用 Fixture:pytest 的 Fixture 提供测试数据数据库驱动:从数据库读取测试数据
@pytest.mark.parametrize
示例代码:
import pytest
@pytest.mark.parametrize("username,password", [
("user1", "pass1"),
("user2", "pass2"),
])
def test_login(page, username, password):
page.goto("https://example.com/login")
page.get_by_placeholder("用户名").fill(username)
page.get_by_placeholder("密码").fill(password)
page.get_by_role("button", name="登录").click()
面试题 18:Playwright 如何实现 Page Object Model(POM)?
答案要点:
定义:将页面元素和操作封装成类优势:代码复用、易于维护、测试逻辑清晰实现:创建页面类,封装元素定位和操作
示例代码:
class LoginPage:
def __init__(self, page):
self.page = page
self.username_input = page.get_by_placeholder("用户名")
self.password_input = page.get_by_placeholder("密码")
self.login_button = page.get_by_role("button", name="登录")
def login(self, username, password):
self.username_input.fill(username)
self.password_input.fill(password)
self.login_button.click()
def goto(self):
self.page.goto("https://example.com/login")
面试题 19:Playwright 的性能优化技巧有哪些?
答案要点:
无头模式: 提高执行速度并行执行:使用多个上下文并行执行减少等待:合理设置超时时间禁用资源:禁用图片、CSS 等不必要的资源复用浏览器:复用浏览器实例而非每次创建网络优化:使用网络拦截减少请求
headless=True
示例代码:
# 禁用图片和 CSS
context = browser.new_context(
viewport={"width": 1920, "height": 1080}
)
# 拦截资源
def handle_route(route):
if route.request.resource_type in ["image", "stylesheet"]:
route.abort()
else:
route.continue_()
page.route("**/*", handle_route)
面试题 20:什么时候选择 Playwright,什么时候选择 Selenium?
答案要点:
选择 Playwright:
新建项目,追求现代化方案需要更好的性能和稳定性需要网络拦截、视频录制等高级功能使用 Python、JavaScript/TypeScript需要移动端测试团队愿意学习新技术
选择 Selenium:
需要支持旧版浏览器团队已有 Selenium 经验需要与现有 Selenium 测试集成使用 Java、C# 等语言(Playwright 支持但生态不如 Selenium)项目已经使用 Selenium,迁移成本高
总结
本文档详细介绍了 Selenium 和 Playwright 两个主流的 Web 自动化测试框架:
Selenium 部分:
元素定位:8 种定位方式及其优先级元素操作:输入、点击、获取信息等常用操作等待机制:显式等待、隐式等待、强制等待的区别和使用弹窗处理:Alert、Confirm、Prompt 和自定义弹窗窗口切换:多窗口场景的处理方法iframe 处理:单层和多层 iframe 的处理动态数据爬取:无限滚动、AJAX、SPA 等场景的处理高级功能:Cookie、JavaScript、截图等实战案例:实际项目中的应用面试题:20 道常见面试问题和答案
Playwright 部分:
核心特性:自动等待、网络拦截、代码生成等元素定位:推荐使用语义化定位方式自动等待:内置智能等待机制,减少 flaky 测试网络功能:请求拦截、响应监听、API 测试高级功能:视频录制、追踪调试、移动端测试与 Selenium 对比:架构、性能、功能等方面的差异实战案例:实际项目中的应用面试题:20 道常见面试问题和答案
选择建议:
Selenium:适合已有 Selenium 经验、需要支持旧版浏览器、使用 Java/C# 的项目Playwright:适合新建项目、追求现代化方案、需要高级功能的项目
掌握这两个框架的知识点,可以应对大部分 Web 自动化测试和 Web 爬虫的需求。在实际项目中,要根据具体情况选择合适的方法,并注意代码的可维护性和性能优化。
本文档详细介绍了 Selenium 自动化测试的各个方面:
元素定位:8 种定位方式及其优先级元素操作:输入、点击、获取信息等常用操作等待机制:显式等待、隐式等待、强制等待的区别和使用弹窗处理:Alert、Confirm、Prompt 和自定义弹窗窗口切换:多窗口场景的处理方法iframe 处理:单层和多层 iframe 的处理动态数据爬取:无限滚动、AJAX、SPA 等场景的处理高级功能:Cookie、JavaScript、截图等实战案例:实际项目中的应用面试题:常见面试问题和答案
掌握这些知识点,可以应对大部分 Selenium 自动化测试和 Web 爬虫的需求。在实际项目中,要根据具体情况选择合适的方法,并注意代码的可维护性和性能优化。
















暂无评论内容