一、基础夯实

1.1 自动化测试与 Selenium 概述

1.1.1 自动化测试的价值与应用场景

自动化测试作为软件测试的重要分支,核心价值体现在效率提升质量保障成本优化三个维度。相较于手动测试,自动化测试可通过脚本重复执行繁琐用例,将回归测试周期从数天缩短至数小时,尤其适合迭代频繁的敏捷开发场景;同时,它能精准捕捉人为易忽略的边界值错误(如高并发下的数据一致性问题),并通过日志留存完整测试轨迹,为问题定位提供可追溯依据。从成本角度看,自动化测试虽前期需投入脚本开发时间,但长期可大幅降低重复测试的人力成本,据行业数据统计,成熟自动化体系可减少 60% 以上的回归测试人力投入。

其典型应用场景集中在三类场景:一是回归测试,针对软件迭代中核心功能(如电商支付流程、金融交易逻辑)的重复验证,确保旧功能不受新代码影响;二是性能测试,通过自动化工具模拟千级、万级用户并发(如 JMeter 结合 Selenium 模拟浏览器端高并发),检测系统响应时间与稳定性;三是跨环境兼容性测试,在不同浏览器(Chrome、Firefox、Safari)、操作系统(Windows、macOS、Linux)及分辨率下,自动化执行 UI 渲染与交互验证,解决手动测试覆盖不全的问题。

1.1.2 Selenium 生态体系

