一、基础夯实
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 及后续版本)围绕 “稳定性提升”“功能扩展”“开发体验优化” 三大方向,推出多个关键新特性:
- WebDriver Bidi(双向通信):突破传统 WebDriver “单向指令” 的局限,实现浏览器与测试脚本的双向数据交互。例如,可通过 Bidi 监听浏览器的网络请求(如 API 接口调用、资源加载)、JavaScript 日志输出、DOM 结构变化等,无需依赖第三方工具(如 Fiddler、Chrome DevTools)即可完成接口与 UI 的联动测试。典型应用场景:在测试电商商品详情页时,可通过 Bidi 监听 “获取商品库存” 的 API 请求,验证返回数据与页面显示库存的一致性,解决 “UI 显示正常但接口数据异常” 的隐性问题。
- 相对定位(Relative Locators):弥补传统绝对定位(如 XPath、ID)在 “元素动态变化” 场景下的不足,支持通过 “相邻元素关系” 定位目标元素,提供 5 种相对定位方法:above(上方)、below(下方)、toLeftOf(左侧)、toRightOf(右侧)、near(附近,默认距离 100px 内)。例如,当测试页面中 “提交按钮” 的 ID 随版本迭代变化,但始终位于 “用户名输入框” 的下方时,可通过
driver.findElement(withTagName("button").below(withId("username")))
定位按钮,提升脚本的可维护性。 - 原生支持 Chrome DevTools Protocol(CDP):Selenium 4.0 及以上版本内置 CDP 接口,可直接调用 Chrome DevTools 的核心功能,如模拟网络限速(测试弱网环境下的页面加载)、设置 Cookie(绕过登录验证)、截取全页面截图(而非传统的可视区域截图)等。例如,在测试视频网站时,通过
driver.executeCdpCommand("Network.emulateNetworkConditions", params)
模拟 2G 网络,验证视频加载的缓冲提示是否正常显示,无需额外集成 Chrome DevTools 相关依赖。 - 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
替换为firefox
或edge
),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()
表单操作要点解析
- 文本输入:使用
send_keys()
方法输入文本,clear()
方法清空内容 - 下拉框处理:
- 对于标准下拉框 (
<select>
标签),推荐使用Select
类 - 对于自定义下拉框,可通过点击触发下拉,再选择选项
- 对于标准下拉框 (
- 单选 / 复选框:
- 使用
is_selected()
检查状态 - 使用
click()
方法切换状态
- 使用
- 表单提交:可以点击提交按钮或使用
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()
鼠标事件要点解析
- ActionChains 工作原理:
- 所有操作会被存储在一个队列中
- 调用
perform()
方法时才会执行所有操作
- 常用鼠标操作:
move_to_element()
:鼠标悬停click()
:左键点击context_click()
:右键点击double_click()
:双击drag_and_drop()
:拖拽到目标元素drag_and_drop_by_offset()
:拖拽指定偏移量
- 注意事项:
- 复杂操作可能需要添加适当的等待时间
- 确保操作元素在执行时可见且可交互
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()
键盘操作要点解析
- Keys 类:提供了所有键盘按键的常量表示,如
Keys.ENTER
、Keys.TAB
等 - 常用组合键:
Keys.CONTROL + 'a'
:全选Keys.CONTROL + 'c'
:复制Keys.CONTROL + 'v'
:粘贴Keys.CONTROL + 'x'
:剪切Keys.SHIFT + Keys.ARROW_LEFT
:向左选中文本
- 跨平台注意事项:
- Windows 和 Linux 使用
Keys.CONTROL
- Mac 使用
Keys.COMMAND
代替Keys.CONTROL
- Windows 和 Linux 使用
- 复杂键盘操作:
- 使用
ActionChains
的key_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 自动化测试中广泛采用的设计模式,它将页面逻辑与测试逻辑分离,提高代码复用性和可维护性。
六大原则
- 封装性:页面元素和操作细节封装在 Page 类内部
- 单一职责:每个 Page 类对应一个页面或页面片段
- 接口抽象:暴露有意义的业务操作,而非技术细节
- 不包含断言:断言应放在测试用例中,而非 Page 类
- 延迟初始化:元素在首次使用时才初始化
- 链式调用:支持流畅的方法调用风格
分层架构
- 基础层:封装通用操作和 Selenium API
- 页面对象层:实现具体页面的元素和操作
- 业务流程层:组合页面操作形成业务流程
- 测试用例层:调用业务流程并进行断言验证
代码实现示例
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)是一种测试方法,它将测试数据与测试逻辑分离,使同一测试逻辑可以用多组不同数据执行。
数据源类型
- Excel/CSV:适合非技术人员维护数据
- JSON:适合结构化数据,易于与 API 测试集成
- 数据库:适合大量数据和动态数据
- 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"]
数据驱动测试最佳实践
- 数据隔离:测试数据应与测试代码完全分离
- 数据验证:加载数据时进行验证,确保格式正确
- 有意义的命名:为每组测试数据提供清晰的名称
- 数据分组:按测试场景或优先级对数据进行分组
- 动态生成:对于大量数据,考虑动态生成而非静态文件
- 敏感数据处理:密码等敏感信息应加密存储或使用环境变量
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()
自动化特征隐藏要点:
- 核心检测点:
navigator.webdriver
属性- 特殊的 User-Agent 模式
- 浏览器窗口大小和行为模式
- 异常的点击和滚动模式
- JavaScript 引擎特征
- 高级隐藏技巧:
- 使用真实浏览器的配置文件
- 模拟真实用户的操作速度和模式
- 随机化操作间隔时间
- 避免固定的操作序列
- 使用 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()
动态代理池要点:
- 代理类型选择:
- 透明代理:会泄露真实 IP,不推荐
- 匿名代理:隐藏真实 IP,但会表明是代理
- 高匿代理:完全隐藏真实 IP 和代理身份,推荐使用
- 代理池维护:
- 定期验证代理有效性
- 记录代理成功率和响应时间
- 自动剔除失效代理
- 按需补充新代理
- 轮换策略:
- 固定间隔轮换
- 基于请求数量轮换
- 检测到异常时立即轮换
- 随机化轮换间隔,避免规律性
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()
验证码处理要点:
- 验证码类型及应对策略:
- 图片验证码:使用 OCR 或第三方 API 识别
- 滑块验证码:模拟人类滑动行为
- 点击验证码:如 reCAPTCHA,需专用 API
- 语音验证码:转文本后识别
- 混合处理策略:
- 优先尝试自动识别
- 自动识别失败时切换到人工处理
- 记录验证码类型和成功率,优化处理方案
- 对频繁出现的验证码,考虑针对性优化
- 注意事项:
- 第三方 API 需要付费,需权衡成本和收益
- 过于频繁的验证码请求可能导致账号风险
- 结合 IP 轮换和行为模拟,减少验证码出现频率
评论