Python Selenium 完全指南:从入门到精通
📚 目录
- 环境准备与基础入门
- 元素定位与交互操作
- 等待机制与异常处理
- 面向对象封装与框架设计
- 进阶技巧与最佳实践
- 性能优化与调试技巧
- 实战案例分析
环境准备与基础入门
1. 安装 Selenium 与浏览器驱动
安装 Selenium
# 使用pip安装最新版本
pip install selenium# 安装特定版本
pip install selenium==4.10.0# 在虚拟环境中安装(推荐)
python -m venv selenium_env
source selenium_env/bin/activate # Linux/Mac
selenium_env\Scripts\activate.bat # Windows
pip install selenium
安装浏览器驱动
从Selenium 4.0开始,提供了自动管理驱动的功能,但了解手动安装方法仍然很重要:
Chrome浏览器:
- 访问 ChromeDriver 下载页面
- 下载与本地Chrome版本匹配的驱动程序
- 将驱动添加到系统PATH中或在代码中指定路径
Firefox浏览器:
- 访问 GeckoDriver 下载页面
- 下载适用于你操作系统的版本
- 将驱动添加到系统PATH中或在代码中指定路径
Edge浏览器:
- 访问 Microsoft Edge Driver 下载页面
- 下载与本地Edge版本匹配的驱动程序
Safari浏览器:
- Safari驱动已内置于macOS中
- 需要在Safari浏览器中启用开发者模式
2. Selenium 4.x 新特性
Selenium 4.x引入了许多重要的改进和新功能:
- 相对定位器:允许基于其他元素的位置来查找元素
- Service对象:用于更好地管理驱动程序服务
- WebDriver Manager:自动管理驱动程序的下载和设置
- CDP(Chrome DevTools Protocol)支持:允许访问浏览器特定的功能
3. WebDriver初始化方法
使用Selenium Manager(推荐,Selenium 4.x)
from selenium import webdriver
from selenium.webdriver.chrome.service import Service# 自动管理驱动
driver = webdriver.Chrome()
传统方法(指定驱动路径)
from selenium import webdriver
from selenium.webdriver.chrome.service import Service# 指定驱动路径
service = Service(executable_path='/path/to/chromedriver')
driver = webdriver.Chrome(service=service)
配置浏览器选项
from selenium import webdriver
from selenium.webdriver.chrome.options import Options# 创建Chrome选项对象
chrome_options = Options()
chrome_options.add_argument("--headless") # 无头模式
chrome_options.add_argument("--window-size=1920,1080") # 设置窗口大小
chrome_options.add_argument("--disable-gpu") # 禁用GPU加速
chrome_options.add_argument("--disable-extensions") # 禁用扩展
chrome_options.add_argument("--proxy-server='direct://'") # 代理设置
chrome_options.add_argument("--proxy-bypass-list=*") # 绕过代理
chrome_options.add_argument("--start-maximized") # 启动时最大化窗口
chrome_options.add_experimental_option("prefs", {"download.default_directory": "/path/to/download/directory", # 设置下载目录"download.prompt_for_download": False, # 禁用下载提示"download.directory_upgrade": True,"safebrowsing.enabled": True
})# 初始化WebDriver
driver = webdriver.Chrome(options=chrome_options)
4. 基础浏览器操作
from selenium import webdriver# 初始化WebDriver
driver = webdriver.Chrome()# 窗口操作
driver.maximize_window() # 最大化窗口
driver.set_window_size(1920, 1080) # 设置窗口大小
driver.set_window_position(0, 0) # 设置窗口位置# 导航操作
driver.get('https://www.example.com') # 打开URL
driver.back() # 后退
driver.forward() # 前进
driver.refresh() # 刷新页面# 页面信息
title = driver.title # 获取页面标题
url = driver.current_url # 获取当前URL
page_source = driver.page_source # 获取页面源代码# Cookie操作
driver.add_cookie({"name": "key", "value": "value"}) # 添加Cookie
cookies = driver.get_cookies() # 获取所有Cookies
driver.delete_cookie("key") # 删除特定Cookie
driver.delete_all_cookies() # 删除所有Cookies# 关闭操作
driver.close() # 关闭当前标签页
driver.quit() # 关闭浏览器,释放资源
5. 常见浏览器配置
无头模式(Headless)
from selenium import webdriver
from selenium.webdriver.chrome.options import Optionschrome_options = Options()
chrome_options.add_argument("--headless")
driver = webdriver.Chrome(options=chrome_options)
使用代理
from selenium import webdriver
from selenium.webdriver.chrome.options import Optionschrome_options = Options()
chrome_options.add_argument('--proxy-server=http://proxyserver:port')
driver = webdriver.Chrome(options=chrome_options)
禁用图片加载(提高性能)
from selenium import webdriver
from selenium.webdriver.chrome.options import Optionschrome_options = Options()
prefs = {"profile.managed_default_content_settings.images": 2}
chrome_options.add_experimental_option("prefs", prefs)
driver = webdriver.Chrome(options=chrome_options)
元素定位与交互操作
1. 元素定位基础
Selenium提供了多种定位元素的方法,每种都有其适用场景:
from selenium import webdriver
from selenium.webdriver.common.by import Bydriver = webdriver.Chrome()
driver.get("https://www.example.com")# 1. 通过ID定位(最推荐,高效且唯一)
element = driver.find_element(By.ID, "login-button")# 2. 通过Name属性定位
element = driver.find_element(By.NAME, "username")# 3. 通过Class Name定位(不唯一时返回第一个匹配元素)
element = driver.find_element(By.CLASS_NAME, "login-form")# 4. 通过Tag Name定位
element = driver.find_element(By.TAG_NAME, "button")# 5. 通过Link Text定位(完全匹配)
element = driver.find_element(By.LINK_TEXT, "Forgot Password?")# 6. 通过Partial Link Text定位(部分匹配)
element = driver.find_element(By.PARTIAL_LINK_TEXT, "Forgot")# 7. 通过CSS选择器定位(强大且灵活)
element = driver.find_element(By.CSS_SELECTOR, "#login-form .submit-button")# 8. 通过XPath定位(最强大但可能较慢)
element = driver.find_element(By.XPATH, "//div[@id='login-form']//button")
2. 高级定位策略
XPath进阶用法
# 绝对路径(从根节点开始)
element = driver.find_element(By.XPATH, "/html/body/div/form/input")# 相对路径(从任意节点开始)
element = driver.find_element(By.XPATH, "//input[@name='username']")# 使用contains()函数
element = driver.find_element(By.XPATH, "//button[contains(@class, 'login')]")# 使用text()函数
element = driver.find_element(By.XPATH, "//a[text()='Forgot Password?']")
element = driver.find_element(By.XPATH, "//a[contains(text(), 'Forgot')]")# 使用AND和OR操作符
element = driver.find_element(By.XPATH, "//input[@type='text' and @name='username']")
element = driver.find_element(By.XPATH, "//button[@type='submit' or @type='button']")# 通过父子关系定位
element = driver.find_element(By.XPATH, "//form[@id='login-form']/input")
parent = driver.find_element(By.XPATH, "//input[@id='username']/..")# 通过兄弟关系定位
element = driver.find_element(By.XPATH, "//input[@id='username']/following-sibling::input")
element = driver.find_element(By.XPATH, "//input[@id='password']/preceding-sibling::input")# 按索引定位
element = driver.find_element(By.XPATH, "(//input[@type='text'])[2]")# 使用轴(axes)
element = driver.find_element(By.XPATH, "//input[@id='username']/ancestor::form")
element = driver.find_element(By.XPATH, "//form/descendant::input")
CSS选择器进阶用法
# 基本选择器
element = driver.find_element(By.CSS_SELECTOR, "#login-button") # ID选择器
element = driver.find_element(By.CSS_SELECTOR, ".login-form") # Class选择器
element = driver.find_element(By.CSS_SELECTOR, "input") # 标签选择器# 属性选择器
element = driver.find_element(By.CSS_SELECTOR, "input[name='username']")
element = driver.find_element(By.CSS_SELECTOR, "input[name^='user']") # 以user开头
element = driver.find_element(By.CSS_SELECTOR, "input[name$='name']") # 以name结尾
element = driver.find_element(By.CSS_SELECTOR, "input[name*='erna']") # 包含erna# 组合选择器
element = driver.find_element(By.CSS_SELECTOR, "form input[type='text']")
element = driver.find_element(By.CSS_SELECTOR, "form > input") # 直接子元素
element = driver.find_element(By.CSS_SELECTOR, "label + input") # 紧邻兄弟元素
element = driver.find_element(By.CSS_SELECTOR, "label ~ input") # 通用兄弟元素# 伪类选择器
element = driver.find_element(By.CSS_SELECTOR, "input:first-child")
element = driver.find_element(By.CSS_SELECTOR, "input:last-child")
element = driver.find_element(By.CSS_SELECTOR, "input:nth-child(2)")
相对定位器(Selenium 4.x新特性)
from selenium.webdriver.support.relative_locator import locate_with# 获取参考元素
username_field = driver.find_element(By.ID, "username")# 使用相对定位器
password_field = driver.find_element(locate_with(By.TAG_NAME, "input").below(username_field))
login_button = driver.find_element(locate_with(By.TAG_NAME, "button").below(password_field))
remember_me = driver.find_element(locate_with(By.TAG_NAME, "input").to_right_of(password_field))
forgot_password = driver.find_element(locate_with(By.TAG_NAME, "a").above(login_button))
3. 查找多个元素
# 查找所有符合条件的元素
elements = driver.find_elements(By.CSS_SELECTOR, ".product-item")# 遍历元素列表
for element in elements:name = element.find_element(By.CLASS_NAME, "product-name").textprice = element.find_element(By.CLASS_NAME, "product-price").textprint(f"产品名称: {name}, 价格: {price}")
4. 元素交互操作
# 输入操作
element.send_keys("test@example.com") # 输入文本
element.send_keys(Keys.CONTROL, 'a') # 键盘组合键(全选)
element.send_keys(Keys.BACK_SPACE) # 退格键# 点击操作
element.click() # 点击元素
element.submit() # 提交表单(适用于表单元素内)# 清除操作
element.clear() # 清除文本输入框# 获取元素属性和状态
value = element.get_attribute("value") # 获取属性值
text = element.text # 获取元素文本内容
tag = element.tag_name # 获取标签名
size = element.size # 获取元素大小
location = element.location # 获取元素位置
is_enabled = element.is_enabled() # 元素是否启用
is_selected = element.is_selected() # 元素是否选中(复选框、单选按钮等)
is_displayed = element.is_displayed() # 元素是否可见# 特殊元素操作
# 下拉菜单
from selenium.webdriver.support.select import Select
select = Select(driver.find_element(By.ID, "dropdown"))
select.select_by_visible_text("Option 1") # 通过文本选择
select.select_by_value("option1") # 通过值选择
select.select_by_index(1) # 通过索引选择
options = select.options # 获取所有选项
first_option = select.first_selected_option # 获取当前选中选项
select.deselect_all() # 取消所有选择(多选下拉框)# 复选框和单选按钮
checkbox = driver.find_element(By.ID, "checkbox")
if not checkbox.is_selected():checkbox.click()
5. 元素查找最佳实践
- 性能优化顺序:ID > Name > CSS > XPath
- 避免使用:
- 绝对XPath路径(容易失效)
- 基于视觉位置的选择器
- 多级嵌套CSS选择器
- 推荐使用:
- 有意义的ID和名称属性
- 数据测试属性(如
data-testid
) - 短而明确的CSS选择器
- 建议添加:
- 页面加载和元素的等待机制
- 查找元素的超时和重试机制
- 详细的错误处理机制
等待机制与异常处理
1. 等待策略
在Web自动化中,页面加载和元素渲染需要时间,等待机制至关重要。
隐式等待(Implicit Wait)
# 设置隐式等待时间(全局设置)
driver.implicitly_wait(10) # 等待最多10秒直到元素出现
隐式等待会在查找元素时自动等待一段时间直到元素出现,如果在指定时间内未找到元素,则抛出异常。
显式等待(Explicit Wait)
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC# 等待元素可见
element = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "element_id"))
)# 等待元素可点击
element = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "button_id"))
)# 等待页面标题包含特定文本
WebDriverWait(driver, 10).until(EC.title_contains("Home Page")
)# 等待元素消失
WebDriverWait(driver, 10).until(EC.invisibility_of_element_located((By.CLASS_NAME, "loading"))
)# 等待警告框出现
WebDriverWait(driver, 10).until(EC.alert_is_present()
)# 等待元素的文本内容满足条件
WebDriverWait(driver, 10).until(EC.text_to_be_present_in_element((By.ID, "status"), "Success")
)# 等待元素的属性值满足条件
WebDriverWait(driver, 10).until(EC.text_to_be_present_in_element_attribute((By.ID, "input"), "value", "text")
)
自定义等待条件
from selenium.webdriver.support.ui import WebDriverWait# 自定义等待条件
def element_has_class(element, class_name):return class_name in element.get_attribute("class").split()# 使用自定义等待条件
element = driver.find_element(By.ID, "myElement")
WebDriverWait(driver, 10).until(lambda driver: element_has_class(element, "active"))
流畅等待(FluentWait)
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException# 创建FluentWait实例
wait = WebDriverWait(driver,timeout=30,poll_frequency=2, # 每2秒检查一次ignored_exceptions=[NoSuchElementException, StaleElementReferenceException]
)# 使用FluentWait
element = wait.until(EC.element_to_be_clickable((By.ID, "myElement")))
2. 异常处理
Selenium操作可能会触发各种异常,合理的异常处理可以提高脚本的健壮性。
常见异常类型
from selenium.common.exceptions import (NoSuchElementException, # 元素未找到TimeoutException, # 等待超时ElementNotVisibleException, # 元素不可见ElementNotInteractableException, # 元素不可交互StaleElementReferenceException, # 元素已过时(DOM已更新)WebDriverException, # WebDriver通用异常InvalidSelectorException, # 无效的选择器UnexpectedAlertPresentException, # 意外的警告框NoAlertPresentException, # 没有警告框SessionNotCreatedException, # 会话创建失败ElementClickInterceptedException # 元素点击被拦截
)
基本异常处理
try:element = driver.find_element(By.ID, "non_existent_element")element.click()
except NoSuchElementException:print("元素未找到")
except ElementNotInteractableException:print("元素不可交互")
except Exception as e:print(f"发生其他异常: {e}")
重试机制
def retry_click(driver, by, value, max_attempts=3, wait_time=1):"""尝试多次点击元素"""from time import sleepfor attempt in range(max_attempts):try:element = driver.find_element(by, value)element.click()return Trueexcept (NoSuchElementException, ElementNotInteractableException, ElementClickInterceptedException, StaleElementReferenceException) as e:if attempt == max_attempts - 1:print(f"无法点击元素,错误: {e}")return Falsesleep(wait_time)return False
处理StaleElementReferenceException
def get_fresh_element(driver, by, value):"""获取一个新鲜的元素引用,避免StaleElementReferenceException"""try:return driver.find_element(by, value)except StaleElementReferenceException:# 重新查找元素return driver.find_element(by, value)
使用装饰器处理异常
import functools
from time import sleepdef retry(max_attempts=3, wait_time=1):"""函数重试装饰器"""def decorator(func):@functools.wraps(func)def wrapper(*args, **kwargs):for attempt in range(max_attempts):try:return func(*args, **kwargs)except (NoSuchElementException, ElementNotInteractableException, StaleElementReferenceException) as e:if attempt == max_attempts - 1:raise esleep(wait_time)return wrapperreturn decorator# 使用装饰器
@retry(max_attempts=5, wait_time=2)
def click_element(driver, by, value):driver.find_element(by, value).click()
3. 等待策略最佳实践
- 避免使用
time.sleep()
:不灵活且低效 - 优先使用显式等待:更精确,可控性更强
- 结合使用隐式等待和显式等待:隐式等待作为全局保护,显式等待针对特定场景
- 设置合理的超时时间:不要过长或过短
- 捕获并处理超时异常:提供适当的恢复机制或用户友好的错误信息
- 为不同网络环境调整等待策略:可配置的超时参数
面向对象封装与框架设计
1. 页面对象模型(Page Object Model, POM)
页面对象模型是一种设计模式,将页面的元素和操作封装在类中,使测试代码更加清晰和可维护。
基本POM结构
class BasePage:"""所有页面的基类"""def __init__(self, driver):self.driver = driverdef find_element(self, locator):return self.driver.find_element(*locator)def find_elements(self, locator):return self.driver.find_elements(*locator)def click(self, locator):self.find_element(locator).click()def input_text(self, locator, text):element = self.find_element(locator)element.clear()element.send_keys(text)def get_text(self, locator):return self.find_element(locator).textdef is_element_present(self, locator):try:self.find_element(locator)return Trueexcept NoSuchElementException:return Falsedef wait_for_element(self, locator, timeout=10):try:WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located(locator))return Trueexcept TimeoutException:return Falseclass LoginPage(BasePage):"""登录页面对象"""# 页面元素定位器_username_field = (By.ID, "username")_password_field = (By.ID, "password")_login_button = (By.ID, "login_button")_error_message = (By.CLASS_NAME, "error-message")def __init__(self, driver):super().__init__(driver)self.driver.get("https://example.com/login")def enter_username(self, username):self.input_text(self._username_field, username)return selfdef enter_password(self, password):self.input_text(self._password_field, password)return selfdef click_login(self):self.click(self._login_button)# 根据登录结果返回不同的页面对象if "dashboard" in self.driver.current_url:return DashboardPage(self.driver)return selfdef login(self, username, password):self.enter_username(username)self.enter_password(password)return self.click_login()def get_error_message(self):if self.is_element_present(self._error_message):return self.get_text(self._error_message)return ""class DashboardPage(BasePage):"""仪表盘页面对象"""_welcome_message = (By.ID, "welcome")_logout_button = (By.ID, "logout")def is_loaded(self):return self.wait_for_element(self._welcome_message)def get_welcome_message(self):return self.get_text(self._welcome_message)def logout(self):self.click(self._logout_button)return LoginPage(self.driver)
使用POM进行测试
def test_login_success():driver = webdriver.Chrome()try:login_page = LoginPage(driver)dashboard_page = login_page.login("valid_user", "valid_password")assert dashboard_page.is_loaded()assert "Welcome" in dashboard_page.get_welcome_message()finally:driver.quit()def test_login_failure():driver = webdriver.Chrome()try:login_page = LoginPage(driver)result_page = login_page.login("invalid_user", "invalid_password")assert isinstance(result_page, LoginPage)assert "Invalid credentials" in result_page.get_error_message()finally:driver.quit()
2. 测试框架集成
与unittest集成
import unittest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManagerclass TestLogin(unittest.TestCase):def setUp(self):self.driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))self.driver.maximize_window()self.login_page = LoginPage(self.driver)def tearDown(self):self.driver.quit()def test_valid_login(self):dashboard_page = self.login_page.login("valid_user", "valid_password")self.assertTrue(dashboard_page.is_loaded())self.assertIn("Welcome", dashboard_page.get_welcome_message())def test_invalid_login(self):result_page = self.login_page.login("invalid_user", "invalid_password")self.assertIsInstance(result_page, LoginPage)self.assertIn("Invalid credentials", result_page.get_error_message())if __name__ == "__main__":unittest.main()
与pytest集成
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager@pytest.fixture
def driver():# 设置driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))driver.maximize_window()yield driver# 清理driver.quit()@pytest.fixture
def login_page(driver):return LoginPage(driver)def test_valid_login(login_page):dashboard_page = login_page.login("valid_user", "valid_password")assert dashboard_page.is_loaded()assert "Welcome" in dashboard_page.get_welcome_message()def test_invalid_login(login_page):result_page = login_page.login("invalid_user", "invalid_password")assert isinstance(result_page, LoginPage)assert "Invalid credentials" in result_page.get_error_message()
与Behave(BDD)集成
# features/login.feature
Feature: User LoginAs a userI want to be able to login to the applicationSo that I can access my accountScenario: Successful login with valid credentialsGiven the user is on the login pageWhen the user enters "valid_user" as usernameAnd the user enters "valid_password" as passwordAnd the user clicks the login buttonThen the user should be redirected to the dashboardAnd the dashboard should display a welcome messageScenario: Failed login with invalid credentialsGiven the user is on the login pageWhen the user enters "invalid_user" as usernameAnd the user enters "invalid_password" as passwordAnd the user clicks the login buttonThen the user should remain on the login pageAnd an error message should be displayed
# steps/login_steps.py
from behave import given, when, then
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage@given('the user is on the login page')
def step_impl(context):context.login_page = LoginPage(context.driver)@when('the user enters "{username}" as username')
def step_impl(context, username):context.login_page.enter_username(username)@when('the user enters "{password}" as password')
def step_impl(context, password):context.login_page.enter_password(password)@when('the user clicks the login button')
def step_impl(context):context.result_page = context.login_page.click_login()@then('the user should be redirected to the dashboard')
def step_impl(context):assert isinstance(context.result_page, DashboardPage)@then('the dashboard should display a welcome message')
def step_impl(context):assert "Welcome" in context.result_page.get_welcome_message()@then('the user should remain on the login page')
def step_impl(context):assert isinstance(context.result_page, LoginPage)@then('an error message should be displayed')
def step_impl(context):assert "Invalid credentials" in context.result_page.get_error_message()
3. 高级框架设计模式
工厂模式
class PageFactory:"""页面对象工厂类"""@staticmethoddef get_page(page_name, driver):pages = {"login": LoginPage,"dashboard": DashboardPage,"profile": ProfilePage,"settings": SettingsPage}if page_name.lower() not in pages:raise ValueError(f"不支持的页面: {page_name}")return pages[page_name.lower()](driver)
单例模式(驱动管理器)
class WebDriverManager:"""WebDriver管理器(单例模式)"""_instance = Nonedef __new__(cls):if cls._instance is None:cls._instance = super(WebDriverManager, cls).__new__(cls)cls._instance.driver = Nonereturn cls._instancedef get_driver(self, browser="chrome"):if self.driver is None:if browser.lower() == "chrome":self.driver = webdriver.Chrome()elif browser.lower() == "firefox":self.driver = webdriver.Firefox()else:raise ValueError(f"不支持的浏览器: {browser}")self.driver.maximize_window()self.driver.implicitly_wait(10)return self.driverdef quit(self):if self.driver:self.driver.quit()self.driver = None
策略模式(等待策略)
from abc import ABC, abstractmethodclass WaitStrategy(ABC):"""等待策略基类"""@abstractmethoddef wait_for(self, driver, locator):passclass VisibilityStrategy(WaitStrategy):"""等待元素可见策略"""def wait_for(self, driver, locator, timeout=10):return WebDriverWait(driver, timeout).until(EC.visibility_of_element_located(locator))class ClickableStrategy(WaitStrategy):"""等待元素可点击策略"""def wait_for(self, driver, locator, timeout=10):return WebDriverWait(driver, timeout).until(EC.element_to_be_clickable(locator))class PresenceStrategy(WaitStrategy):"""等待元素存在策略"""def wait_for(self, driver, locator, timeout=10):return WebDriverWait(driver, timeout).until(EC.presence_of_element_located(locator))# 使用策略模式的高级页面基类
class AdvancedBasePage:def __init__(self, driver):self.driver = driverself.wait_strategies = {"visible": VisibilityStrategy(),"clickable": ClickableStrategy(),"present": PresenceStrategy()}def find_element(self, locator, strategy="present"):return self.wait_strategies[strategy].wait_for(self.driver, locator)
4. 配置与日志管理
配置管理
import json
import osclass ConfigManager:"""配置管理器"""_instance = Nonedef __new__(cls):if cls._instance is None:cls._instance = super(ConfigManager, cls).__new__(cls)cls._instance.config = {}cls._instance.load_config()return cls._instancedef load_config(self, config_file="config.json"):if os.path.exists(config_file):with open(config_file, "r") as f:self.config = json.load(f)else:# 默认配置self.config = {"browser": "chrome","implicit_wait": 10,"explicit_wait": 20,"base_url": "https://example.com","headless": False,"screenshots_dir": "screenshots","logs_dir": "logs"}def get(self, key, default=None):return self.config.get(key, default)
日志管理
import logging
import os
from datetime import datetimeclass LogManager:"""日志管理器"""_instance = Nonedef __new__(cls):if cls._instance is None:cls._instance = super(LogManager, cls).__new__(cls)cls._instance.setup_logger()return cls._instancedef setup_logger(self):config = ConfigManager().get("logs", {})logs_dir = config.get("dir", "logs")log_level = config.get("level", "INFO")if not os.path.exists(logs_dir):os.makedirs(logs_dir)log_file = os.path.join(logs_dir, f"test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")# 设置日志级别映射level_map = {"DEBUG": logging.DEBUG,"INFO": logging.INFO,"WARNING": logging.WARNING,"ERROR": logging.ERROR,"CRITICAL": logging.CRITICAL}# 设置根日志记录器self.logger = logging.getLogger("selenium_framework")self.logger.setLevel(level_map.get(log_level.upper(), logging.INFO))# 文件处理器file_handler = logging.FileHandler(log_file)file_handler.setLevel(level_map.get(log_level.upper(), logging.INFO))# 控制台处理器console_handler = logging.StreamHandler()console_handler.setLevel(level_map.get(log_level.upper(), logging.INFO))# 日志格式formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')file_handler.setFormatter(formatter)console_handler.setFormatter(formatter)# 添加处理器self.logger.addHandler(file_handler)self.logger.addHandler(console_handler)def get_logger(self):return self.logger
进阶技巧与最佳实践
1. 高级交互操作
ActionChains高级操作
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys# 基本鼠标操作
def perform_hover(driver, element):"""执行悬停操作"""ActionChains(driver).move_to_element(element).perform()def perform_right_click(driver, element):"""执行右键点击操作"""ActionChains(driver).context_click(element).perform()def perform_double_click(driver, element):"""执行双击操作"""ActionChains(driver).double_click(element).perform()def perform_drag_and_drop(driver, source_element, target_element):"""执行拖放操作"""ActionChains(driver).drag_and_drop(source_element, target_element).perform()def perform_drag_and_drop_by_offset(driver, element, x_offset, y_offset):"""执行偏移拖放操作"""ActionChains(driver).drag_and_drop_by_offset(element, x_offset, y_offset).perform()# 组合键盘操作
def perform_ctrl_click(driver, element):"""执行Ctrl+点击操作(多选)"""ActionChains(driver).key_down(Keys.CONTROL).click(element).key_up(Keys.CONTROL).perform()def perform_shift_click(driver, element):"""执行Shift+点击操作(范围选择)"""ActionChains(driver).key_down(Keys.SHIFT).click(element).key_up(Keys.SHIFT).perform()def perform_select_all(driver):"""执行全选操作(Ctrl+A)"""ActionChains(driver).key_down(Keys.CONTROL).send_keys('a').key_up(Keys.CONTROL).perform()def perform_copy(driver):"""执行复制操作(Ctrl+C)"""ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()def perform_paste(driver):"""执行粘贴操作(Ctrl+V)"""ActionChains(driver).key_down(Keys.CONTROL).send_keys('v').key_up(Keys.CONTROL).perform()# 链式组合操作
def perform_complex_action(driver, element1, element2):"""执行复杂组合操作"""ActionChains(driver)\.move_to_element(element1)\.pause(1) # 暂停1秒.click()\.move_to_element(element2)\.click()\.perform()
处理JavaScript事件
def trigger_js_event(driver, element, event_name):"""触发JavaScript事件"""js_code = f"arguments[0].dispatchEvent(new Event('{event_name}'));"driver.execute_script(js_code, element)def focus_element(driver, element):"""使元素获取焦点"""driver.execute_script("arguments[0].focus();", element)def blur_element(driver, element):"""使元素失去焦点"""driver.execute_script("arguments[0].blur();", element)def scroll_to_element(driver, element):"""滚动到元素位置"""driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", element)def scroll_to_top(driver):"""滚动到页面顶部"""driver.execute_script("window.scrollTo(0, 0);")def scroll_to_bottom(driver):"""滚动到页面底部"""driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
2. 窗口与标签页管理
def switch_to_window_by_title(driver, title):"""切换到指定标题的窗口"""current_window = driver.current_window_handlefor window in driver.window_handles:driver.switch_to.window(window)if title in driver.title:return True# 如果没有找到匹配标题的窗口,切回原窗口driver.switch_to.window(current_window)return Falsedef switch_to_window_by_url(driver, url_part):"""切换到URL包含指定部分的窗口"""current_window = driver.current_window_handlefor window in driver.window_handles:driver.switch_to.window(window)if url_part in driver.current_url:return True# 如果没有找到匹配URL的窗口,切回原窗口driver.switch_to.window(current_window)return Falsedef close_all_windows_except_current(driver):"""关闭除当前窗口外的所有窗口"""current_window = driver.current_window_handlefor window in driver.window_handles:if window != current_window:driver.switch_to.window(window)driver.close()driver.switch_to.window(current_window)def open_new_tab(driver, url=None):"""打开新标签页"""driver.execute_script("window.open();")driver.switch_to.window(driver.window_handles[-1])if url:driver.get(url)def handle_popup_window(driver, action="accept"):"""处理弹出窗口"""try:if action.lower() == "accept":driver.switch_to.alert.accept()elif action.lower() == "dismiss":driver.switch_to.alert.dismiss()elif action.lower() == "text":return driver.switch_to.alert.textelse:raise ValueError(f"不支持的操作: {action}")return Trueexcept:return False
3. iframe处理
def switch_to_frame_by_index(driver, index):"""通过索引切换到iframe"""try:driver.switch_to.frame(index)return Trueexcept:return Falsedef switch_to_frame_by_name_or_id(driver, name_or_id):"""通过名称或ID切换到iframe"""try:driver.switch_to.frame(name_or_id)return Trueexcept:return Falsedef switch_to_frame_by_element(driver, element):"""通过元素切换到iframe"""try:driver.switch_to.frame(element)return Trueexcept:return Falsedef switch_to_parent_frame(driver):"""切换到父iframe"""try:driver.switch_to.parent_frame()return Trueexcept:return Falsedef switch_to_default_content(driver):"""切换到主文档"""try:driver.switch_to.default_content()return Trueexcept:return Falsedef get_iframe_count(driver):"""获取页面中iframe的数量"""return len(driver.find_elements(By.TAG_NAME, "iframe"))def execute_in_iframe(driver, iframe_locator, action_func):"""在iframe中执行操作"""driver.switch_to.frame(driver.find_element(*iframe_locator))try:result = action_func(driver)return resultfinally:driver.switch_to.default_content()
4. 文件上传与下载
文件上传
def upload_file(driver, file_input_locator, file_path):"""上传文件(适用于<input type="file">元素)"""try:file_input = driver.find_element(*file_input_locator)file_input.send_keys(file_path)return Trueexcept Exception as e:print(f"文件上传失败: {e}")return Falsedef upload_file_without_input(driver, upload_button_locator, file_path):"""上传文件(适用于没有可见<input type="file">的情况)使用JS创建一个隐藏的文件输入元素"""try:# 创建一个隐藏的文件输入元素js_script = """const input = document.createElement('input');input.type = 'file';input.style.display = 'none';input.id = 'hidden-file-input';document.body.appendChild(input);return input;"""file_input = driver.execute_script(js_script)# 设置文件路径file_input.send_keys(file_path)# 触发上传按钮的点击事件upload_button = driver.find_element(*upload_button_locator)driver.execute_script("arguments[0].click();", upload_button)# 移除隐藏的文件输入元素driver.execute_script("document.getElementById('hidden-file-input').remove();")return Trueexcept Exception as e:print(f"文件上传失败: {e}")return False
文件下载
import os
import time
from pathlib import Pathdef setup_chrome_download_path(download_dir):"""设置Chrome浏览器的下载路径"""options = webdriver.ChromeOptions()prefs = {"download.default_directory": download_dir,"download.prompt_for_download": False,"download.directory_upgrade": True,"safebrowsing.enabled": True}options.add_experimental_option("prefs", prefs)return optionsdef wait_for_download_to_complete(download_dir, timeout=60, check_interval=1):"""等待下载完成"""start_time = time.time()while time.time() - start_time < timeout:# 检查是否有部分下载的文件(.crdownload, .part等)downloading_files = list(Path(download_dir).glob("*.crdownload")) + list(Path(download_dir).glob("*.part"))if not downloading_files:# 找出最近下载的文件downloaded_files = list(Path(download_dir).glob("*"))if downloaded_files:downloaded_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)return str(downloaded_files[0])time.sleep(check_interval)raise TimeoutError("文件下载超时")def download_file(driver, download_button_locator, download_dir, timeout=60):"""下载文件"""try:# 确保下载目录存在os.makedirs(download_dir, exist_ok=True)# 点击下载按钮download_button = driver.find_element(*download_button_locator)download_button.click()# 等待下载完成downloaded_file = wait_for_download_to_complete(download_dir, timeout)return downloaded_fileexcept Exception as e:print(f"文件下载失败: {e}")return None
5. 截图与日志
import os
import time
from datetime import datetimedef take_screenshot(driver, directory="screenshots", filename=None):"""截取屏幕截图"""try:# 确保目录存在os.makedirs(directory, exist_ok=True)# 如果未指定文件名,使用时间戳生成if not filename:timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")filename = f"screenshot_{timestamp}.png"# 拼接完整路径file_path = os.path.join(directory, filename)# 截图driver.save_screenshot(file_path)return file_pathexcept Exception as e:print(f"截图失败: {e}")return Nonedef take_element_screenshot(driver, element, directory="screenshots", filename=None):"""截取元素截图"""try:# 确保目录存在os.makedirs(directory, exist_ok=True)# 如果未指定文件名,使用时间戳生成if not filename:timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")filename = f"element_screenshot_{timestamp}.png"# 拼接完整路径file_path = os.path.join(directory, filename)# 截取元素截图element.screenshot(file_path)return file_pathexcept Exception as e:print(f"元素截图失败: {e}")return Nonedef screenshot_on_failure(func):"""失败时自动截图的装饰器"""def wrapper(*args, **kwargs):try:return func(*args, **kwargs)except Exception as e:# 假设第一个参数是self,第二个参数是driverdriver = args[1] if len(args) > 1 else Noneif driver:timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")filename = f"failure_{func.__name__}_{timestamp}.png"take_screenshot(driver, filename=filename)raise ereturn wrapper
6. 高级断言与验证
def verify_element_text(driver, locator, expected_text, contains=False):"""验证元素文本"""try:element = driver.find_element(*locator)actual_text = element.textif contains:assert expected_text in actual_text, f"期望文本包含'{expected_text}',实际文本为'{actual_text}'"else:assert actual_text == expected_text, f"期望文本为'{expected_text}',实际文本为'{actual_text}'"return Trueexcept AssertionError as e:print(f"验证失败: {e}")return Falsedef verify_element_attribute(driver, locator, attribute, expected_value, contains=False):"""验证元素属性"""try:element = driver.find_element(*locator)actual_value = element.get_attribute(attribute)if contains:assert expected_value in actual_value, f"期望属性'{attribute}'包含'{expected_value}',实际值为'{actual_value}'"else:assert actual_value == expected_value, f"期望属性'{attribute}'为'{expected_value}',实际值为'{actual_value}'"return Trueexcept AssertionError as e:print(f"验证失败: {e}")return Falsedef verify_element_visible(driver, locator, timeout=10):"""验证元素可见"""try:WebDriverWait(driver, timeout).until(EC.visibility_of_element_located(locator))return Trueexcept TimeoutException:print(f"元素在{timeout}秒内未可见: {locator}")return Falsedef verify_element_not_visible(driver, locator, timeout=10):"""验证元素不可见"""try:WebDriverWait(driver, timeout).until(EC.invisibility_of_element_located(locator))return Trueexcept TimeoutException:print(f"元素在{timeout}秒内仍然可见: {locator}")return Falsedef verify_url(driver, expected_url, contains=False, timeout=10):"""验证URL"""try:if contains:WebDriverWait(driver, timeout).until(lambda d: expected_url in d.current_url)else:WebDriverWait(driver, timeout).until(lambda d: d.current_url == expected_url)return Trueexcept TimeoutException:print(f"URL验证失败,期望URL{'' if contains else '为'}{expected_url},实际URL为{driver.current_url}")return Falsedef verify_title(driver, expected_title, contains=False, timeout=10):"""验证页面标题"""try:if contains:WebDriverWait(driver, timeout).until(lambda d: expected_title in d.title)else:WebDriverWait(driver, timeout).until(lambda d: d.title == expected_title)return Trueexcept TimeoutException:print(f"标题验证失败,期望标题{'' if contains else '为'}{expected_title},实际标题为{driver.title}")return False
性能优化与调试技巧
1. 性能测试与优化
测量页面加载时间
def measure_page_load_time(driver, url):"""测量页面加载时间"""start_time = time.time()driver.get(url)# 等待页面完全加载WebDriverWait(driver, 60).until(lambda d: d.execute_script("return document.readyState") == "complete")end_time = time.time()load_time = end_time - start_timereturn load_time
使用Performance API获取详细性能数据
def get_performance_metrics(driver):"""获取浏览器性能指标"""# 使用Navigation Timing APInavigation_timing = driver.execute_script("""var performance = window.performance;var timingObj = performance.timing;var loadTime = timingObj.loadEventEnd - timingObj.navigationStart;var dnsTime = timingObj.domainLookupEnd - timingObj.domainLookupStart;var tcpTime = timingObj.connectEnd - timingObj.connectStart;var serverTime = timingObj.responseEnd - timingObj.requestStart;var domTime = timingObj.domComplete - timingObj.domLoading;return {'loadTime': loadTime,'dnsTime': dnsTime,'tcpTime': tcpTime,'serverTime': serverTime,'domTime': domTime,'firstPaint': timingObj.responseStart - timingObj.navigationStart,'ttfb': timingObj.responseStart - timingObj.requestStart};""")return navigation_timingdef get_resource_timing(driver):"""获取资源加载时间"""resources = driver.execute_script("""var resources = window.performance.getEntriesByType('resource');return resources.map(function(resource) {return {'name': resource.name,'startTime': resource.startTime,'duration': resource.duration,'initiatorType': resource.initiatorType,'size': resource.transferSize};});""")return resources
优化执行速度
def optimize_chrome_for_performance():"""优化Chrome浏览器以提高性能"""options = webdriver.ChromeOptions()# 禁用不必要的浏览器功能options.add_argument("--disable-extensions")options.add_argument("--disable-gpu")options.add_argument("--disable-dev-shm-usage")options.add_argument("--disable-browser-side-navigation")options.add_argument("--disable-infobars")options.add_argument("--disable-notifications")options.add_argument("--disable-popup-blocking")# 减少内存使用options.add_argument("--disable-features=site-per-process")options.add_argument("--process-per-site")# 禁用图片加载以提高速度prefs = {"profile.managed_default_content_settings.images": 2,"profile.default_content_setting_values.notifications": 2,"profile.default_content_setting_values.geolocation": 2}options.add_experimental_option("prefs", prefs)# 使用无头模式options.add_argument("--headless")return options
2. 高级调试技巧
获取浏览器控制台日志
def get_browser_logs(driver):"""获取浏览器控制台日志"""logs = driver.get_log('browser')return logsdef print_browser_logs(driver):"""打印浏览器控制台日志"""logs = driver.get_log('browser')for log in logs:print(f"[{log['level']}] {log['message']}")