Selenium 作为开源 Web 自动化测试框架,其生态体系以 “工具链协同” 为核心,三大组件分工明确且无缝衔接:

  • Selenium IDE:定位为 “自动化脚本快速生成工具”,以浏览器插件形式(支持 Chrome、Firefox)运行,通过可视化录制(记录用户在浏览器的点击、输入等操作)自动生成测试脚本(支持 Python、Java、C# 等语言),无需手动编写代码。它的核心优势是降低入门门槛,适合测试新手快速搭建基础用例,同时支持脚本导出与后续优化,是自动化测试的 “入门工具”。
  • Selenium WebDriver:生态体系的 “核心执行组件”,通过与浏览器原生 API 直接交互(而非模拟 JS 注入),实现对浏览器的无侵入控制,支持页面元素定位(ID、XPath、CSS Selector 等)、事件触发(点击、输入、下拉选择)、页面切换(窗口、iframe)等核心操作。相较于早期的 Selenium RC,WebDriver 解决了 “浏览器兼容性差”“执行速度慢” 等问题,且支持多语言绑定,可与 JUnit、TestNG 等测试框架集成,构建完整的自动化测试流程。
  • Selenium Grid:专注于 “分布式测试调度”,通过搭建 “Hub-Node” 架构实现测试任务的分布式执行 ——Hub 作为调度中心,接收测试用例请求并分配至不同的 Node 节点(Node 节点可部署在不同操作系统、不同浏览器环境),从而实现 “并行执行多用例”“跨环境批量验证”。例如,在电商平台大促前,可通过 Grid 同时在 Windows-Chrome、macOS-Safari、Linux-Firefox 等环境下执行支付流程测试,大幅缩短测试周期,是大规模自动化测试的 “关键支撑组件”。

1.1.3 Selenium 重要的新特性解析

近年来 Selenium 的版本迭代(如 Selenium 4.0 及后续版本)围绕 “稳定性提升”“功能扩展”“开发体验优化” 三大方向,推出多个关键新特性:

  1. WebDriver Bidi(双向通信):突破传统 WebDriver “单向指令” 的局限,实现浏览器与测试脚本的双向数据交互。例如,可通过 Bidi 监听浏览器的网络请求(如 API 接口调用、资源加载)、JavaScript 日志输出、DOM 结构变化等,无需依赖第三方工具(如 Fiddler、Chrome DevTools)即可完成接口与 UI 的联动测试。典型应用场景:在测试电商商品详情页时,可通过 Bidi 监听 “获取商品库存” 的 API 请求,验证返回数据与页面显示库存的一致性,解决 “UI 显示正常但接口数据异常” 的隐性问题。
  2. 相对定位(Relative Locators):弥补传统绝对定位(如 XPath、ID)在 “元素动态变化” 场景下的不足,支持通过 “相邻元素关系” 定位目标元素,提供 5 种相对定位方法:above(上方)、below(下方)、toLeftOf(左侧)、toRightOf(右侧)、near(附近,默认距离 100px 内)。例如,当测试页面中 “提交按钮” 的 ID 随版本迭代变化,但始终位于 “用户名输入框” 的下方时,可通过 driver.findElement(withTagName("button").below(withId("username")))定位按钮,提升脚本的可维护性。
  3. 原生支持 Chrome DevTools Protocol(CDP):Selenium 4.0 及以上版本内置 CDP 接口,可直接调用 Chrome DevTools 的核心功能,如模拟网络限速(测试弱网环境下的页面加载)、设置 Cookie(绕过登录验证)、截取全页面截图(而非传统的可视区域截图)等。例如,在测试视频网站时,通过 driver.executeCdpCommand("Network.emulateNetworkConditions", params)模拟 2G 网络,验证视频加载的缓冲提示是否正常显示,无需额外集成 Chrome DevTools 相关依赖。
  4. Grid 重构(Selenium Grid 4):对原有 Grid 架构进行全面优化,支持 “无 Hub 模式”(通过 Docker 容器部署独立 Node,无需单独搭建 Hub)、动态节点注册(Node 可自动向 Hub 上报环境信息)、Web UI 监控面板(可视化查看节点状态、用例执行进度)。此外,Grid 4 支持与 Docker Compose 集成,可通过配置文件快速部署多环境节点集群,简化分布式测试的搭建流程,降低运维成本。

1.2 环境搭建

1.2.1 Selenium 库安装与 WebDriver 管理

在 Python 环境中安装 Selenium 库操作简洁,首先确保已正确安装 Python(建议 3.10 及以上版本)并配置好环境变量。打开命令行终端,直接执行 pip install selenium命令,即可完成最新稳定版 Selenium 的安装。若项目需指定特定版本,可在命令后追加版本号,例如 pip install selenium==4.24.0,满足不同项目的版本兼容性需求。

WebDriver 管理曾是 Python 自动化测试环境搭建的难点,传统方式需手动匹配浏览器版本下载对应驱动,还需配置系统环境变量,极易出现版本不兼容问题。而Selenium 4.6 及以上版本内置的 Selenium Manager,为 Python 用户提供了自动化解决方案,具体最佳实践如下:

  • 无额外配置启动:在 Python 脚本中,只需简单导入 webdriver 模块并初始化浏览器,如 from selenium import webdriver; driver = webdriver.Chrome(),Selenium Manager 会在后台自动检测本地 Chrome 浏览器的版本,下载匹配的 ChromeDriver,无需手动设置 webdriver.chrome.driver系统变量,极大简化了初始化流程。
  • 自定义驱动路径:若需将驱动文件统一存放在指定目录(如项目下的 drivers 文件夹),可通过 webdriver.ChromeService类指定驱动路径。示例代码为 from selenium.webdriver.chrome.service import Service; service = Service(executable_path="D:/project/drivers/chromedriver.exe"); driver = webdriver.Chrome(service=service)。此时 Selenium Manager 会优先使用指定路径下的驱动,若检测到驱动与浏览器版本不匹配,会自动更新该路径下的驱动文件。
  • 离线环境适配:针对无网络环境,可先在联网机器上通过命令行执行 selenium-manager --browser chrome(若需适配 Firefox 或 Edge,将 chrome替换为 firefoxedge),Selenium Manager 会自动下载对应浏览器的最新驱动。将下载好的驱动文件拷贝至离线环境的指定路径,再通过上述自定义驱动路径的方式在 Python 脚本中调用,即可解决离线环境下驱动缺失的问题。

1.2.2 浏览器驱动配置

  • Chrome 浏览器:首先打开 Chrome 浏览器,通过 “设置 - 关于 Chrome” 查看浏览器版本(如 140.0.7339.186)。前往Chrome Drive 官网,下载与浏览器主版本号匹配的驱动。Windows 系统下,将下载的 chromedriver.exe文件放入 Python 的安装目录(如 C:\Python310),或添加该驱动所在路径到系统环境变量 “PATH” 中;Linux/macOS 系统则需将驱动文件放入 /usr/local/bin目录,并通过 chmod +x /usr/local/bin/chromedriver命令赋予执行权限。配置完成后,在 Python 脚本中执行 driver = webdriver.Chrome(),若浏览器能正常启动,说明配置成功;也可在命令行执行 chromedriver --version,验证驱动版本与浏览器版本是否匹配。
  • Firefox 浏览器:打开 Firefox 浏览器,通过 “帮助 - 关于 Firefox” 查看版本(如 138.0)。前往GeckoDriver 官网,下载对应操作系统的 GeckoDriver。Windows 系统将 geckodriver.exe加入系统环境变量 “PATH”;Linux/macOS 系统将驱动放入 /usr/local/bin并赋予执行权限(chmod +x /usr/local/bin/geckodriver)。Python 脚本中初始化浏览器的代码为 driver = webdriver.Firefox(),若能成功启动 Firefox,配置即完成。GeckoDriver 对 Firefox 版本兼容性较高,通常最新版 GeckoDriver 可支持近 3 个主版本的 Firefox。
  • Edge 浏览器:Edge 基于 Chromium 内核,驱动配置与 Chrome 类似。打开 Edge 浏览器,通过 “设置 - 关于 Microsoft Edge” 查看版本(如 140.0.3485.81)。前往EdgeDrive 官网,下载对应版本的 EdgeDriver。Windows 系统将 msedgedriver.exe加入系统环境变量 “PATH”;Linux/macOS 系统将驱动放入 /usr/local/bin并赋予执行权限(chmod +x /usr/local/bin/msedgedriver)。Python 脚本中通过 driver = webdriver.Edge()初始化浏览器,验证是否能正常启动,也可通过 msedgedriver --version命令检查驱动版本。

二、核心技能

2.1 WebDriver 基础操作详解

2.1.1 浏览器控制

2.1.1.1 窗口管理

WebDriver 提供了丰富的窗口管理方法,包括设置窗口大小、位置和切换窗口等:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options

# 浏览器可执行文件的本地路径
browser_path = "D:/Software/Chrome/chrome.exe"
# WebDriver的本地路径
webdriver_path = "D:/Software/Chrome/chromedriver-win64/chromedriver.exe"
# 创建浏览器选项对象
chrome_options = Options()
# 设置浏览器路径
chrome_options.binary_location = browser_path
# 解决沙箱问题
chrome_options.add_argument("--no-sandbox")
# 创建服务对象,指定WebDriver路径
service = Service(executable_path=webdriver_path)
# 初始化WebDriver时传入服务和选项
driver = webdriver.Chrome(service=service, options=chrome_options)

# 打开网页
driver.get("https://www.baidu.com")

# 窗口最大化
driver.maximize_window()

# 设置窗口位置(屏幕左上角为原点)
driver.set_window_position(100, 100)  # x=100, y=100

# 获取窗口信息
print("窗口大小:", driver.get_window_size())
print("窗口位置:", driver.get_window_position())
print("当前窗口句柄:", driver.current_window_handle)
print("所有窗口句柄:", driver.window_handles)

# 关闭当前窗口
driver.close()

# 退出浏览器(关闭所有窗口)
driver.quit()

2.1.1.2 导航操作

# 前进、后退和刷新
driver.get("https://www.baidu.com")
driver.get("https://www.bilibili.com/")

driver.back()  # 后退到baidu
driver.forward()  # 前进到bilibili
driver.refresh()  # 刷新当前页面

# 获取当前URL和标题
print("当前URL:", driver.current_url)
print("页面标题:", driver.title)

2.1.1.3 截图操作

截图是测试中常用的功能,可用于记录测试结果或错误状态:

# 前进、后退和刷新
driver.get("https://www.baidu.com")

# 截取当前窗口并保存
driver.get_screenshot_as_file("screenshot.png")

# 截取元素并保存
element = driver.find_element(By.ID, "lg")
element.screenshot("element_screenshot.png")

# 获取截图的二进制数据
screenshot_bytes = driver.get_screenshot_as_png()

2.1.2 元素定位策略

WebDriver 提供了多种元素定位方法,以下是 8 大原生方法及 CSS 高级选择器的使用:

from selenium.webdriver.common.by import By

driver.get("https://www.bilibili.com/")

# 1. ID定位 - 最精准的定位方式
element_by_id = driver.find_element(By.ID, "nav-searchform")
# 2. Name定位 - 通过元素的name属性
element_by_name = driver.find_element(By.NAME, "description")
# 3. Class Name定位 - 通过元素的class属性
element_by_class = driver.find_element(By.CLASS_NAME, "nav-search-btn")
# 4. Tag Name定位 - 通过HTML标签名
element_by_tag = driver.find_element(By.TAG_NAME, "link")
# 5. Link Text定位 - 通过链接的完整文本
element_by_link_text = driver.find_element(By.LINK_TEXT, "番剧")
# 6. Partial Link Text定位 - 通过链接的部分文本
element_by_partial_link = driver.find_element(By.PARTIAL_LINK_TEXT, "游")

# 7. XPath定位 - 功能强大的定位方式
# 绝对路径(不推荐)
element_by_xpath_absolute = driver.find_element(By.XPATH, "/html/body/div[2]")
# 相对路径(推荐)
element_by_xpath_relative = driver.find_element(By.XPATH,
                                                "//div[@class='center-search-container offset-center-search']//div[1]")
# 包含文本
element_by_xpath_text = driver.find_element(By.XPATH, "//*[contains(text(), '鬼畜')]")

# 8. CSS Selector定位 - 高效的定位方式
element_by_css = driver.find_element(By.CSS_SELECTOR,
                                     "#i_cecream > div.bili-feed4 > div.bili-header.large-header > div.bili-header__bar > div")

# CSS高级选择器示例
# 1. 属性选择器
css_attr = driver.find_element(By.CSS_SELECTOR, "input[type='text']")  # 特定属性值
css_attr_contains = driver.find_element(By.CSS_SELECTOR, "a[href*='documentary']")  # 属性包含值
css_attr_starts = driver.find_element(By.CSS_SELECTOR, "a[href^='https']")  # 属性以值开头
# 2. 层级选择器
css_child = driver.find_element(By.CSS_SELECTOR, "ul > li:first-child")  # 直接子元素
css_descendant = driver.find_element(By.CSS_SELECTOR, "div.container p")  # 后代元素
# 3. 伪类选择器
driver.execute_script("arguments[0].focus();",
                      driver.find_element(By.CLASS_NAME, "nav-search-input"))  # 使用 JavaScript 强制聚焦
css_pseudo = driver.find_element(By.CSS_SELECTOR, "input:focus")  # 获取焦点的元素
css_nth_child = driver.find_element(By.CSS_SELECTOR, "div div:nth-child(3)")  # 第3个子元素

# 查找多个元素(返回列表)
elements = driver.find_elements(By.TAG_NAME, "a")
print(f"找到{len(elements)}个链接元素")

driver.quit()

定位策略选择建议

  • 优先使用 ID(唯一且稳定)
  • 其次考虑 Name 或 Class Name
  • 链接使用 Link Text 或 Partial Link Text
  • 复杂场景使用 XPath 或 CSS Selector
  • CSS Selector 通常比 XPath 执行速度更快

2.1.3 动态元素处理

在现代 Web 应用中,许多元素是动态加载的,需要使用等待机制确保元素加载完成后再进行操作。

2.1.3.1 隐式等待

隐式等待设置一个全局超时时间,适用于整个 WebDriver 生命周期:

from selenium.webdriver.common.by import By

# 设置隐式等待时间为10秒
driver.implicitly_wait(10)

driver.get("https://www.google.com")
# 如果元素未立即出现,会等待最多10秒
element = driver.find_element(By.ID, "APjFqb")

driver.quit()

2.1.3.2 显式等待

显式等待针对特定元素设置等待条件和超时时间,更加灵活:

from selenium.webdriver.common.by import By
from selenium.common import TimeoutException
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver.get("https://www.bing.com")

# 创建一个等待对象,超时时间10秒
wait = WebDriverWait(driver, 10)

try:
    # 等待元素可见
    input_element = wait.until(
        EC.visibility_of_element_located((By.ID, 'sb_form_q'))
    )
    search_btn_element = wait.until(
        EC.visibility_of_element_located((By.ID, 'search_icon'))
    )
    qr_code_element = wait.until(
        EC.visibility_of_element_located((By.CLASS_NAME, 'id_qrcode_close'))
    )
    # 等待元素可点击
    search_btn_clickable_element = wait.until(
        EC.element_to_be_clickable((By.ID, "search_icon"))
    )
    qr_code_close_element = wait.until(
        EC.element_to_be_clickable((By.CLASS_NAME, "id_qrcode_close"))
    )

    # 搜索北京时间结果
    time.sleep(3)
    input_element.send_keys('北京时间')
    qr_code_close_element.click()
    time.sleep(1)
    search_btn_clickable_element.click()

    driver.get_screenshot_as_file('aaa.png')
    # 等待元素存在于DOM中(不一定可见)
    present_element = wait.until(
        EC.presence_of_element_located((By.CLASS_NAME, "b_scopebar"))
    )

    # 等待iframe加载完成并切换到该iframe
    iframe = wait.until(
        EC.frame_to_be_available_and_switch_to_it((By.ID, "OverlayIFrame"))
    )

    # 等待元素消失
    wait.until(
        EC.invisibility_of_element_located((By.ID, "digit_time"))
    )
except TimeoutException as e:
    print("等待超时,元素未满足条件", e)
finally:
    driver.quit()

2.1.4 交互行为模拟

2.1.4.1 表单操作

表单是 Web 应用中最常见的交互元素,Selenium 提供了丰富的 API 来操作各种表单控件。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

# 初始化浏览器驱动
driver = webdriver.Chrome()
driver.get("https://example.com/form")  # 替换为实际的表单页面URL
driver.maximize_window()

try:
    # 1. 文本输入框操作
    username = driver.find_element(By.ID, "username")
    username.clear()  # 清空输入框
    username.send_keys("test_user")  # 输入文本
    time.sleep(1)  # 仅用于演示,实际测试中应使用显式等待
  
    # 2. 密码框操作
    password = driver.find_element(By.NAME, "password")
    password.send_keys("secure_password")
    time.sleep(1)
  
    # 3. 下拉框操作 (单选)
    # 方法1: 使用Select类
    country_select = Select(driver.find_element(By.ID, "country"))
    country_select.select_by_visible_text("China")  # 按可见文本选择
    time.sleep(1)
  
    country_select.select_by_value("us")  # 按值选择
    time.sleep(1)
  
    country_select.select_by_index(2)  # 按索引选择
    time.sleep(1)
  
    # 方法2: 直接点击选项
    driver.find_element(By.ID, "country").click()
    driver.find_element(By.XPATH, "//option[text()='Canada']").click()
    time.sleep(1)
  
    # 4. 单选按钮操作
    gender_male = driver.find_element(By.ID, "gender_male")
    if not gender_male.is_selected():
        gender_male.click()
    time.sleep(1)
  
    gender_female = driver.find_element(By.ID, "gender_female")
    if not gender_female.is_selected():
        gender_female.click()
    time.sleep(1)
  
    # 5. 复选框操作
    hobbies = driver.find_elements(By.NAME, "hobbies")
    for hobby in hobbies:
        # 选择前两个爱好
        if hobby.get_attribute("value") in ["reading", "sports"] and not hobby.is_selected():
            hobby.click()
    time.sleep(1)
  
    # 取消选择第一个爱好
    if hobbies[0].is_selected():
        hobbies[0].click()
    time.sleep(1)
  
    # 6. 提交表单
    submit_button = driver.find_element(By.XPATH, "//button[@type='submit']")
    submit_button.click()
  
    # 等待表单提交完成
    WebDriverWait(driver, 10).until(
        EC.title_contains("Success")
    )
    print("表单提交成功")

except Exception as e:
    print(f"操作过程中发生错误: {str(e)}")

finally:
    # 等待几秒后关闭浏览器
    time.sleep(3)
    driver.quit()

表单操作要点解析

  1. 文本输入:使用 send_keys()方法输入文本,clear()方法清空内容
  2. 下拉框处理
    • 对于标准下拉框 (<select>标签),推荐使用 Select
    • 对于自定义下拉框,可通过点击触发下拉,再选择选项
  3. 单选 / 复选框
    • 使用 is_selected()检查状态
    • 使用 click()方法切换状态
  4. 表单提交:可以点击提交按钮或使用 submit()方法

2.1.4.2 鼠标事件

Selenium 的 ActionChains类提供了模拟各种鼠标操作的方法,适用于处理复杂的用户交互。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

# 初始化浏览器驱动
driver = webdriver.Chrome()
driver.get("https://example.com/interactive")  # 替换为实际测试页面
driver.maximize_window()

try:
    # 等待页面加载完成
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.TAG_NAME, "body"))
    )
  
    # 1. 鼠标悬停操作
    menu_item = driver.find_element(By.ID, "main_menu")
    submenu_item = driver.find_element(By.ID, "submenu_item")
  
    # 初始化ActionChains
    actions = ActionChains(driver)
  
    # 悬停在主菜单上
    actions.move_to_element(menu_item).perform()
    time.sleep(2)  # 等待子菜单显示
  
    # 悬停在子菜单上并点击
    actions.move_to_element(submenu_item).click().perform()
    time.sleep(2)
  
    # 返回上一页
    driver.back()
    time.sleep(1)
  
    # 2. 鼠标拖拽操作
    source_element = driver.find_element(By.ID, "draggable")
    target_element = driver.find_element(By.ID, "droppable")
  
    # 方法1: 直接拖拽到目标元素
    actions.drag_and_drop(source_element, target_element).perform()
    time.sleep(2)
  
    # 重置位置(假设页面有重置按钮)
    driver.find_element(By.ID, "reset_drag_drop").click()
    time.sleep(1)
  
    # 方法2: 拖拽指定偏移量
    actions.drag_and_drop_by_offset(
        source_element, 
        100,  # x轴偏移量
        50    # y轴偏移量
    ).perform()
    time.sleep(2)
  
    # 3. 右键点击操作
    context_menu_element = driver.find_element(By.ID, "context_menu_element")
  
    # 右键点击元素
    actions.context_click(context_menu_element).perform()
    time.sleep(2)
  
    # 在右键菜单中选择一个选项
    driver.find_element(By.XPATH, "//div[contains(@class, 'context-menu')]/div[text()='Edit']").click()
    time.sleep(2)
  
    # 4. 双击操作
    double_click_element = driver.find_element(By.ID, "double_click_element")
    actions.double_click(double_click_element).perform()
    time.sleep(2)

