Selenium、playwright

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 版本:
chrome://version/
下载对应版本的 ChromeDriver:https://chromedriver.chromium.org/将驱动放到 PATH 环境变量中

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()
,使用
WebDriverWait
检查元素状态:确保元素可见、可点击后再操作处理加载指示器:等待加载完成后再提取数据处理异常:使用 try-except 处理元素不存在的情况优化性能:减少不必要的等待,合理设置超时时间处理反爬虫:添加 User-Agent、使用代理、控制请求频率


十、其他高级功能

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:显式等待和隐式等待的区别?

答案要点

特性 隐式等待 显式等待
设置方式
driver.implicitly_wait(10)

WebDriverWait(driver, 10)
作用范围 全局,对所有元素查找生效 局部,针对特定条件
等待条件 只能等待元素存在 可以等待多种条件(可见、可点击等)
灵活性 较低 较高
性能 如果元素不存在,必须等到超时 条件满足立即返回
推荐使用 作为兜底 关键操作使用

面试题 4:如何处理动态加载的元素?

答案要点

使用显式等待
WebDriverWait
+
expected_conditions
等待元素出现
EC.presence_of_element_located()
等待元素可见
EC.visibility_of_element_located()
等待 AJAX 完成:检查加载指示器消失等待特定条件:文本出现、元素可点击等避免使用
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()
切换方式
通过索引:
driver.switch_to.frame(0)
通过 name/id:
driver.switch_to.frame("iframe-id")
通过 WebElement:
driver.switch_to.frame(iframe_element)
切换回主页面
driver.switch_to.default_content()
多层 iframe:使用
parent_frame()
逐层返回最佳实践:使用显式等待确保 iframe 加载完成

面试题 6:如何处理弹窗?

答案要点

Alert 弹窗
driver.switch_to.alert.accept()
Confirm 弹窗
alert.accept()

alert.dismiss()
Prompt 弹窗
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 的执行速度?

答案要点

使用无头模式
--headless
禁用图片和 CSS:减少资源加载设置页面加载策略
page_load_strategy = "eager"
使用显式等待而非强制等待:避免不必要的等待合理设置超时时间:不要设置过长的等待时间使用 CSS Selector 而非 XPath:CSS Selector 性能更好并行执行:使用 Selenium Grid 或 pytest-xdist

面试题 11:如何处理反爬虫机制?

答案要点

设置 User-Agent:模拟真实浏览器使用代理:轮换 IP 地址控制请求频率:添加延迟,避免请求过快处理验证码:使用 OCR 或第三方服务使用 Cookie:保持会话状态模拟人类行为:随机延迟、鼠标移动等

示例代码


options = Options()
options.add_argument("--user-agent=Mozilla/5.0...")
driver = webdriver.Chrome(options=options)

面试题 12:Selenium 中如何处理下拉框?

答案要点

使用 Select 类
from selenium.webdriver.support.ui import Select
创建 Select 对象
select = Select(element)
选择方式

select_by_visible_text()
:通过可见文本
select_by_value()
:通过 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?

答案要点

使用
execute_script()
:执行 JavaScript 代码返回值:可以获取 JavaScript 执行的结果传递参数:通过
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 的
@pytest.mark.parametrize
从文件读取数据:CSV、JSON、Excel 等使用 Fixture:pytest 的 Fixture 提供测试数据数据库驱动:从数据库读取测试数据

示例代码


@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 种定位方式,需要导入
By
Playwright:语义化定位更稳定,API 更简洁推荐:Playwright 的语义化定位更不容易因页面变化而失效

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()
:等待元素可见、可编辑
text_content()
:等待元素附加到 DOM 超时设置:默认 30 秒,可通过
timeout
参数修改优势:减少 flaky 测试,无需手动等待显式等待:仍可使用
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()
:通过标签定位 备选方式

locator()
:CSS Selector、XPath 定位策略:优先使用语义化定位,避免使用 XPath

示例代码


# 推荐:角色定位
page.get_by_role("button", name="登录")

# 推荐:文本定位
page.get_by_text("点击这里")

# 备选:CSS Selector
page.locator("#username")

面试题 4:Playwright 如何处理动态加载的内容?

答案要点

自动等待:内置自动等待机制,自动等待元素加载完成显式等待

wait_for_selector()
:等待选择器出现
wait_for_load_state()
:等待页面加载状态
wait_for_url()
:等待 URL 变化 网络等待
wait_for_load_state("networkidle")
等待网络空闲无限滚动:使用
evaluate()
执行 JavaScript 滚动

示例代码


# 自动等待元素出现
page.get_by_text("加载完成").click()

# 等待网络空闲
page.wait_for_load_state("networkidle")

# 等待 URL 变化
page.wait_for_url("**/dashboard")

面试题 5:Playwright 如何实现网络请求拦截?

答案要点

拦截请求:使用
page.route()
拦截网络请求修改请求:可以修改 URL、请求头、请求体模拟响应:可以返回模拟响应,不发送真实请求监听响应:使用
page.on("response")
监听响应应用场景:API 测试、Mock 数据、性能测试

示例代码


# 拦截请求
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:使用
page.frame_locator()
定位 iframe在 iframe 内操作:直接在 frame_locator 上操作元素自动切换:Playwright 自动处理 iframe 切换,无需手动切换多层 iframe:支持嵌套 iframe

示例代码


# 定位 iframe
iframe = page.frame_locator("iframe[name='myframe']")

# 在 iframe 内操作
iframe.get_by_role("button", name="提交").click()

面试题 8:Playwright 的 Codegen 工具如何使用?

答案要点

启动方式
playwright codegen <url>
功能:录制操作并生成测试代码支持语言:Python、JavaScript、TypeScript、Java、C#输出格式:可以保存到文件优势:快速生成测试代码,学习 Playwright API

使用示例


# 启动代码生成器
playwright codegen https://example.com

# 指定浏览器和输出文件
playwright codegen --browser firefox --target python -o test.py https://example.com

面试题 9:Playwright 如何实现移动端测试?

答案要点

设备预设:使用
playwright.devices
获取设备预设自定义设备:可以自定义视口、User-Agent 等触摸操作:支持触摸事件模拟地理位置:可以模拟地理位置权限:可以模拟权限请求

示例代码


# 使用设备预设
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)如何使用?

答案要点

使用
expect()
:Playwright 提供
expect
API 进行断言自动等待:断言会自动等待条件满足常用断言

to_be_visible()
:元素可见
to_have_text()
:文本内容
to_have_value()
:输入值
to_have_url()
: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
context.cookies()
添加 Cookie
context.add_cookies([cookie])
清除 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 的
@pytest.mark.parametrize
从文件读取:CSV、JSON、Excel 等使用 Fixture:pytest 的 Fixture 提供测试数据数据库驱动:从数据库读取测试数据

示例代码


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 的性能优化技巧有哪些?

答案要点

无头模式
headless=True
提高执行速度并行执行:使用多个上下文并行执行减少等待:合理设置超时时间禁用资源:禁用图片、CSS 等不必要的资源复用浏览器:复用浏览器实例而非每次创建网络优化:使用网络拦截减少请求

示例代码


# 禁用图片和 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 爬虫的需求。在实际项目中,要根据具体情况选择合适的方法,并注意代码的可维护性和性能优化。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容