Python Selenium 完全指南:从入门到精通
Python Selenium 完全指南:从入门到精通
📚 目录
环境准备与基础入门
1. 安装 Selenium 与浏览器驱动
安装 Selenium
# 使用pip安装最新版本pip install selenium# 安装特定版本pip install selenium==4.10.0# 在虚拟环境中安装(推荐)python -m venv selenium_envsource selenium_env/bin/activate # Linux/Macselenium_env\\Scripts\\activate.bat # Windowspip 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 webdriverfrom selenium.webdriver.chrome.service import Service# 自动管理驱动driver = webdriver.Chrome()
传统方法(指定驱动路径)
from selenium import webdriverfrom selenium.webdriver.chrome.service import Service# 指定驱动路径service = Service(executable_path=\'/path/to/chromedriver\')driver = webdriver.Chrome(service=service)
配置浏览器选项
from selenium import webdriverfrom 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})# 初始化WebDriverdriver = webdriver.Chrome(options=chrome_options)
4. 基础浏览器操作
from selenium import webdriver# 初始化WebDriverdriver = webdriver.Chrome()# 窗口操作driver.maximize_window() # 最大化窗口driver.set_window_size(1920, 1080) # 设置窗口大小driver.set_window_position(0, 0) # 设置窗口位置# 导航操作driver.get(\'https://www.example.com\') # 打开URLdriver.back() # 后退driver.forward() # 前进driver.refresh() # 刷新页面# 页面信息title = driver.title # 获取页面标题url = driver.current_url # 获取当前URLpage_source = driver.page_source # 获取页面源代码# Cookie操作driver.add_cookie({\"name\": \"key\", \"value\": \"value\"}) # 添加Cookiecookies = driver.get_cookies() # 获取所有Cookiesdriver.delete_cookie(\"key\") # 删除特定Cookiedriver.delete_all_cookies() # 删除所有Cookies# 关闭操作driver.close() # 关闭当前标签页driver.quit() # 关闭浏览器,释放资源
5. 常见浏览器配置
无头模式(Headless)
from selenium import webdriverfrom selenium.webdriver.chrome.options import Optionschrome_options = Options()chrome_options.add_argument(\"--headless\")driver = webdriver.Chrome(options=chrome_options)
使用代理
from selenium import webdriverfrom 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 webdriverfrom 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 webdriverfrom 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\").text price = element.find_element(By.CLASS_NAME, \"product-price\").text print(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 Selectselect = 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 WebDriverWaitfrom 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 WebDriverWaitfrom selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException# 创建FluentWait实例wait = WebDriverWait( driver, timeout=30, poll_frequency=2, # 每2秒检查一次 ignored_exceptions=[NoSuchElementException, StaleElementReferenceException])# 使用FluentWaitelement = 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 sleep for attempt in range(max_attempts): try: element = driver.find_element(by, value) element.click() return True except (NoSuchElementException, ElementNotInteractableException, ElementClickInterceptedException, StaleElementReferenceException) as e: if attempt == max_attempts - 1: print(f\"无法点击元素,错误: {e}\") return False sleep(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 functoolsfrom 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 e sleep(wait_time) return wrapper return 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 = driver def 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).text def is_element_present(self, locator): try: self.find_element(locator) return True except NoSuchElementException: return False def wait_for_element(self, locator, timeout=10): try: WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) ) return True except 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 self def enter_password(self, password): self.input_text(self._password_field, password) return self def click_login(self): self.click(self._login_button) # 根据登录结果返回不同的页面对象 if \"dashboard\" in self.driver.current_url: return DashboardPage(self.driver) return self def 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 unittestfrom selenium import webdriverfrom selenium.webdriver.chrome.service import Servicefrom 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 pytestfrom selenium import webdriverfrom selenium.webdriver.chrome.service import Servicefrom webdriver_manager.chrome import ChromeDriverManager@pytest.fixturedef driver(): # 设置 driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) driver.maximize_window() yield driver # 清理 driver.quit()@pytest.fixturedef 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.featureFeature: User Login As a user I want to be able to login to the application So that I can access my account Scenario: Successful login with valid credentials Given the user is on the login page When the user enters \"valid_user\" as username And the user enters \"valid_password\" as password And the user clicks the login button Then the user should be redirected to the dashboard And the dashboard should display a welcome message Scenario: Failed login with invalid credentials Given the user is on the login page When the user enters \"invalid_user\" as username And the user enters \"invalid_password\" as password And the user clicks the login button Then the user should remain on the login page And an error message should be displayed
# steps/login_steps.pyfrom behave import given, when, thenfrom pages.login_page import LoginPagefrom 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: \"\"\" 页面对象工厂类 \"\"\" @staticmethod def 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 = None def __new__(cls): if cls._instance is None: cls._instance = super(WebDriverManager, cls).__new__(cls) cls._instance.driver = None return cls._instance def 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.driver def quit(self): if self.driver: self.driver.quit() self.driver = None
策略模式(等待策略)
from abc import ABC, abstractmethodclass WaitStrategy(ABC): \"\"\" 等待策略基类 \"\"\" @abstractmethod def 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 = driver self.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 jsonimport osclass ConfigManager: \"\"\" 配置管理器 \"\"\" _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(ConfigManager, cls).__new__(cls) cls._instance.config = {} cls._instance.load_config() return cls._instance def 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 loggingimport osfrom datetime import datetimeclass LogManager: \"\"\" 日志管理器 \"\"\" _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(LogManager, cls).__new__(cls) cls._instance.setup_logger() return cls._instance def 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 ActionChainsfrom 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_handle for 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_handle for 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_handle for 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.text else: raise ValueError(f\"不支持的操作: {action}\") return True except: return False
3. iframe处理
def switch_to_frame_by_index(driver, index): \"\"\"通过索引切换到iframe\"\"\" try: driver.switch_to.frame(index) return True except: return Falsedef switch_to_frame_by_name_or_id(driver, name_or_id): \"\"\"通过名称或ID切换到iframe\"\"\" try: driver.switch_to.frame(name_or_id) return True except: return Falsedef switch_to_frame_by_element(driver, element): \"\"\"通过元素切换到iframe\"\"\" try: driver.switch_to.frame(element) return True except: return Falsedef switch_to_parent_frame(driver): \"\"\"切换到父iframe\"\"\" try: driver.switch_to.parent_frame() return True except: return Falsedef switch_to_default_content(driver): \"\"\"切换到主文档\"\"\" try: driver.switch_to.default_content() return True except: 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 result finally: driver.switch_to.default_content()
4. 文件上传与下载
文件上传
def upload_file(driver, file_input_locator, file_path): \"\"\" 上传文件(适用于元素) \"\"\" try: file_input = driver.find_element(*file_input_locator) file_input.send_keys(file_path) return True except Exception as e: print(f\"文件上传失败: {e}\") return Falsedef upload_file_without_input(driver, upload_button_locator, file_path): \"\"\" 上传文件(适用于没有可见的情况) 使用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 True except Exception as e: print(f\"文件上传失败: {e}\") return False
文件下载
import osimport timefrom 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_file except Exception as e: print(f\"文件下载失败: {e}\") return None
5. 截图与日志
import osimport timefrom 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_path except 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_path except 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,第二个参数是driver driver = args[1] if len(args) > 1 else None if driver: timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\") filename = f\"failure_{func.__name__}_{timestamp}.png\" take_screenshot(driver, filename=filename) raise e return wrapper
6. 高级断言与验证
def verify_element_text(driver, locator, expected_text, contains=False): \"\"\" 验证元素文本 \"\"\" try: element = driver.find_element(*locator) actual_text = element.text if contains: assert expected_text in actual_text, f\"期望文本包含\'{expected_text}\',实际文本为\'{actual_text}\'\" else: assert actual_text == expected_text, f\"期望文本为\'{expected_text}\',实际文本为\'{actual_text}\'\" return True except 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 True except 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 True except 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 True except 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 True except 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 True except 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_time return load_time
使用Performance API获取详细性能数据
def get_performance_metrics(driver): \"\"\" 获取浏览器性能指标 \"\"\" # 使用Navigation Timing API navigation_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\']}\")