except Exception as e:
    print(f"鼠标操作发生错误: {str(e)}")

finally:
    # 等待几秒后关闭浏览器
    time.sleep(3)
    driver.quit()

鼠标事件要点解析

  1. ActionChains 工作原理
    • 所有操作会被存储在一个队列中
    • 调用 perform()方法时才会执行所有操作
  2. 常用鼠标操作
    • move_to_element():鼠标悬停
    • click():左键点击
    • context_click():右键点击
    • double_click():双击
    • drag_and_drop():拖拽到目标元素
    • drag_and_drop_by_offset():拖拽指定偏移量
  3. 注意事项
    • 复杂操作可能需要添加适当的等待时间
    • 确保操作元素在执行时可见且可交互

2.1.4.2 键盘操作

Selenium 提供了模拟键盘输入的功能,包括单个按键、组合键以及特殊字符的输入。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
import time

# 初始化浏览器驱动
driver = webdriver.Chrome()
driver.get("https://example.com/editor")  # 替换为实际测试页面
driver.maximize_window()

try:
    # 获取文本输入区域
    text_area = driver.find_element(By.ID, "text_editor")
    text_area.click()
    time.sleep(1)
  
    # 1. 基本文本输入
    text_area.send_keys("Hello, Selenium!")
    time.sleep(2)
  
    # 2. 特殊按键
    # 按Enter键
    text_area.send_keys(Keys.ENTER)
    time.sleep(1)
  
    # 输入新行文本
    text_area.send_keys("This is a new line.")
    time.sleep(1)
  
    # 按Backspace键删除最后一个字符
    text_area.send_keys(Keys.BACKSPACE)
    time.sleep(1)
  
    # 3. 组合键操作
    # Ctrl+A 全选
    text_area.send_keys(Keys.CONTROL, 'a')
    time.sleep(1)
  
    # Ctrl+C 复制
    text_area.send_keys(Keys.CONTROL, 'c')
    time.sleep(1)
  
    # 按两次Enter键
    text_area.send_keys(Keys.ENTER * 2)
    time.sleep(1)
  
    # Ctrl+V 粘贴
    text_area.send_keys(Keys.CONTROL, 'v')
    time.sleep(2)
  
    # 4. 使用ActionChains实现更复杂的键盘操作
    actions = ActionChains(driver)
  
    # 移动到文本末尾
    actions.send_keys(Keys.END).perform()
    time.sleep(1)
  
    # 按住Shift键,然后按Home键选择从光标到行首的内容
    actions.key_down(Keys.SHIFT)\
           .send_keys(Keys.HOME)\
           .key_up(Keys.SHIFT)\
           .perform()
    time.sleep(2)
  
    # 输入新内容替换选中的内容
    actions.send_keys("Replaced text").perform()
    time.sleep(2)
  
    # 5. 输入特殊字符
    text_area.send_keys(Keys.ENTER)
    text_area.send_keys("Special characters: !@#$%^&*()_+{}|:\"<>?")
    time.sleep(2)

