Python Selenium自动化测试与爬虫实战:从环境搭建到框架设计
1. 从手动到自动:为什么我们需要模拟网页点击
作为一名在测试和爬虫领域摸爬滚打多年的开发者,我几乎每天都要和浏览器打交道。早期,我的工作流程是这样的:打开浏览器,输入网址,等待页面加载,找到那个该死的按钮,点击,再等待,再点击……日复一日,重复、枯燥,还容易因为手滑点错。直到我遇到了Selenium,它彻底改变了我的工作方式。简单来说,Selenium就是一个能让你用代码控制浏览器的工具,而“模拟网页点击”则是它最基础、最核心,也最实用的功能之一。
你可能已经听说过Python和Selenium,但或许还在犹豫:这玩意儿到底能干嘛?我直接告诉你,它的应用场景远超你的想象。对于测试工程师,它是自动化测试的基石,可以模拟用户操作,验证功能是否正常,尤其是在回归测试中,能节省海量时间。对于数据分析师或爬虫开发者,当目标网站的数据需要通过点击按钮、翻页、展开下拉菜单才能获取时,Selenium就是那把“万能钥匙”。甚至对于运营或市场人员,一些日常的、重复的网页操作(如批量上传、数据录入)也能用它来解放双手。
很多人一听到“自动化”、“编程”就头大,觉得门槛很高。但我想说,用Python操作Selenium模拟点击,其核心逻辑和你手动操作几乎一模一样:找到那个元素,然后点击它。难点不在于“点击”这个动作,而在于如何稳定、准确地“找到”那个元素,以及如何处理点击前后网页的各种“反应”。这篇文章,我就从一个老司机的角度,带你绕过我当年踩过的所有坑,手把手让你掌握这套“点石成金”的本事。我们不会空谈理论,所有内容都围绕一个目标:写出一段能真正跑起来、稳定工作的代码。
2. 环境搭建:选对武器,避开版本“地狱”
工欲善其事,必先利其器。Selenium环境的搭建是第一步,也是劝退很多新手的“第一道坎”。问题往往不是出在Selenium本身,而是出在它依赖的浏览器和驱动上。下面,我会给出一个当前(基于2024年的常见环境)最稳定、兼容性最好的方案。
2.1 Python与Selenium库安装
首先,确保你安装了Python。我个人推荐使用Python 3.8到3.10之间的版本,这是目前生态兼容性最好的区间。安装Python时,务必勾选“Add Python to PATH”,这是很多新手忽略导致后续命令找不到的关键一步。
安装Selenium库非常简单,打开你的命令行(Windows的CMD或PowerShell,Mac/Linux的Terminal),输入以下命令:
pip install selenium通常情况下,这会安装最新稳定版的Selenium(如4.x版本)。但请注意,有时最新版可能会引入一些不兼容的改动。如果你追求极致的稳定,特别是项目需要长期运行,可以指定一个稍旧的版本,例如:
pip install selenium==4.10.0这里有一个非常重要的经验:永远记录下你项目所使用的Python和Selenium的具体版本号。最好是在项目根目录创建一个requirements.txt文件,里面写上selenium==4.10.0。这样,当你换一台机器或将来重新搭建环境时,可以一键复原,避免版本冲突带来的莫名错误。
2.2 浏览器与WebDriver:版本锁死的艺术
这是整个环境搭建中最核心、也最容易出错的部分。Selenium自己并不包含浏览器,它需要通过一个叫做“WebDriver”的中间件来与真实的浏览器(如Chrome, Firefox)通信。你必须保证三者的版本匹配:浏览器版本、对应的WebDriver版本、以及Selenium库的版本。
核心原则:先确定浏览器版本,再下载对应版本的WebDriver。
以最常用的Chrome为例:
- 查看Chrome版本:打开Chrome浏览器,点击右上角三个点 -> 帮助 -> 关于Google Chrome。记下完整的版本号,例如
114.0.5735.199。 - 下载对应ChromeDriver:访问ChromeDriver的官方下载站。这里的关键是,对于较新的Chrome版本(大约115之后),官方推荐使用Chrome for Testing渠道的驱动。更通用的方法是,在项目中通过
webdriver-manager这个库来自动管理驱动,这是目前最优雅的解决方案。 - 安装webdriver-manager:
pip install webdriver-manager - 在代码中使用自动管理:
这段代码会自动处理版本匹配问题,极大降低了环境配置的复杂度。from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager # 自动下载、缓存并匹配当前Chrome版本的驱动 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service)webdriver-manager同样支持Firefox、Edge等浏览器。
为什么我强烈推荐webdriver-manager?早期,我们需要手动下载chromedriver.exe,然后放到系统路径或者项目目录下。这带来几个问题:一是每次浏览器自动升级后,驱动就失效了,需要重新下载;二是在团队协作或部署到服务器时,每台机器的环境都要手动配置,非常麻烦。webdriver-manager解决了这一切,它让WebDriver像Python库一样被自动管理。
注意:在某些严格的内网环境或无法连接外网的服务器上,
webdriver-manager可能无法在线下载驱动。此时,你需要回归手动模式:下载对应版本的驱动文件,在代码中指定其绝对路径。service = Service(executable_path=r‘C:\path\to\your\chromedriver.exe‘) # Windows示例 # 或 service = Service(executable_path=‘/usr/local/bin/chromedriver‘) # Mac/Linux示例
2.3 验证环境与第一个脚本
环境装好后,我们写一个最简单的脚本来验证一切是否正常。这个脚本将打开百度首页,在搜索框输入关键词,并点击“百度一下”按钮。
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager import time # 1. 创建驱动服务,自动管理ChromeDriver service = Service(ChromeDriverManager().install()) # 2. 初始化浏览器驱动,这里可以添加很多选项 driver = webdriver.Chrome(service=service) try: # 3. 打开目标网页 driver.get(‘https://www.baidu.com‘) # 等待页面加载,这是一个简单的强制等待,实际项目中会用更智能的方式 time.sleep(2) # 4. 定位搜索框元素 # 通过元素的‘id‘属性定位,这是最快最准的方式 search_box = driver.find_element(By.ID, ‘kw‘) # 5. 在搜索框中输入文字 search_box.send_keys(‘Selenium自动化测试‘) # 6. 定位“百度一下”按钮并点击 # 通过元素的‘id‘属性定位 search_button = driver.find_element(By.ID, ‘su‘) search_button.click() # 7. 等待搜索结果加载 time.sleep(3) # 8. 可以打印当前页面的标题或URL作为验证 print(‘当前页面标题:‘, driver.title) print(‘当前页面URL:‘, driver.current_url) finally: # 9. 等待一会儿,方便肉眼观察结果,然后关闭浏览器 time.sleep(5) driver.quit()运行这段代码,你应该能看到Chrome浏览器自动打开,访问百度,输入文字并完成搜索,然后关闭。如果成功,恭喜你,你的Selenium环境已经就绪。如果失败,请根据错误信息(通常是关于驱动或浏览器的)回溯检查上述步骤。
3. 元素定位:精准“找到”目标的八种武器
模拟点击的第一步,也是最重要的一步,就是告诉Selenium:“你要点击页面上哪个东西?”。这个过程叫做“元素定位”。Selenium提供了多达8种定位策略,但并非每种都同样好用。掌握它们的优先级和适用场景,是写出稳定脚本的关键。
3.1 定位策略的优先级与实战选择
我把定位策略按优先级排序如下:
ID:元素的唯一身份证。如果元素有
id属性,且这个id不是动态生成的,无条件优先使用它。它查找速度最快,最精确。element = driver.find_element(By.ID, ‘login-button‘)Name:元素的名称。常用于表单元素(input, select等)。如果
name是唯一且稳定的,也是很好的选择。element = driver.find_element(By.NAME, ‘username‘)CSS Selector:CSS选择器。这是功能最强大、最灵活的定位方式,几乎可以定位任何元素。当ID和Name不可用时,它是首选。语法和前端CSS选择器一致。
# 通过class定位 element = driver.find_element(By.CSS_SELECTOR, ‘.submit-btn‘) # 通过属性定位 element = driver.find_element(By.CSS_SELECTOR, ‘input[type=“submit”]‘) # 通过层级关系定位 element = driver.find_element(By.CSS_SELECTOR, ‘div#container > ul.list > li:first-child‘)XPath:XML路径语言。功能同样强大,可以在整个DOM树中进行复杂查询。但通常比CSS Selector慢,且语法更复杂。在CSS无法解决的极端情况下(如需要根据文本内容定位)使用。
# 绝对路径(脆弱,不推荐) element = driver.find_element(By.XPATH, ‘/html/body/div[1]/form/input[3]‘) # 相对路径结合属性(推荐) element = driver.find_element(By.XPATH, ‘//input[@id=“kw”]‘) # 根据文本内容定位(CSS做不到) element = driver.find_element(By.XPATH, ‘//button[text()=“登录”]‘)Link Text / Partial Link Text:专门用于定位超链接(
<a>标签)。通过链接的完整或部分文本定位。# 完整文本 element = driver.find_element(By.LINK_TEXT, ‘忘记密码?‘) # 部分文本 element = driver.find_element(By.PARTIAL_LINK_TEXT, ‘密码‘)Class Name:通过CSS类名定位。谨慎使用,因为一个类名通常被多个元素共享,容易定位到多个元素而非一个。
# 可能返回多个元素,通常用find_elements elements = driver.find_elements(By.CLASS_NAME, ‘btn‘)Tag Name:通过标签名定位(如
div,input)。极少单独使用,因为一个页面有太多相同标签。通常与其他定位方式结合,或在查找多个同类元素时使用。
实战经验:如何选择?
- 黄金法则:优先使用ID>Name>CSS Selector。
- 为什么CSS Selector优于XPath?在大多数现代浏览器中,CSS Selector的解析和查找速度比XPath更快,且语法更简洁,更易于阅读和维护。XPath的遍历机制在复杂DOM中可能更慢。
- 如何获取这些定位信息?使用浏览器的开发者工具(F12)。在Elements面板,右键点击目标元素,选择“Copy” -> “Copy selector” 或 “Copy XPath”。但请注意,浏览器自动生成的CSS或XPath路径往往非常长且脆弱(依赖于复杂的层级结构),不建议直接使用。你应该从中分析出核心的、稳定的属性(如ID、独特的class组合)来构造更简洁的定位器。
3.2 处理动态元素与等待机制
你一定会遇到这种情况:代码执行find_element时,浏览器却报错说“找不到元素”。十有八九,是因为页面还没加载完,元素还不存在。这就是“等待”要解决的问题。Selenium中有三种等待方式:
强制等待:
time.sleep(秒数)。简单粗暴,但效率低下。因为你不知道网络或服务器到底需要多久,设短了会失败,设长了浪费时间。仅在调试或极少数确定场景下使用。隐式等待:
driver.implicitly_wait(秒数)。设置一个全局的等待时间。在查找任何元素时,如果没立刻找到,WebDriver会轮询DOM直到超时。它只对find_element系列方法有效。缺点是它会影响整个Driver生命周期,并且不适用于等待元素的特定状态(如可点击、可见)。driver.implicitly_wait(10) # 后续查找元素最多等10秒显式等待:这是生产环境中最推荐、最健壮的方式。它允许你为某个特定的元素设定等待条件,条件满足则继续,超时则报错。这需要用到
WebDriverWait和expected_conditions(EC)模块。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待最多10秒,直到ID为‘dynamic-button‘的元素可被点击 wait = WebDriverWait(driver, 10) element = wait.until(EC.element_to_be_clickable((By.ID, ‘dynamic-button‘))) element.click()
为什么显式等待最好?因为它更“智能”。它等待的是“条件”,而不仅仅是“元素存在”。常见的条件有:
presence_of_element_located: 元素出现在DOM中(不一定可见)。visibility_of_element_located: 元素可见(有宽高,非隐藏)。element_to_be_clickable: 元素可见且可点击。text_to_be_present_in_element: 元素中包含特定文本。
对于动态加载的内容(如AJAX请求返回后出现的列表、点击按钮后弹出的模态框),显式等待是唯一可靠的解决方案。我的经验是:几乎在所有需要等待的地方,都使用显式等待。将隐式等待作为一个很短的全局后备(例如3秒),而将具体的、重要的等待逻辑用显式等待清晰地写出来。
4. 点击操作进阶:不仅仅是.click()
你以为element.click()就是点击的全部?那就太小看现代网页的复杂性了。真实的点击操作会遇到各种“妖魔鬼怪”。
4.1 基础点击与JavaScript点击
大多数情况下,element.click()就能工作。但有些元素,特别是那些由JavaScript动态生成、或者被其他元素(如透明的遮罩层)覆盖的元素,标准的click()方法可能会失效(报错ElementClickInterceptedException或ElementNotInterceptableException)。
这时,我们可以尝试用JavaScript直接执行点击:
driver.execute_script(“arguments[0].click();“, element)execute_script方法允许我们在浏览器上下文中直接运行JavaScript。arguments[0]对应传入的第一个参数element。这种方式绕过了Selenium的模拟点击,直接触发元素的点击事件,通常能解决大部分点击失效的问题。
但是,请谨慎使用JS点击!因为它完全模拟了JavaScript的click()事件,而忽略了元素可能存在的“不可点击”状态(如disabled属性)。这可能导致业务逻辑错误。因此,我的建议是:优先使用Selenium原生的click(),仅在它确认失效时,再考虑使用JS点击,并做好相应的状态判断。
4.2 处理复杂交互:悬停、双击与右键
有些交互不仅仅是单击。例如,下拉菜单需要先将鼠标悬停(hover)在父菜单上,子菜单才会出现。
Selenium的ActionChains类就是用来处理这类复杂键盘鼠标操作的。
from selenium.webdriver.common.action_chains import ActionChains menu = driver.find_element(By.ID, ‘dropdown-menu‘) submenu = driver.find_element(By.ID, ‘sub-item‘) # 创建动作链 actions = ActionChains(driver) # 将鼠标移动到menu元素上,然后暂停,再点击出现的submenu actions.move_to_element(menu).pause(1).click(submenu).perform() # perform()是执行动作链中所有动作的命令动作链可以组合多个操作:move_to_element(悬停)、click_and_hold(点击并按住)、double_click(双击)、context_click(右键点击)、drag_and_drop(拖放)等。perform()方法必须被调用,动作才会真正执行。
4.3 文件上传与文件下载
文件上传:对于<input type=“file”>元素,不要尝试去点击它然后操作系统的文件选择框(这非常复杂且不稳定)。最直接的方法是使用send_keys()直接传入文件路径。
upload_element = driver.find_element(By.XPATH, ‘//input[@type=“file”]‘) upload_element.send_keys(‘/Users/yourname/Desktop/test_image.jpg‘)这样,文件路径就被直接设置到了输入框,相当于完成了上传操作。
文件下载:控制浏览器下载文件需要预先设置好下载选项。以下是一个Chrome浏览器的示例,设置下载路径并禁止弹窗:
from selenium import webdriver from selenium.webdriver.chrome.options import Options chrome_options = Options() prefs = { ‘download.default_directory‘: ‘/path/to/your/download/folder‘, # 设置下载路径 ‘download.prompt_for_download‘: False, # 禁止下载弹窗 ‘download.directory_upgrade‘: True, ‘safebrowsing.enabled‘: True } chrome_options.add_experimental_option(‘prefs‘, prefs) driver = webdriver.Chrome(options=chrome_options)设置好后,点击下载链接,文件就会自动保存到指定目录,无需人工干预。
5. 实战避坑指南:让脚本稳定运行的七个关键
写一个能跑通的脚本不难,写一个能在各种环境下稳定运行、长期有效的脚本才是真本事。下面这些坑,都是我亲身踩过,用加班时间换来的经验。
5.1 等待的艺术:告别sleep,拥抱智能等待
前面提到了显式等待,但实际应用中还有更深的技巧。一个常见的反模式是:为了等待一个元素,在代码里到处写满了time.sleep(10)。这不仅效率低,而且当网络慢时可能不够,网络快时又浪费生命。
正确做法是组合使用等待策略:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException def wait_for_element(driver, locator, timeout=10): “”“等待元素出现并可见,返回元素,超时返回None”“” try: element = WebDriverWait(driver, timeout).until( EC.visibility_of_element_located(locator) ) return element except TimeoutException: print(f“元素 {locator} 在 {timeout} 秒内未找到或不可见。“) # 这里可以记录日志、截图,方便后续排查 driver.save_screenshot(‘timeout_error.png‘) return None # 使用示例 submit_btn_locator = (By.ID, ‘submit‘) submit_button = wait_for_element(driver, submit_btn_locator) if submit_button: submit_button.click() else: # 处理元素未找到的情况,例如重试或终止脚本 print(“关键提交按钮未找到,流程终止。“) driver.quit()将等待逻辑封装成函数,可以复用代码,并使主流程更清晰。同时,在等待失败时进行截图,这是线上问题排查的救命稻草。
5.2 元素状态检测:避免无效点击
在点击前,检查元素状态是一个好习惯。例如,按钮可能是disabled状态,或者被一个加载动画覆盖。
# 检查元素是否可点击(可见且 enabled) if element.is_enabled() and element.is_displayed(): element.click() else: print(“元素不可点击,当前状态:“) print(f“是否启用:{element.is_enabled()}“) print(f“是否显示:{element.is_displayed()}“) # 可以等待一段时间再重试,或执行其他逻辑5.3 处理弹窗与多窗口
点击一个链接,可能会打开新标签页或弹出浏览器弹窗。你需要切换Driver的焦点。
# 获取当前所有窗口的句柄 main_window = driver.current_window_handle all_windows = driver.window_handles # 这是一个列表 # 点击某个会打开新窗口的链接 driver.find_element(By.LINK_TEXT, ‘在新窗口打开‘).click() # 等待新窗口出现 WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2)) # 切换到新窗口 for window_handle in driver.window_handles: if window_handle != main_window: driver.switch_to.window(window_handle) break # 在新窗口操作... print(“新窗口标题:“, driver.title) # 操作完毕后,可以切回主窗口 driver.switch_to.window(main_window)对于JavaScript的alert,confirm,prompt弹窗,使用driver.switch_to.alert来处理:
# 点击触发alert的按钮 driver.find_element(By.ID, ‘trigger-alert‘).click() # 切换到alert alert = driver.switch_to.alert print(“弹窗文本:“, alert.text) # 点击确认 alert.accept() # 或者点击取消 # alert.dismiss() # 对于prompt,还可以输入文字 # alert.send_keys(‘输入的文字‘)5.4 应对页面刷新与导航
点击后页面可能会刷新或跳转。此时,之前获取到的元素引用(WebElement对象)会失效(StaleElementReferenceException)。你必须重新定位元素。
try: old_element = driver.find_element(By.ID, ‘some-button‘) old_element.click() # 点击后页面刷新 # 刷新后,下面的操作会报 StaleElementReferenceException # old_element.click() except StaleElementReferenceException: # 重新定位元素 new_element = driver.find_element(By.ID, ‘some-button‘) new_element.click()更稳健的做法是,在每次需要操作元素前,都重新进行定位,尤其是在已知页面会发生变化的操作之后。
5.5 浏览器选项配置:让自动化更“像人”
默认情况下,Selenium启动的浏览器会有一些特征(如状态栏显示“正由自动测试软件控制”),一些网站会检测并屏蔽这类自动化浏览器。我们可以通过配置选项来让浏览器更“隐形”,并优化自动化体验。
from selenium import webdriver from selenium.webdriver.chrome.options import Options chrome_options = Options() # 1. 无头模式:不显示浏览器GUI,在服务器上运行必备 # chrome_options.add_argument(‘--headless‘) # 注释掉则显示浏览器 # 2. 禁用自动化控制提示 chrome_options.add_experimental_option(“excludeSwitches“, [“enable-automation“]) chrome_options.add_experimental_option(‘useAutomationExtension‘, False) # 3. 规避检测(高级) chrome_options.add_argument(‘--disable-blink-features=AutomationControlled‘) # 4. 其他常用优化参数 chrome_options.add_argument(‘--no-sandbox‘) # Linux服务器上可能需要 chrome_options.add_argument(‘--disable-dev-shm-usage‘) # 解决共享内存问题 chrome_options.add_argument(‘--disable-gpu‘) # 某些虚拟环境需要 chrome_options.add_argument(‘--window-size=1920,1080‘) # 设置初始窗口大小 # 5. 用户数据目录(可选):复用已有浏览器缓存、登录状态等 # chrome_options.add_argument(r‘--user-data-dir=C:\Users\YourName\AppData\Local\Google\Chrome\User Data‘) # chrome_options.add_argument(‘--profile-directory=Default‘) driver = webdriver.Chrome(options=chrome_options)关于无头模式:在本地调试时,建议关闭无头模式,这样你能直观看到浏览器的操作过程,便于调试。在服务器部署执行定时任务时,再开启无头模式以节省资源。
5.6 日志记录与错误处理
一个健壮的脚本必须有完善的日志和错误处理机制。不要让你的脚本在深夜失败后悄无声息。
import logging from selenium.common.exceptions import NoSuchElementException, TimeoutException # 配置日志 logging.basicConfig(level=logging.INFO, format=‘%(asctime)s - %(name)s - %(levelname)s - %(message)s‘, handlers=[ logging.FileHandler(‘selenium_automation.log‘), logging.StreamHandler() ]) logger = logging.getLogger(__name__) def safe_click(driver, locator, description=“元素“): “”“安全的点击操作,包含重试和日志记录”“” max_retries = 2 for attempt in range(max_retries): try: element = WebDriverWait(driver, 10).until( EC.element_to_be_clickable(locator) ) element.click() logger.info(f“成功点击 {description}: {locator}“) return True except (TimeoutException, NoSuchElementException) as e: logger.warning(f“第{attempt+1}次尝试点击 {description} 失败: {e}“) if attempt == max_retries - 1: logger.error(f“点击 {description} 最终失败,正在截图。“) driver.save_screenshot(f‘click_failure_{description.replace(“ “, “_“)}.png‘) return False time.sleep(2) # 等待2秒后重试 return False # 使用示例 if not safe_click(driver, (By.ID, ‘next-page‘), “下一页按钮“): logger.error(“流程因无法点击下一页而中断。“) driver.quit() exit(1)这个safe_click函数增加了重试机制和详细的日志记录,并在最终失败时截图。在实际项目中,你应该将这类通用操作封装成工具函数。
5.7 资源管理与优雅退出
务必确保在脚本结束时(无论成功还是异常),关闭浏览器驱动,释放资源。否则会导致后台残留Chrome进程。
try: # 你的主要业务逻辑 do_some_automation(driver) except Exception as e: logger.error(f“脚本执行发生异常: {e}“, exc_info=True) # 异常时截图 driver.save_screenshot(‘unexpected_error.png‘) finally: # 无论是否异常,最终都确保退出驱动 driver.quit() logger.info(“浏览器驱动已退出,资源释放完毕。“)使用try...except...finally结构是保证资源清理的最佳实践。
6. 从脚本到框架:构建可维护的自动化项目
当你掌握了单个脚本的编写后,很快就会面临管理多个脚本、复用代码、处理数据等问题。这时,就需要考虑项目结构了。虽然标题是“模拟点击”,但任何有价值的自动化任务都不会只是一个孤立的点击动作。这里介绍两种广泛采用的模式。
6.1 Page Object Model:让代码远离“选择器地狱”
Page Object Model是一种设计模式,其核心思想是将页面抽象成一个类,将页面上的元素定位器和操作这些元素的方法封装在这个类里。测试脚本(或业务脚本)则通过调用页面对象的方法来操作页面,而无需关心元素具体是如何定位的。
没有POM的代码(难以维护):
# 脚本中充斥着各种find_element和定位字符串 driver.find_element(By.ID, ‘username‘).send_keys(‘admin‘) driver.find_element(By.ID, ‘password‘).send_keys(‘123456‘) driver.find_element(By.CSS_SELECTOR, ‘button.login-btn‘).click()使用POM后的代码:
# pages/login_page.py class LoginPage: def __init__(self, driver): self.driver = driver self.username_input = (By.ID, ‘username‘) self.password_input = (By.ID, ‘password‘) self.login_button = (By.CSS_SELECTOR, ‘button.login-btn‘) def enter_username(self, username): self.driver.find_element(*self.username_input).send_keys(username) def enter_password(self, password): self.driver.find_element(*self.password_input).send_keys(password) def click_login(self): self.driver.find_element(*self.login_button).click() def login(self, username, password): self.enter_username(username) self.enter_password(password) self.click_login() # 主脚本 main.py from pages.login_page import LoginPage login_page = LoginPage(driver) login_page.login(‘admin‘, ‘123456‘)POM带来的好处:
- 高可维护性:当页面元素的定位器(如ID、CSS选择器)发生变化时,你只需要修改对应的Page Object类中的一处代码,所有使用该页面的脚本都自动生效。
- 高可读性:业务脚本读起来就像自然语言,
login_page.login(...),清晰表达了“在登录页执行登录操作”。 - 低冗余:公共的页面操作被封装成方法,可以在多个脚本中复用。
6.2 与pytest结合:组织用例与生成报告
pytest是Python最主流的测试框架之一,它同样非常适合用来组织和管理自动化脚本,即使你的目的不是测试,而是数据抓取或日常自动化。
项目结构示例:
my_automation_project/ ├── conftest.py # pytest配置文件,定义全局fixture(如driver) ├── pages/ # 存放所有Page Object类 │ ├── __init__.py │ ├── login_page.py │ └── home_page.py ├── tests/ # 存放测试用例/自动化任务 │ ├── __init__.py │ ├── test_login.py # 一个用例文件 │ └── test_data_crawl.py ├── utils/ # 工具函数 │ ├── __init__.py │ └── helper.py └── requirements.txt # 项目依赖conftest.py定义共享的driver:
import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager @pytest.fixture(scope=“session“) # 整个测试会话只启动一次浏览器 def driver(): service = Service(ChromeDriverManager().install()) chrome_options = webdriver.ChromeOptions() # chrome_options.add_argument(‘--headless‘) _driver = webdriver.Chrome(service=service, options=chrome_options) _driver.implicitly_wait(5) yield _driver # 将driver提供给测试用例 _driver.quit() # 所有用例执行完毕后退出 @pytest.fixture def login_page(driver): # 依赖上面的driver fixture from pages.login_page import LoginPage return LoginPage(driver)tests/test_login.py编写用例:
def test_admin_login(login_page): “”“测试管理员登录”“” login_page.driver.get(‘https://example.com/login‘) login_page.login(‘admin‘, ‘admin123‘) # 添加断言,验证登录成功 assert “Dashboard“ in login_page.driver.title # 或者验证某个成功登录后才出现的元素 # assert login_page.is_logged_in() == True def test_data_crawl(driver): “”“一个数据抓取任务,同样可以用pytest组织”“” driver.get(‘https://example.com/data-list‘) # ... 执行抓取逻辑 data = extract_data(driver) assert len(data) > 0 # 断言抓取到了数据 # 可以将数据保存到文件或数据库 save_to_csv(data, ‘output.csv‘)运行与报告:在项目根目录下执行pytest tests/ -v即可运行所有用例。你还可以生成漂亮的HTML报告,这得益于pytest丰富的插件生态,例如pytest-html:
pytest tests/ -v --html=report.html --self-contained-html这会在当前目录生成一个report.html文件,里面清晰记录了每个用例的执行结果、通过与否、甚至失败时的错误信息和截图(需要额外配置)。这对于监控自动化任务的执行情况非常有帮助。
将Selenium脚本用POM和pytest组织起来,虽然前期需要一些设计工作,但对于任何需要长期维护、扩展或团队协作的项目来说,这笔投资都是绝对值得的。它让你的代码从“一次性脚本”升级为“可维护的自动化工程”。