Python Selenium从入门到精通:全面指南

Python Selenium 完全指南:从入门到精通

📚 目录

  1. 环境准备与基础入门
  2. 元素定位与交互操作
  3. 等待机制与异常处理
  4. 面向对象封装与框架设计
  5. 进阶技巧与最佳实践
  6. 性能优化与调试技巧
  7. 实战案例分析

环境准备与基础入门

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 Options
    
    chrome_options = Options()
    chrome_options.add_argument("--headless")
    driver = webdriver.Chrome(options=chrome_options)
    
    使用代理
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    
    chrome_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 Options
    
    chrome_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 By
    
    driver = 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 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. 元素查找最佳实践

    1. 性能优化顺序:ID > Name > CSS > XPath
    2. 避免使用
    3. 绝对XPath路径(容易失效)
    4. 基于视觉位置的选择器
    5. 多级嵌套CSS选择器
    6. 推荐使用
    7. 有意义的ID和名称属性
    8. 数据测试属性(如data-testid)
    9. 短而明确的CSS选择器
    10. 建议添加
    11. 页面加载和元素的等待机制
    12. 查找元素的超时和重试机制
    13. 详细的错误处理机制

    等待机制与异常处理

    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 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 functools
    from time import sleep
    
    def 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. 等待策略最佳实践

    1. 避免使用time.sleep():不灵活且低效
    2. 优先使用显式等待:更精确,可控性更强
    3. 结合使用隐式等待和显式等待:隐式等待作为全局保护,显式等待针对特定场景
    4. 设置合理的超时时间:不要过长或过短
    5. 捕获并处理超时异常:提供适当的恢复机制或用户友好的错误信息
    6. 为不同网络环境调整等待策略:可配置的超时参数

    面向对象封装与框架设计

    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 False
    
    
    class 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 unittest
    from selenium import webdriver
    from selenium.webdriver.chrome.service import Service
    from webdriver_manager.chrome import ChromeDriverManager
    
    class 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 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.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:
        """
        页面对象工厂类
        """
        @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, abstractmethod
    
    class WaitStrategy(ABC):
        """
        等待策略基类
        """
        @abstractmethod
        def wait_for(self, driver, locator):
            pass
    
    class 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 json
    import os
    
    class 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 logging
    import os
    from datetime import datetime
    
    class 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 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_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 False
    
    def 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 False
    
    def 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 False
    
    def 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 False
    
    def switch_to_frame_by_element(driver, element):
        """通过元素切换到iframe"""
        try:
            driver.switch_to.frame(element)
            return True
        except:
            return False
    
    def switch_to_parent_frame(driver):
        """切换到父iframe"""
        try:
            driver.switch_to.parent_frame()
            return True
        except:
            return False
    
    def switch_to_default_content(driver):
        """切换到主文档"""
        try:
            driver.switch_to.default_content()
            return True
        except:
            return False
    
    def 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):
        """
        上传文件(适用于<input type="file">元素)
        """
        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 False
    
    def 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 True
        except Exception as e:
            print(f"文件上传失败: {e}")
            return False
    
    文件下载
    import os
    import time
    from pathlib import Path
    
    def 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 options
    
    def 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 os
    import time
    from datetime import datetime
    
    def 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 None
    
    def 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 None
    
    def 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 False
    
    def 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 False
    
    def 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 False
    
    def 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 False
    
    def 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 False
    
    def 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_timing
    
    def 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 logs
    
    def print_browser_logs(driver):
        """
        打印浏览器控制台日志
        """
        logs = driver.get_log('browser')
        for log in logs:
            print(f"[{log['level']}] {log['message']}")
    

    作者:猿榜编程

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python Selenium从入门到精通:全面指南

    发表回复