except Exception as e:
    print(f"键盘操作发生错误: {str(e)}")

finally:
    # 等待几秒后关闭浏览器
    time.sleep(3)
    driver.quit()

键盘操作要点解析

  1. Keys 类:提供了所有键盘按键的常量表示,如 Keys.ENTERKeys.TAB
  2. 常用组合键
    • Keys.CONTROL + 'a':全选
    • Keys.CONTROL + 'c':复制
    • Keys.CONTROL + 'v':粘贴
    • Keys.CONTROL + 'x':剪切
    • Keys.SHIFT + Keys.ARROW_LEFT:向左选中文本
  3. 跨平台注意事项
    • Windows 和 Linux 使用 Keys.CONTROL
    • Mac 使用 Keys.COMMAND代替 Keys.CONTROL
  4. 复杂键盘操作
    • 使用 ActionChainskey_down()key_up()方法处理需要按住的键
    • 可以通过链式调用组合多个键盘操作

2.1.5 复杂场景处理

2.1.5.1 多窗口 / Frame 切换

在 Web 应用中,页面常常包含多个窗口或 Frame,这给自动化测试带来了挑战。Selenium 提供了灵活的方法来处理这些场景。

窗口切换

当页面打开新窗口时,Selenium 需要切换到相应的窗口才能操作其中的元素:

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

# 初始化浏览器驱动
driver = webdriver.Chrome()
driver.maximize_window()

# 打开主页面
driver.get("https://example.com")

# 获取当前窗口句柄(主窗口)
main_window = driver.current_window_handle
print(f"主窗口句柄: {main_window}")

# 点击打开新窗口的链接
open_new_window_btn = driver.find_element(By.LINK_TEXT, "打开新窗口")
open_new_window_btn.click()

# 等待新窗口打开
time.sleep(2)

# 获取所有窗口句柄
all_windows = driver.window_handles
print(f"所有窗口句柄: {all_windows}")

# 切换到新窗口
for window in all_windows:
    if window != main_window:
        driver.switch_to.window(window)
        print(f"已切换到新窗口: {window}")
        break

# 在新窗口中执行操作
new_window_element = driver.find_element(By.ID, "new_window_content")
print(f"新窗口内容: {new_window_element.text}")

# 关闭新窗口
driver.close()

# 切换回主窗口
driver.switch_to.window(main_window)
print("已切换回主窗口")

# 继续在主窗口操作...

# 关闭浏览器
driver.quit()

Frame 切换

Frame 是嵌入在页面中的文档,Selenium 需要明确切换到相应的 Frame 才能操作其中的元素:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchFrameException
import time

# 初始化浏览器驱动
driver = webdriver.Chrome()
driver.get("https://example.com/frames")

try:
    # 通过 ID 切换到 Frame
    driver.switch_to.frame("frame1")
    print("已切换到 frame1")
  
    # 在 frame1 中操作元素
    frame1_element = driver.find_element(By.ID, "frame1_content")
    print(f"frame1 内容: {frame1_element.text}")
  
    # 切换到子 Frame
    driver.switch_to.frame("subframe1")
    print("已切换到 subframe1")
  
    # 在子 Frame 中操作元素
    subframe_element = driver.find_element(By.TAG_NAME, "p")
    print(f"subframe1 内容: {subframe_element.text}")
  
    # 切换回父 Frame
    driver.switch_to.parent_frame()
    print("已切换回父 Frame")
  
    # 切换回主文档
    driver.switch_to.default_content()
    print("已切换回主文档")
  
    # 通过索引切换到 Frame(索引从 0 开始)
    driver.switch_to.frame(1)
    print("已通过索引切换到第二个 Frame")
  
    # 操作完成后切回主文档
    driver.switch_to.default_content()
  
except NoSuchFrameException as e:
    print(f"切换 Frame 时出错: {e}")

# 关闭浏览器
driver.quit()

技术要点

  • 使用 switch_to.window(window_handle) 切换窗口
  • 使用 window_handles 获取所有窗口句柄
  • 切换 Frame 可以通过 ID、名称、索引或 WebElement
  • 使用 switch_to.parent_frame() 返回上一级 Frame
  • 使用 switch_to.default_content() 返回主文档

2.1.5.2 弹窗与文件上传

Web 应用中常见的弹窗和文件上传功能需要特殊处理方法。

处理弹窗

Selenium 可以处理 JavaScript 弹窗(alert、confirm、prompt):

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
import time

# 初始化浏览器驱动
driver = webdriver.Chrome()
driver.get("https://example.com/popups")

# 处理警告弹窗 (alert)
alert_btn = driver.find_element(By.ID, "alert_btn")
alert_btn.click()

# 等待弹窗出现并切换到弹窗
alert = WebDriverWait(driver, 10).until(EC.alert_is_present())
print(f"警告弹窗文本: {alert.text}")

# 接受弹窗
alert.accept()
print("已接受警告弹窗")

# 处理确认弹窗 (confirm)
confirm_btn = driver.find_element(By.ID, "confirm_btn")
confirm_btn.click()

# 切换到确认弹窗
confirm = WebDriverWait(driver, 10).until(EC.alert_is_present())
print(f"确认弹窗文本: {confirm.text}")

# 取消弹窗
confirm.dismiss()
print("已取消确认弹窗")

# 处理提示弹窗 (prompt)
prompt_btn = driver.find_element(By.ID, "prompt_btn")
prompt_btn.click()

# 切换到提示弹窗
prompt = WebDriverWait(driver, 10).until(EC.alert_is_present())
print(f"提示弹窗文本: {prompt.text}")

# 输入文本并接受
prompt.send_keys("Selenium 自动化测试")
prompt.accept()
print("已在提示弹窗中输入文本并确认")

# 验证输入结果
result = driver.find_element(By.ID, "prompt_result")
print(f"提示弹窗输入结果: {result.text}")

# 关闭浏览器
driver.quit()

文件上传处理

文件上传是 Web 应用中常见的功能,Selenium 提供了简单的处理方式:

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
import os
import time

# 初始化浏览器驱动
driver = webdriver.Chrome()
driver.get("https://example.com/upload")

# 获取要上传的文件的绝对路径
file_path = os.path.abspath("test_file.txt")
print(f"要上传的文件路径: {file_path}")

try:
    # 找到文件上传输入框并直接发送文件路径
    # 注意:必须是 type 为 file 的 input 元素
    upload_input = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "file_upload_input"))
    )
  
    # 直接发送文件路径,无需点击上传按钮
    upload_input.send_keys(file_path)
    print("文件路径已发送到上传输入框")
  
    # 点击上传按钮(如果需要)
    upload_button = driver.find_element(By.ID, "upload_button")
    upload_button.click()
  
    # 等待上传完成
    WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((By.ID, "upload_success_message"))
    )
  
    # 验证上传结果
    success_message = driver.find_element(By.ID, "upload_success_message")
    print(f"上传结果: {success_message.text}")
  
except Exception as e:
    print(f"文件上传出错: {e}")

# 关闭浏览器
driver.quit()

技术要点

  • 使用 switch_to.alert 处理 JavaScript 弹窗
  • 弹窗操作包括 accept()(确认)、dismiss()(取消)和 send_keys()(输入文本)
  • 文件上传通过找到 type 为 file 的 input 元素,直接使用 send_keys() 方法传入文件路径
  • 复杂上传组件可能需要结合 AutoIt 或 pyautogui 等工具

2.1.5.3 浏览器日志分析与性能监控

Selenium 可以获取浏览器日志并监控页面性能,这对于调试和性能测试非常有用。

浏览器日志分析

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import json

# 配置 Chrome 选项以启用日志
chrome_options = Options()
# 设置日志级别,0 为详细,3 为错误
chrome_options.add_argument("--log-level=0")

# 配置日志偏好设置
desired_capabilities = webdriver.DesiredCapabilities.CHROME
desired_capabilities['goog:loggingPrefs'] = {
    'browser': 'ALL',       # 浏览器控制台日志
    'driver': 'ALL',        # 驱动日志
    'performance': 'ALL'    # 性能日志
}

# 初始化浏览器驱动
driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),
    options=chrome_options,
    desired_capabilities=desired_capabilities
)

# 打开测试页面
driver.get("https://example.com")

# 获取浏览器控制台日志
browser_logs = driver.get_log('browser')
print(f"浏览器控制台日志数量: {len(browser_logs)}")

# 打印错误级别以上的日志
print("\n错误级别以上的浏览器日志:")
for log in browser_logs:
    if log['level'] in ['SEVERE', 'ERROR']:
        print(f"[{log['level']}] {log['timestamp']}: {log['message']}")

# 获取性能日志
performance_logs = driver.get_log('performance')
print(f"\n性能日志数量: {len(performance_logs)}")

# 解析并打印部分性能日志
print("\n部分性能日志解析:")
for log in performance_logs[:5]:  # 只打印前5条
    try:
        log_message = json.loads(log['message'])['message']
        print(f"[{log['level']}] {log_message['method']}")
    except json.JSONDecodeError:
        print(f"无法解析日志: {log['message']}")

# 关闭浏览器
driver.quit()

性能监控

Selenium 结合浏览器性能日志可以监控页面加载性能:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import json
import time

def get_page_load_time(driver):
    """获取页面加载时间"""
    return driver.execute_script("return window.performance.timing.loadEventEnd - window.performance.timing.navigationStart;")

def get_resource_load_times(driver):
    """获取各资源加载时间"""
    resources = driver.execute_script("""
        return window.performance.getEntriesByType('resource').map(resource => ({
            name: resource.name,
            duration: resource.duration,
            startTime: resource.startTime,
            endTime: resource.startTime + resource.duration
        }));
    """)
    # 按持续时间排序
    return sorted(resources, key=lambda x: x['duration'], reverse=True)

# 配置 Chrome 选项
chrome_options = Options()
chrome_options.add_argument("--headless")  # 无头模式,可选
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1920,1080")

# 启用性能日志
desired_capabilities = webdriver.DesiredCapabilities.CHROME
desired_capabilities['goog:loggingPrefs'] = {'performance': 'ALL'}

# 初始化浏览器驱动
driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),
    options=chrome_options,
    desired_capabilities=desired_capabilities
)

# 打开测试页面
driver.get("https://example.com")

# 等待页面完全加载
time.sleep(3)

# 获取并打印页面加载时间
load_time = get_page_load_time(driver)
print(f"页面加载总时间: {load_time:.2f} 毫秒")

# 获取并打印资源加载时间(前5个最慢的资源)
resource_times = get_resource_load_times(driver)
print("\n加载最慢的5个资源:")
for i, resource in enumerate(resource_times[:5]):
    print(f"{i+1}. {resource['name']} - 耗时: {resource['duration']:.2f} 毫秒")

# 分析性能日志中的网络请求
performance_logs = driver.get_log('performance')
network_requests = []

for log in performance_logs:
    try:
        log_json = json.loads(log['message'])
        message = log_json['message']
  
        # 收集网络请求信息
        if message['method'] == 'Network.requestWillBeSent':
            request = message['params']['request']
            network_requests.append({
                'url': request['url'],
                'method': request['method'],
                'timestamp': log['timestamp']
            })
    except (json.JSONDecodeError, KeyError):
        continue

print(f"\n捕获到的网络请求数量: {len(network_requests)}")

# 关闭浏览器
driver.quit()

技术要点

  • 通过配置 goog:loggingPrefs 启用不同类型的日志
  • 可以获取浏览器控制台日志、驱动日志和性能日志
  • 使用 window.performance API 获取页面和资源加载时间
  • 性能日志可以解析出网络请求、响应等详细信息
  • 结合这些数据可以进行性能瓶颈分析和优化

2.1.6 设计模式

2.1.6.1 Page Object 模式

Page Object 模式是 Selenium 自动化测试中广泛采用的设计模式,它将页面逻辑与测试逻辑分离,提高代码复用性和可维护性。

六大原则

  1. 封装性:页面元素和操作细节封装在 Page 类内部
  2. 单一职责:每个 Page 类对应一个页面或页面片段
  3. 接口抽象:暴露有意义的业务操作,而非技术细节
  4. 不包含断言:断言应放在测试用例中,而非 Page 类
  5. 延迟初始化:元素在首次使用时才初始化
  6. 链式调用:支持流畅的方法调用风格

分层架构

  1. 基础层:封装通用操作和 Selenium API
  2. 页面对象层:实现具体页面的元素和操作
  3. 业务流程层:组合页面操作形成业务流程
  4. 测试用例层:调用业务流程并进行断言验证

代码实现示例

1. 基础层封装
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import TimeoutException

class BasePage:
    """所有页面的基类,封装通用的页面操作"""
  
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
        self.action = ActionChains(driver)
  
    def find_element(self, locator):
        """查找单个元素,封装显式等待"""
        try:
            return self.wait.until(EC.presence_of_element_located(locator))
        except TimeoutException:
            raise Exception(f"元素 {locator} 未找到")
  
    def find_elements(self, locator):
        """查找多个元素"""
        try:
            return self.wait.until(EC.presence_of_all_elements_located(locator))
        except TimeoutException:
            raise Exception(f"未找到符合 {locator} 的元素")
  
    def click(self, locator):
        """点击元素"""
        self.wait.until(EC.element_to_be_clickable(locator)).click()
        return self  # 支持链式调用
  
    def input_text(self, locator, text):
        """向输入框输入文本"""
        element = self.find_element(locator)
        element.clear()
        element.send_keys(text)
        return self  # 支持链式调用
  
    def get_text(self, locator):
        """获取元素文本"""
        return self.find_element(locator).text
  
    def is_displayed(self, locator):
        """判断元素是否可见"""
        try:
            return self.wait.until(EC.visibility_of_element_located(locator)).is_displayed()
        except TimeoutException:
            return False
  
    def scroll_to_element(self, locator):
        """滚动到元素可见"""
        element = self.find_element(locator)
        self.driver.execute_script("arguments[0].scrollIntoView();", element)
        return self
  
    def switch_to_frame(self, frame_locator):
        """切换到iframe"""
        self.wait.until(EC.frame_to_be_available_and_switch_to_it(frame_locator))
        return self
  
    def switch_to_default_content(self):
        """切换回主文档"""
        self.driver.switch_to.default_content()
        return self
  
    def get_current_url(self):
        """获取当前页面URL"""
        return self.driver.current_url
2. 页面对象层实现
from selenium.webdriver.common.by import By
from base_page import BasePage

class LoginPage(BasePage):
    """登录页面的Page Object"""
  
    # 页面元素定位器
    _username_input = (By.ID, "username")
    _password_input = (By.ID, "password")
    _login_button = (By.ID, "login_btn")
    _error_message = (By.ID, "error_message")
    _forgot_password_link = (By.LINK_TEXT, "Forgot Password?")
    _register_link = (By.LINK_TEXT, "Register")
  
    def enter_username(self, username):
        """输入用户名"""
        return self.input_text(self._username_input, username)
  
    def enter_password(self, password):
        """输入密码"""
        return self.input_text(self._password_input, password)
  
    def click_login(self):
        """点击登录按钮"""
        self.click(self._login_button)
        # 从登录页面跳转到主页,返回主页对象
        from home_page import HomePage
        return HomePage(self.driver)
  
    def get_error_message(self):
        """获取错误提示信息"""
        return self.get_text(self._error_message)
  
    def is_error_message_displayed(self):
        """判断错误提示是否显示"""
        return self.is_displayed(self._error_message)
  
    def click_forgot_password(self):
        """点击忘记密码链接"""
        self.click(self._forgot_password_link)
        from forgot_password_page import ForgotPasswordPage
        return ForgotPasswordPage(self.driver)
  
    def click_register(self):
        """点击注册链接"""
        self.click(self._register_link)
        from register_page import RegisterPage
        return RegisterPage(self.driver)
  
    def login(self, username, password):
        """完整登录流程"""
        return self.enter_username(username)\
                   .enter_password(password)\
                   .click_login()
3. 业务流程层
from login_page import LoginPage
from home_page import HomePage
from settings_page import SettingsPage

class AuthWorkflows:
    """认证相关的业务流程"""
  
    def __init__(self, driver):
        self.driver = driver
        self.login_page = LoginPage(driver)
  
    def login_as_user(self, username, password):
        """以指定用户登录系统"""
        home_page = self.login_page.login(username, password)
        # 验证登录成功
        assert home_page.is_user_menu_displayed(), "登录后未显示用户菜单"
        return home_page
  
    def login_and_navigate_to_settings(self, username, password):
        """登录并导航到设置页面"""
        home_page = self.login_as_user(username, password)
        settings_page = home_page.navigate_to_settings()
        assert settings_page.get_page_title() == "Account Settings", "导航到设置页面失败"
        return settings_page
  
    def login_and_logout(self, username, password):
        """登录后立即退出"""
        home_page = self.login_as_user(username, password)
        login_page = home_page.logout()
        assert login_page.is_login_button_displayed(), "退出后未返回登录页面"
        return login_page
4. 测试用例层
import pytest
from selenium import webdriver
from login_page import LoginPage
from auth_workflows import AuthWorkflows

@pytest.fixture(scope="function")
def driver():
    driver = webdriver.Chrome()
    driver.maximize_window()
    driver.get("https://example.com/login")
    yield driver
    driver.quit()

def test_successful_login(driver):
    """测试成功登录"""
    auth_flow = AuthWorkflows(driver)
    home_page = auth_flow.login_as_user("testuser", "testpass123")
  
    # 验证登录状态
    assert home_page.get_welcome_message() == "Welcome, testuser"
    assert home_page.is_logout_button_displayed()

def test_invalid_credentials(driver):
    """测试无效凭据登录"""
    login_page = LoginPage(driver)
    login_page.enter_username("testuser")\
              .enter_password("wrongpass")\
              .click_login()
  
    # 验证错误信息
    assert login_page.is_error_message_displayed()
    assert login_page.get_error_message() == "Invalid username or password"

def test_navigate_to_register_page(driver):
    """测试从登录页导航到注册页"""
    login_page = LoginPage(driver)
    register_page = login_page.click_register()
  
    # 验证页面跳转
    assert "register" in register_page.get_current_url()
    assert register_page.get_page_title() == "Create New Account"

2.1.6.2 数据驱动测试

数据驱动测试(DDT)是一种测试方法,它将测试数据与测试逻辑分离,使同一测试逻辑可以用多组不同数据执行。

数据源类型

  1. Excel/CSV:适合非技术人员维护数据
  2. JSON:适合结构化数据,易于与 API 测试集成
  3. 数据库:适合大量数据和动态数据
  4. YAML:适合复杂结构和分层数据

代码实现示例

1. 使用 JSON 数据源
{
  "valid_login": {
    "username": "testuser",
    "password": "testpass123",
    "expected_result": "Welcome, testuser"
  },
  "invalid_password": {
    "username": "testuser",
    "password": "wrongpass",
    "expected_result": "Invalid username or password"
  },
  "empty_credentials": {
    "username": "",
    "password": "",
    "expected_result": "Username is required"
  },
  "locked_account": {
    "username": "lockeduser",
    "password": "anypass",
    "expected_result": "This account has been locked"
  }
}
2. 使用 CSV 数据源
username,password,expected_result
testuser,testpass123,Welcome, testuser
testuser,wrongpass,Invalid username or password
,"",Username is required
lockeduser,anypass,This account has been locked
3. 数据驱动测试实现
import pytest
import json
import csv
from selenium import webdriver
from login_page import LoginPage

# 加载JSON测试数据
def load_json_test_data(file_path):
    with open(file_path, 'r') as f:
        data = json.load(f)
    # 转换为pytest参数化所需的格式
    return [(name, details) for name, details in data.items()]

# 加载CSV测试数据
def load_csv_test_data(file_path):
    test_data = []
    with open(file_path, 'r') as f:
        reader = csv.DictReader(f)
        for row in reader:
            test_data.append((
                f"{row['username']}_{row['password']}",  # 测试用例名称
                row  # 测试数据
            ))
    return test_data

@pytest.fixture(scope="function")
def driver():
    driver = webdriver.Chrome()
    driver.maximize_window()
    driver.get("https://example.com/login")
    yield driver
    driver.quit()

@pytest.fixture(scope="function")
def login_page(driver):
    return LoginPage(driver)

# 使用JSON数据的参数化测试
@pytest.mark.parametrize("test_name, test_data", 
                         load_json_test_data("login_test_data.json"))
def test_login_with_json_data(login_page, test_name, test_data):
    """使用JSON数据驱动的登录测试"""
    login_page.enter_username(test_data["username"])\
              .enter_password(test_data["password"])\
              .click_login()
  
    if "Welcome" in test_data["expected_result"]:
        # 验证成功登录
        assert login_page.get_welcome_message() == test_data["expected_result"]
    else:
        # 验证错误信息
        assert login_page.is_error_message_displayed()
        assert login_page.get_error_message() == test_data["expected_result"]

# 使用CSV数据的参数化测试
@pytest.mark.parametrize("test_name, test_data", 
                         load_csv_test_data("login_test_data.csv"))
def test_login_with_csv_data(login_page, test_name, test_data):
    """使用CSV数据驱动的登录测试"""
    login_page.enter_username(test_data["username"])\
              .enter_password(test_data["password"])\
              .click_login()
  
    if "Welcome" in test_data["expected_result"]:
        assert login_page.get_welcome_message() == test_data["expected_result"]
    else:
        assert login_page.is_error_message_displayed()
        assert login_page.get_error_message() == test_data["expected_result"]

数据驱动测试最佳实践

  1. 数据隔离:测试数据应与测试代码完全分离
  2. 数据验证:加载数据时进行验证,确保格式正确
  3. 有意义的命名:为每组测试数据提供清晰的名称
  4. 数据分组:按测试场景或优先级对数据进行分组
  5. 动态生成:对于大量数据,考虑动态生成而非静态文件
  6. 敏感数据处理:密码等敏感信息应加密存储或使用环境变量

2.1.7 反爬策略与稳定性优化

在使用 Selenium 进行 Web 自动化时,经常会遇到网站的反爬机制和测试稳定性问题。本章本章将详细介绍如何应对这些挑战,包括隐藏自动化特征、使用动态代理以及处理验证码等关键技术。

2.1.7.1 自动化特征隐藏

1. 禁用 WebDriver 标识

现代浏览器会通过 navigator.webdriver属性暴露自动化控制状态,我们需要禁用这一标识。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time

def create_undetectable_driver():
    """创建难以被检测的Chrome浏览器实例"""
    chrome_options = Options()
  
    # 基础配置
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-dev-shm-usage")
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    chrome_options.add_argument("--disable-extensions")
    chrome_options.add_argument("--start-maximized")
    chrome_options.add_argument("--remote-debugging-port=9222")
  
    # 禁用自动化控制特征
    chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
    chrome_options.add_experimental_option("useAutomationExtension", False)
  
    # 初始化驱动
    driver = webdriver.Chrome(
        service=Service(ChromeDriverManager().install()),
        options=chrome_options
    )
  
    # 进一步隐藏webdriver属性
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            });
            Object.defineProperty(navigator, 'languages', {
                get: () => ['en-US', 'en']
            });
            Object.defineProperty(navigator, 'plugins', {
                get: () => [1, 2, 3]
            });
        """
    })
  
    return driver

# 使用示例
if __name__ == "__main__":
    driver = create_undetectable_driver()
    driver.get("https://example.com")
  
    # 验证webdriver属性是否被隐藏
    webdriver_value = driver.execute_script("return navigator.webdriver")
    print(f"navigator.webdriver value: {webdriver_value}")  # 应为undefined
  
    time.sleep(5)
    driver.quit()
2. 修改 User-Agent
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import random
import requests

def get_random_user_agent():
    """从网上获取随机的User-Agent或使用预定义列表"""
    # 方法1: 使用预定义的User-Agent列表
    user_agents = [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36",
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/111.0",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15",
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/112.0.1722.58 Safari/537.36"
    ]
  
    # 方法2: 从在线API获取最新User-Agent(需要网络连接)
    try:
        response = requests.get("https://api.user-agents.net/random")
        if response.status_code == 200:
            return response.json()["data"]["ua"]
    except:
        pass  # 失败时使用预定义列表
  
    return random.choice(user_agents)

def create_driver_with_random_ua():
    """创建带有随机User-Agent的浏览器实例"""
    chrome_options = Options()
  
    # 添加随机User-Agent
    user_agent = get_random_user_agent()
    chrome_options.add_argument(f"user-agent={user_agent}")
  
    # 添加其他必要配置
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
  
    driver = webdriver.Chrome(options=chrome_options)
  
    # 验证User-Agent设置
    print(f"使用的User-Agent: {user_agent}")
    actual_ua = driver.execute_script("return navigator.userAgent")
    print(f"实际的User-Agent: {actual_ua}")
  
    return driver

# 使用示例
if __name__ == "__main__":
    driver = create_driver_with_random_ua()
    driver.get("https://example.com")
    input("按Enter键继续...")
    driver.quit()

自动化特征隐藏要点

  1. 核心检测点:
    • navigator.webdriver属性
    • 特殊的 User-Agent 模式
    • 浏览器窗口大小和行为模式
    • 异常的点击和滚动模式
    • JavaScript 引擎特征
  2. 高级隐藏技巧:
    • 使用真实浏览器的配置文件
    • 模拟真实用户的操作速度和模式
    • 随机化操作间隔时间
    • 避免固定的操作序列
    • 使用 CDP (Chrome DevTools Protocol) 修改浏览器行为
3. 动态代理池与 IP 轮换

当需要大量访问目标网站时,IP 地址轮换是避免被封禁的关键策略。动态代理池可以提供稳定的 IP 来源。

代理池实现

import requests
import random
from dataclasses import dataclass
from typing import List, Optional
import time

@dataclass
class Proxy:
    """代理服务器信息"""
    ip: str
    port: int
    protocol: str = "http"
    anonymity: str = "unknown"
    country: str = "unknown"
    last_checked: float = 0.0
    success_count: int = 0
    fail_count: int = 0
  
    @property
    def url(self) -> str:
        """返回代理URL"""
        return f"{self.protocol}://{self.ip}:{self.port}"

class ProxyPool:
    """代理池管理类"""
  
    def __init__(self, min_working_proxies: int = 5):
        self.proxies: List[Proxy] = []
        self.working_proxies: List[Proxy] = []
        self.min_working_proxies = min_working_proxies
        self.test_url = "https://example.com"
        self.test_timeout = 10
  
    def fetch_proxies_from_api(self) -> List[Proxy]:
        """从代理API获取代理列表"""
        proxies = []
  
        # 示例1: 从公开代理API获取
        try:
            # 注意:实际使用时需要替换为可靠的代理服务
            response = requests.get(
                "https://api.proxyscrape.com/v2/?request=getproxies&protocol=http&timeout=10000&country=all&ssl=all&anonymity=all",
                timeout=10
            )
            if response.status_code == 200:
                for line in response.text.splitlines():
                    if line:
                        ip, port = line.split(':')
                        proxies.append(Proxy(ip=ip, port=int(port)))
        except Exception as e:
            print(f"从API获取代理失败: {e}")
  
        # 示例2: 从自定义代理服务获取(推荐)
        try:
            # 这里替换为你的私有代理服务API
            api_key = "your_proxy_api_key"
            response = requests.get(
                f"https://your-proxy-service.com/api/proxies?api_key={api_key}&protocol=http",
                timeout=10
            )
            if response.status_code == 200:
                for proxy_data in response.json()["proxies"]:
                    proxies.append(Proxy(
                        ip=proxy_data["ip"],
                        port=proxy_data["port"],
                        protocol=proxy_data["protocol"],
                        anonymity=proxy_data["anonymity"],
                        country=proxy_data["country"]
                    ))
        except Exception as e:
            print(f"从自定义API获取代理失败: {e}")
    
        return proxies
  
    def validate_proxy(self, proxy: Proxy) -> bool:
        """验证代理是否可用"""
        try:
            proxies = {
                "http": proxy.url,
                "https": proxy.url
            }
            start_time = time.time()
            response = requests.get(
                self.test_url,
                proxies=proxies,
                timeout=self.test_timeout
            )
            proxy.last_checked = time.time()
            return response.status_code == 200
        except:
            return False
  
    def refresh_proxies(self) -> None:
        """刷新代理池并验证代理"""
        print("刷新代理池...")
        # 获取新代理
        new_proxies = self.fetch_proxies_from_api()
        self.proxies = list({p.url: p for p in self.proxies + new_proxies}.values())
  
        # 验证所有代理
        self.working_proxies = []
        for proxy in self.proxies:
            if self.validate_proxy(proxy):
                self.working_proxies.append(proxy)
                proxy.success_count += 1
                print(f"有效代理: {proxy.url}")
            else:
                proxy.fail_count += 1
                print(f"无效代理: {proxy.url}")
  
        print(f"代理池刷新完成,有效代理数量: {len(self.working_proxies)}")
  
        # 如果有效代理不足,再次尝试
        if len(self.working_proxies) < self.min_working_proxies:
            print(f"有效代理不足,10秒后再次尝试...")
            time.sleep(10)
            self.refresh_proxies()
  
    def get_random_proxy(self) -> Optional[Proxy]:
        """获取一个随机的有效代理"""
        # 如果有效代理不足,刷新代理池
        if len(self.working_proxies) < self.min_working_proxies:
            self.refresh_proxies()
    
        if self.working_proxies:
            # 优先选择成功率高的代理
            self.working_proxies.sort(key=lambda p: p.fail_count / (p.success_count + p.fail_count + 1))
            return random.choice(self.working_proxies[:int(len(self.working_proxies)*0.7)])
        return None

# 使用示例
if __name__ == "__main__":
    proxy_pool = ProxyPool(min_working_proxies=3)
    proxy_pool.refresh_proxies()
  
    for i in range(5):
        proxy = proxy_pool.get_random_proxy()
        if proxy:
            print(f"第{i+1}次获取的代理: {proxy.url}")
        time.sleep(2)

在 Selenium 中使用代理

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from proxy_pool import ProxyPool, Proxy
import time

def create_driver_with_proxy(proxy: Proxy):
    """创建使用指定代理的浏览器实例"""
    chrome_options = Options()
  
    # 设置代理
    chrome_options.add_argument(f'--proxy-server={proxy.url}')
  
    # 添加其他必要配置
    chrome_options.add_argument("--disable-blink-features=AutomationControlled")
    chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
  
    # 创建驱动
    driver = webdriver.Chrome(options=chrome_options)
  
    return driver

def test_proxy_rotation():
    """测试代理轮换功能"""
    # 初始化代理池
    proxy_pool = ProxyPool(min_working_proxies=3)
  
    # 测试多个代理
    for i in range(3):
        # 获取随机代理
        proxy = proxy_pool.get_random_proxy()
        if not proxy:
            print("没有可用代理,测试结束")
            break
    
        print(f"\n第{i+1}次测试,使用代理: {proxy.url}")
  
        try:
            # 创建带代理的浏览器
            driver = create_driver_with_proxy(proxy)
    
            # 访问IP查询网站验证IP
            driver.get("https://api.ipify.org?format=json")
            time.sleep(2)
    
            # 获取当前IP
            ip_info = driver.find_element("tag name", "pre").text
            print(f"当前IP信息: {ip_info}")
    
            # 进行其他操作...
            driver.get("https://example.com")
            time.sleep(3)
    
        except Exception as e:
            print(f"使用代理时出错: {e}")
            proxy.fail_count += 1
        finally:
            if 'driver' in locals():
                driver.quit()

if __name__ == "__main__":
    test_proxy_rotation()

动态代理池要点

  1. 代理类型选择:
    • 透明代理:会泄露真实 IP,不推荐
    • 匿名代理:隐藏真实 IP,但会表明是代理
    • 高匿代理:完全隐藏真实 IP 和代理身份,推荐使用
  2. 代理池维护:
    • 定期验证代理有效性
    • 记录代理成功率和响应时间
    • 自动剔除失效代理
    • 按需补充新代理
  3. 轮换策略:
    • 固定间隔轮换
    • 基于请求数量轮换
    • 检测到异常时立即轮换
    • 随机化轮换间隔,避免规律性
4. 验证码处理

验证码是网站防止自动化访问的常用手段,处理验证码需要结合多种技术方案。

第三方 API 处理验证码

import requests
import base64
import time
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

class CaptchaSolver:
    """验证码解决器,使用第三方API"""
  
    def __init__(self, api_key):
        self.api_key = api_key
        # 示例使用2Captcha API,你可以替换为其他服务
        self.base_url = "http://2captcha.com/in.php"
        self.result_url = "http://2captcha.com/res.php"
  
    def solve_image_captcha(self, image_path=None, image_base64=None):
        """
        解决图片验证码
        :param image_path: 图片路径
        :param image_base64: 图片的base64编码
        :return: 验证码识别结果
        """
        if not image_path and not image_base64:
            raise ValueError("必须提供图片路径或base64编码")
  
        # 获取图片的base64编码
        if not image_base64:
            with open(image_path, "rb") as f:
                image_base64 = base64.b64encode(f.read()).decode('utf-8')
  
        # 发送验证码到API
        params = {
            "key": self.api_key,
            "method": "base64",
            "body": image_base64,
            "json": 1
        }
  
        response = requests.post(self.base_url, data=params)
        result = response.json()
  
        if result["status"] != 1:
            raise Exception(f"提交验证码失败: {result['request']}")
  
        captcha_id = result["request"]
        print(f"验证码提交成功,ID: {captcha_id}")
  
        # 轮询等待结果
        for _ in range(30):  # 最多等待30次
            params = {
                "key": self.api_key,
                "action": "get",
                "id": captcha_id,
                "json": 1
            }
    
            response = requests.get(self.result_url, params=params)
            result = response.json()
    
            if result["status"] == 1:
                return result["request"]
    
            time.sleep(2)  # 每2秒查询一次
  
        raise Exception("获取验证码结果超时")
  
    def solve_recaptcha(self, site_key, page_url):
        """
        解决reCAPTCHA验证码
        :param site_key: 网站的reCAPTCHA site key
        :param page_url: 包含验证码的页面URL
        :return: 验证码识别结果
        """
        # 发送reCAPTCHA到API
        params = {
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": site_key,
            "pageurl": page_url,
            "json": 1
        }
  
        response = requests.post(self.base_url, data=params)
        result = response.json()
  
        if result["status"] != 1:
            raise Exception(f"提交reCAPTCHA失败: {result['request']}")
  
        captcha_id = result["request"]
        print(f"reCAPTCHA提交成功,ID: {captcha_id}")
  
        # 轮询等待结果
        for _ in range(60):  # reCAPTCHA可能需要更长时间
            params = {
                "key": self.api_key,
                "action": "get",
                "id": captcha_id,
                "json": 1
            }
    
            response = requests.get(self.result_url, params=params)
            result = response.json()
    
            if result["status"] == 1:
                return result["request"]
    
            time.sleep(5)  # 每5秒查询一次
  
        raise Exception("获取reCAPTCHA结果超时")

def handle_captcha_in_page(driver, solver):
    """在页面中处理验证码"""
    try:
        # 检查是否存在图片验证码
        captcha_image = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.ID, "captcha-image"))
        )
  
        # 截取验证码图片
        captcha_screenshot = captcha_image.screenshot_as_png
        image_base64 = base64.b64encode(captcha_screenshot).decode('utf-8')
  
        # 识别验证码
        captcha_text = solver.solve_image_captcha(image_base64=image_base64)
        print(f"识别到的验证码: {captcha_text}")
  
        # 输入验证码
        captcha_input = driver.find_element(By.ID, "captcha-input")
        captcha_input.clear()
        captcha_input.send_keys(captcha_text)
  
        return True
  
    except Exception as e:
        print(f"处理图片验证码时出错: {e}")
  
    try:
        # 检查是否存在reCAPTCHA
        recaptcha_frame = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, "iframe[src*='recaptcha']"))
        )
  
        # 获取site key
        site_key = driver.find_element(By.CSS_SELECTOR, "div.g-recaptcha").get_attribute("data-sitekey")
        page_url = driver.current_url
  
        # 解决reCAPTCHA
        recaptcha_response = solver.solve_recaptcha(site_key, page_url)
  
        # 将结果注入页面
        driver.execute_script(f"""
            document.getElementById("g-recaptcha-response").innerHTML = "{recaptcha_response}";
            document.querySelector("form").dispatchEvent(new Event('submit', {{bubbles: true}}));
        """)
  
        return True
  
    except Exception as e:
        print(f"处理reCAPTCHA时出错: {e}")
  
    return False

# 使用示例
if __name__ == "__main__":
    # 初始化验证码解决器(替换为你的API密钥)
    captcha_solver = CaptchaSolver(api_key="YOUR_2CAPTCHA_API_KEY")
  
    # 创建浏览器实例
    driver = webdriver.Chrome()
    driver.get("https://example.com/login-with-captcha")
  
    # 输入用户名和密码
    driver.find_element(By.ID, "username").send_keys("testuser")
    driver.find_element(By.ID, "password").send_keys("testpass")
  
    # 处理验证码
    if handle_captcha_in_page(driver, captcha_solver):
        print("验证码处理成功,等待登录结果...")
        time.sleep(5)
    else:
        print("无法自动处理验证码,请手动处理")
        input("处理完成后按Enter键继续...")
  
    driver.quit()

人工辅助处理验证码

对于复杂的验证码,自动识别可能成功率不高,此时可以采用人工辅助的方式。

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
import time
import os
from datetime import datetime

class ManualCaptchaHandler:
    """人工辅助验证码处理器"""
  
    def __init__(self, screenshot_dir="captcha_screenshots"):
        self.screenshot_dir = screenshot_dir
        # 创建截图目录
        if not os.path.exists(self.screenshot_dir):
            os.makedirs(self.screenshot_dir)
  
    def save_captcha_screenshot(self, driver, element=None):
        """
        保存验证码截图
        :param driver: WebDriver实例
        :param element: 验证码元素(可选)
        :return: 截图路径
        """
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        screenshot_path = os.path.join(self.screenshot_dir, f"captcha_{timestamp}.png")
  
        if element:
            # 只截取验证码元素
            element.screenshot(screenshot_path)
        else:
            # 截取整个页面
            driver.save_screenshot(screenshot_path)
    
        print(f"验证码截图已保存至: {screenshot_path}")
        return screenshot_path
  
    def wait_for_manual_solution(self, driver, timeout=300):
        """
        等待人工输入验证码
        :param driver: WebDriver实例
        :param timeout: 超时时间(秒)
        :return: 是否成功
        """
        try:
            print("请手动解决验证码,完成后按Enter键继续...")
    
            # 等待用户输入
            input("验证码已解决? (按Enter继续)")
    
            # 检查是否已通过验证
            # 这里需要根据实际网站的验证成功标识进行修改
            WebDriverWait(driver, 10).until(
                EC.invisibility_of_element_located((By.ID, "captcha-container"))
            )
    
            print("验证码已通过")
            return True
    
        except Exception as e:
            print(f"等待人工处理验证码超时或失败: {e}")
            return False

def process_page_with_captcha(driver):
    """处理包含验证码的页面"""
    captcha_handler = ManualCaptchaHandler()
  
    try:
        # 检查是否存在验证码
        captcha_container = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.ID, "captcha-container"))
        )
        print("检测到验证码")
  
        # 尝试找到验证码图片元素
        try:
            captcha_image = driver.find_element(By.ID, "captcha-image")
            captcha_handler.save_captcha_screenshot(driver, captcha_image)
        except:
            # 如果找不到特定元素,截取整个页面
            captcha_handler.save_captcha_screenshot(driver)
  
        # 等待人工处理
        return captcha_handler.wait_for_manual_solution(driver)
  
    except:
        print("未检测到验证码")
        return True

# 使用示例
if __name__ == "__main__":
    driver = webdriver.Chrome()
    driver.get("https://example.com/protected-page")
  
    # 处理页面中的验证码
    if process_page_with_captcha(driver):
        print("继续执行后续操作...")
        # 执行其他操作...
        time.sleep(5)
    else:
        print("验证码处理失败,无法继续")
  
    driver.quit()

验证码处理要点

  1. 验证码类型及应对策略:
    • 图片验证码:使用 OCR 或第三方 API 识别
    • 滑块验证码:模拟人类滑动行为
    • 点击验证码:如 reCAPTCHA,需专用 API
    • 语音验证码:转文本后识别
  2. 混合处理策略:
    • 优先尝试自动识别
    • 自动识别失败时切换到人工处理
    • 记录验证码类型和成功率,优化处理方案
    • 对频繁出现的验证码,考虑针对性优化
  3. 注意事项:
    • 第三方 API 需要付费,需权衡成本和收益
    • 过于频繁的验证码请求可能导致账号风险
    • 结合 IP 轮换和行为模拟,减少验证码出现频率