尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Selenium Web自动化实战:从环境搭建到工程化封装

Selenium Web自动化实战:从环境搭建到工程化封装 接触过Web UI自动化的同学应该都绕不开Selenium浏览器自动化测试框架这个老朋友。不管你是做测试开发、爬虫采集还是想把自己重复的网页操作变成脚本这套框架几乎都是入门的第一选择。这篇文章我带你从头过一遍Selenium的完整知识链路环境搭建、核心API、工程化封装、经典坑位以及Selenium 4带来的新变化。文中所有脚本都是可以直接抄作业的级别每段代码我会说明为什么这么写踩过哪些坑适合什么人参考。1. Selenium到底是什么能帮我解决什么问题1.1 核心需求解析一个自动化库不是一个插件很多人第一次搜索“Selenium”时会误以为它是一个浏览器插件装上就能用。实际上它是一个标准化的Web自动化测试库通过驱动WebDriver与浏览器通信模拟真实用户的操作行为点击、输入、滚动、切换页面、提取数据等等。它能解决的问题围绕“用代码代替人手去操作浏览器”我实战中主要遇到三类场景自动化回归测试产品每迭代一次几百条用例如果全部手工点一遍一个下午就没了。用Selenium在夜间跑完回归早上看报告是测试团队最常见的用法。爬虫与数据采集遇到需要登录、需要点击下一页、渲染完全由JavaScript动态生成内容的网站用requests库很难处理Selenium可以直接拿到渲染后的完整DOM。重复性网页操作比如批量导出后台报表、批量录入表单、定时抢票/秒杀辅助脚本这类日常杂活用Selenium自动化后能省下大量时间。Selenium适合所有需要“驱动浏览器”的场景但它有自己的定位它是一个模拟用户行为的工具不是性能压测工具也不是接口测试工具。如果你要测的是后端接口吞吐量或者想在高并发下压测服务端选JMeter或Locust如果你要验证的是页面上某个按钮点击后是否正确跳转、表单提交后是否弹正确提示Selenium就是最趁手的选择。1.2 为什么选Selenium而不是其他无头浏览器市面上做浏览器自动化的方案并不少Playwright、Cypress、Puppeteer、Pyppeteer我都接触过各有长处。但Selenium至今仍是很多团队的首选原因是它有三点别人很难替代的优势第一它是W3C标准化的WebDriver协议。浏览器厂商Chrome、Firefox、Edge、Safari直接配合这个标准来做驱动支持意味着你写的脚本可以跑在几乎所有主流浏览器上而且未来浏览器升级时Selenium跟进兼容的速度通常比较快。第二语言绑定生态广。Selenium官方支持Java、Python、C#、Ruby、JavaScript等主流语言。团队里原来写Java的测开同学可以直接上手不用为了自动化去专门学Python个人做小工具选Python开发效率也高。第三社区积累极厚。你在自动化中踩到的坑大概率别人早就踩过Stack Overflow和GitHub上随便搜都能找到答案。这一点对于团队内部推广落地非常关键新人遇到问题求助时资料明显比其他框架丰富得多。当然Selenium也有短板执行速度偏慢面对重前端SPA应用时偶尔要处理动态渲染带来的额外复杂度截图、网络拦截这类能力不如Playwright顺手。但如果你面对的是常规B端系统、电商平台、企业后台这类业务系统Selenium成熟稳定、资料丰富的特点已经足够覆盖绝大多数需求。2. 环境搭建装完Selenium库还不够驱动才是关键2.1 基础环境准备Python和pip一行命令我用Python做演示。先确认本机Python版本在3.8以上打开终端执行python --version pip --version两者的版本号都能正常打印后执行安装pip install selenium安装完成后在Python交互环境里验证一下import selenium print(selenium.__version__)能打印出版本号说明Selenium库本身已经装好。但到这里只完成了一半真正让浏览器“跑起来”还需要搞掂驱动。2.2 澄清一个热搜误区没有“Selenium插件”这种东西网上搜索“怎么安装Selenium插件”的朋友其实是把概念搞混了。Selenium库本身不是浏览器插件它不能像安装Chrome扩展那样一键装上。你的脚本想要控制Chrome浏览器必须提供一个翻译官把Selenium发出的WebDriver协议指令翻译成浏览器内部操作这个翻译官就是ChromeDriver。不同浏览器对应不同驱动浏览器驱动名称下载入口说明Chromechromedriver在Chrome浏览器里访问 chrome://version 查看版本号然后到驱动镜像源下载对应版本Edgemsedgedriver基于Chromium在 edge://version 查看版本下载对应驱动Firefoxgeckodriver下载后配置到PATH路径即可SafarisafaridrivermacOS系统自带无需额外下载很多入门者栽在此处——装好了Selenium库运行脚本却报错selenium.common.exceptions.WebDriverException: Message: chromedriver executable needs to be in PATH.这里就是驱动没配好。解决方式有三种按推荐程度排序第一种让Selenium Manager自动处理最推荐Selenium 4.6以上版本内置。Selenium 4.6开始内置了Selenium Manager你只需要写from selenium import webdriver driver webdriver.Chrome()它会自动检查本机Chrome版本自动下载匹配的Driver到缓存目录。实测下来非常省心推荐直接用它。第二种用第三方库webdriver-manager手动触发。如果你还在用Selenium 4.5以下版本或者需要更精细控制驱动版本可以装这个包pip install webdriver-manager使用方式from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)第三种完全手动下载。前往chromedriver官方镜像网站找到和你Chrome主版本号一致的chromedriver下载后解压把可执行文件放到系统PATH目录下Windows可以放Python安装目录的Scripts文件夹里或者直接放到项目根目录代码里指定路径from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(rD:\tools\chromedriver.exe) driver webdriver.Chrome(serviceservice)2.3 驱动版本匹配验证第一个最小脚本环境配置是否正确用一个最小脚本就能验证from selenium import webdriver driver webdriver.Chrome() driver.get(https://www.baidu.com) print(driver.title) driver.quit()这段代码做了四件事启动浏览器、打开百度首页、打印页面标题、关闭浏览器。如果能顺利打印出“百度一下你就知道”整个Selenium环境就通了。如果driver.quit()在脚本中途抛异常时没有执行浏览器窗口容易残留一堆僵尸进程这点在写完整框架时一定要留意。3. 核心API实操定位、操作、等待三件套必须吃透3.1 元素定位的8种方式优先级怎么排自动化操作浏览器的本质就是不断在页面上找到元素、操作元素。找不到元素后面一切免谈。Selenium提供了8种定位方式定位方式方法适用场景IDfind_element(By.ID, value)元素有唯一id时最高效Namefind_element(By.NAME, value)表单常用但name值可能重复Class Namefind_element(By.CLASS_NAME, value)适配样式类名注意多类名情况Tag Namefind_element(By.TAG_NAME, input)批量收集标签时使用Link Textfind_element(By.LINK_TEXT, 完整链接文字)定位a标签文本必须全等Partial Link Textfind_element(By.PARTIAL_LINK_TEXT, 部分链接文字)定位a标签允许模糊匹配XPathfind_element(By.XPATH, //*[idvalue])定位复杂结构灵活但也脆弱CSS Selectorfind_element(By.CSS_SELECTOR, #value)语法简洁性能好推荐掌握我个人的实战优先级是ID优先于NameName优先于Class兜底用CSS SelectorXPath只在CSS很难表达时才用。原因在于ID在页面中天然唯一定位稳定性最高CSS选择器性能好、表达式短XPath定位能力强但盲用丰富多样的函数容易写出依赖页面层级结构的绝对路径这种定位器一碰到前端结构变更就集体作废。写XPath时有一条铁律不要用 /html/body/div[1]/div[3]/div[2]/input 这种绝对路径要用相对路径结合属性# 推荐 //input[placeholder请输入用户名] //button[contains(text(),登录)] //div[classsearch-bar]//input高亮一个日常容易忽略的细节Selenium定位到多个元素时find_element只返回第一个find_elements返回列表。在写爬虫时凡是想“拿所有商品卡片”的都用find_elements。3.2 核心操作点击、输入、提交、下拉框与文件上传元素定位到之后最常用的是这些操作element driver.find_element(By.ID, username) element.send_keys(test_user) # 输入文本 element.clear() # 清空输入框 element.click() # 点击按钮 element.submit() # 在表单内提交 element.text # 获取可见文本 element.get_attribute(value) # 获取value属性下拉框的处理要用Select类不要直接click再click选项那样既慢又不稳定from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.ID, city)) select.select_by_visible_text(上海) # 按可见文本选择 select.select_by_value(shanghai) # 按value选择 select.select_by_index(2) # 按索引选择文件上传是一个高频但不方便自测的场景。如果页面上是直接用send_keys传本地文件路径driver.find_element(By.CSS_SELECTOR, input[typefile]).send_keys(rD:\test_files\data.xlsx)注意input标签的文件上传不能先click再操作会弹出系统文件选择框脚本就卡住了。直接send_keys是本问题的正确解法。对应地如果遇到不是input标签的文件上传组件多半是JavaScript实现的建议优先考虑autoit或pywinauto这类桌面端工具配合处理。3.3 等待机制90%偶发性失败都出在这里自动化脚本跑一半报“找不到元素”第一反应不要急着改定位器先考虑页面元素是不是还没加载出来。浏览器加载页面是异步过程元素渲染有先后脚本执行速度远快于页面渲染速度两种速度不匹配就会产生竞争条件。这时有三种等待策略强制等待time.sleep()不推荐。固定等3秒网络快时白白浪费2秒网络慢时3秒又不够墙上的钟表不是服务器的加载时间。隐式等待implicitly_wait()设置后每次find_element都会在元素未出现时轮询查找一段时间driver.implicitly_wait(10)它像一个全局超时机制适合起步预设。但它的坑在于只对元素存在性生效对元素可见、可点击这类状态不负责而且设置后对全局所有find_element都生效遇到某些需要精确等待的业务逻辑会比较被动。显式等待WebDriverWait expected_conditions最推荐它就是专门为“等到某个条件满足再继续”设计的from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) login_btn wait.until(EC.element_to_be_clickable((By.ID, loginBtn))) login_btn.click()常见的条件有presence_of_element_located元素出现在DOM中visibility_of_element_located元素可见element_to_be_clickable元素可见且可点击element_to_be_selected下拉选项被选中title_contains页面标题包含指定文本显式等待的思路是“等到条件满足就立刻进行下一步超时再报错”。用它能解决绝大多数页面渲染造成的偶发失败。我的习惯是全局设一个10秒的隐式等待兜底关键节点再加上显式等待精确控制。3.4 完整案例登录后搜索并断言结果把前面几个知识点串起来写一个最常见的完整场景脚本——登录电商后台、搜索商品、验证搜索结果from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver webdriver.Chrome() driver.implicitly_wait(10) try: # 1. 打开登录页 driver.get(https://example.com/login) # 2. 输入账号密码点击登录 driver.find_element(By.ID, username).send_keys(test_user) driver.find_element(By.ID, password).send_keys(test_pass) driver.find_element(By.ID, loginBtn).click() # 3. 等待登录跳转判断title变化 WebDriverWait(driver, 10).until(EC.title_contains(首页)) # 4. 搜索商品 driver.find_element(By.ID, searchInput).send_keys(无线鼠标) driver.find_element(By.ID, searchBtn).click() # 5. 校验搜索结果中是否包含关键词 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, product-list)) ) assert 无线鼠标 in driver.page_source print(测试通过) finally: driver.quit()finally里的driver.quit()非常重要它保证不管断言成败浏览器都能被关掉避免残留进程拖垮后面执行的用例。4. 工程化封装从能写脚本到能投入项目的框架4.1 不封装的脚本会带来什么灾难很多初学者写完上面那种脚本就以为自动化完成了。实际上项目里维护这种“平铺直叙”的脚本是一场灾难登录逻辑在10个用例里出现10次每个用例里有一份重复的driver初始化代码突然有一天前端把“用户名输入框”的id从username改成了userName你要去这10个用例里逐个修改脚本跑挂了截图、日志全没有只能靠肉眼看控制台报错重新跑。一个能经受住业务迭代考验的自动化测试工程至少要分层BasePage封装通用操作Page Object封装页面对象TestCase引用页面对象写业务用例。这套结构不是玄学而是经过多年验证的工程实践。4.2 BasePage基类把高频操作沉淀成方法BasePage是一层“工具层”把查找元素、点击、输入、等待这些高频操作封装成方法后续所有页面对象继承它即可from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver, timeout10): self.driver driver self.wait WebDriverWait(driver, timeout) def find_element(self, locator): return self.wait.until(EC.presence_of_element_located(locator)) def find_elements(self, locator): return self.wait.until(EC.presence_of_all_elements_located(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注意这里locator是一个元组比如(By.ID, username)正好对应expected_conditions的入参格式用起来很顺手。封装完成后用例层几乎不用再关心底层WebDriver API长什么样。4.3 Page Object模式让每个页面都有自己的“类”Page Object页面对象的核心思想是一个页面对应一个类页面上所有可操作元素和业务操作都在类中定义。这样业务用例读起来就像自然语言一样前端变化时也只改一个类文件。以登录页和首页为例class LoginPage(BasePage): username_loc (By.ID, username) password_loc (By.ID, password) login_btn_loc (By.ID, loginBtn) def login(self, username, password): self.input_text(self.username_loc, username) self.input_text(self.password_loc, password) self.click(self.login_btn_loc) class HomePage(BasePage): search_input_loc (By.ID, searchInput) search_btn_loc (By.ID, searchBtn) def search(self, keyword): self.input_text(self.search_input_loc, keyword) self.click(self.search_btn_loc)用例层就变得非常简洁def test_login_and_search(driver): login_page LoginPage(driver) login_page.login(test_user, test_pass) home_page HomePage(driver) home_page.search(无线鼠标)这种结构最直观的好处定位信息集中管理。一处修改全工程生效。项目想做数据驱动、关键字驱动时基于这套结构扩展也容易得多。4.4 搭配pytestfixture、截图和参数化Selenium本身不提供测试管理能力工程里一般搭配pytest来组织用例。用pytest的fixture统一管理浏览器的启动和关闭import pytest from selenium import webdriver pytest.fixture def driver(): driver webdriver.Chrome() driver.implicitly_wait(10) yield driver driver.quit()这样每个用例函数只要声明一下参数driverpytest就会自动创建一个浏览器实例并在用例结束后自动关闭。用例失败时自动截图是个很实用的调试手段写一个pytest钩子函数pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(driver) if driver: driver.save_screenshot(freport/{item.name}.png)数据驱动也能用pytest实现得很干净pytest.mark.parametrize(username,password,expected, [ (user1, pass1, 登录成功), (user2, wrong, 密码错误), ]) def test_login(driver, username, password, expected): ...配上pytest-html或allure插件可以生成非常好看的测试报告。这块组合拳是当前Python自动化项目的主流配置。4.5 测试数据与配置的分离框架做到一定规模你会发现数据放在代码里非常痛苦——改一个测试账号要改代码、重新提交、重新部署。合理的做法是把环境信息、账号数据、URL等从代码里抽离出来放到配置文件YAML/JSON或Excel中代码读取配置后动态执行。我用一个最简化的config.yaml示例base_url: https://example.com login_data: username: test_user password: test_pass timeout: 10 headless: false在代码里用PyYAML读取并挂到全局这样一套代码切测试环境、预发布环境只需改一个配置文件不需要动一行用例代码。环境切换、回归执行都会省下大量时间。5. 常见问题与排查技巧踩过的坑你就不用再踩了5.1 驱动与浏览器版本不匹配现象启动浏览器瞬间报错提示SessionNotCreatedException或者“This version of ChromeDriver only supports Chrome version XX”。原因Chrome浏览器悄悄自动升级了而本地chromedriver还是旧版本。解法查看浏览器版本号下载对应版本的驱动。如果你用的是Selenium Manager或webdriver-manager它们会帮你匹配手动管理驱动时建议养成本地存放驱动的习惯或用webdriver-manager统一管理避免每换一台电脑都要重新弄一遍。5.2 元素偶发定位不到时好时坏现象同一个脚本今天跑过明天就挂或者10次里挂2次重跑又能过。排查思路先检查是不是等待不够。把强制sleep换成显式等待。用try/except打印出当前的page_source看看元素是不是真的不在页面上。降低浏览器执行速度手工在页面上确认元素是否存在。如果手动看得到元素脚本找不到基本是时间问题如果手动也找不到可能是登录状态失效、前端改了布局属于另外的排查方向。加了显式等待仍然偶发失败我习惯在定位前先打印当前URL和窗口句柄确认脚本是否停留在预期页面上。很多“偶发”其实是因为某个前置操作跳转到了新标签页脚本还在旧页面里找元素自然找不到。5.3 始终找不到嵌入页的内容先检查iframe现象页面肉眼可见某个输入框但脚本find_element就是报NoSuchElementException。原因内容在iframe内嵌框架里。iframe就像页面里嵌的一个独立文档在切换到该iframe之前主页面的脚本无从感知它的内部元素。解法# 切换到iframe支持id、name、索引、WebElement driver.switch_to.frame(frame_id) # 操作完记得切回主文档否则后续元素全部找不到 driver.switch_to.default_content()iframe嵌套多层的场景需要一级一级往里切。每切一层就相当于进入一个独立世界离开时也要一级一级往回收。写这个逻辑时强烈建议封装成工具函数避免每处都写switch_to。5.4 点击后打开了新标签页脚本却还在旧页面现象点击“在新标签页中打开”类按钮后后续元素定位全部失败。原因Selenium默认操作的是当前活动标签页新开标签页后它不会自动切过去。解法手动通过window_handles切换句柄# 打开新页面后 handles driver.window_handles driver.switch_to.window(handles[-1]) # 切到最后一个标签页 # 用完关掉新页切回原来的 driver.close() driver.switch_to.window(handles[0])这里最保险的做法是记录打开标签页前后的句柄集合用“后退”的方式找回原来句柄而不是想当然地用[-1]。因为多个标签页的打开顺序不一定和索引顺序一致。5.5 headless模式跑批量脚本需要跑批量脚本或者挂在服务器上执行的时候无头模式headless是标配不弹出浏览器窗口资源占用更少from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions)注意headless模式下某些弹窗、hover效果的表现和真实浏览器略有差异如果脚本先在有头模式调试通过切到headless后出现怪问题可以先取消headless做对比确认到底是代码问题还是头less兼容问题。5.6 常见问题速查表现象可能原因解决方案chromedriver needs to be in PATH驱动未安装/未加入PATH使用Selenium Manager或手动配置驱动路径SessionNotCreatedException驱动与浏览器版本不匹配下载匹配版本的驱动NoSuchElementException元素未加载、在iframe中、定位器过期显式等待、切换iframe、更新定位器ElementClickInterceptedException元素被遮挡/悬浮层覆盖等待遮挡消失或强制用JavaScript点击StaleElementReferenceException页面刷新后旧元素引用失效重新定位元素后再操作点击无反应按钮disabled或事件未绑定显式等待element_to_be_clickableStaleElementReferenceException特别有欺骗性它报错看起来像元素找不到实际上是在说“你手里攥着的引用已经失效了”。页面一刷新哪怕同一个位置的元素变成了新的DOM节点旧引用也不能再用。解决办法就是重新find一遍元素再继续操作。6. Selenium 4新特性与过来人的一点建议6.1 相对定位器按位置关系找元素Selenium 4引入了一组相对定位器可以基于位置关系定位元素这个概念很适合“找到某个元素上方/下方/左侧/右侧的元素”这类需求from selenium.webdriver.common.by import By from selenium.webdriver.support.relative_locator import locate_with password_input driver.find_element( locate_with(By.TAG_NAME, input).below(By.ID, username) )这类定位器在复杂布局、没有明确属性可用的场景下很实用。不过相对定位的解析依赖浏览器支持遇到页面结构频繁变化时还是优先考虑数据属性。6.2 新APIfind_element取代旧用法Selenium 4已经将find_element_by_id这类旧API标记为过时统一推荐新版写法。老的教程代码中大量使用的find_element_by_class_name、find_element_by_xpath在新版本中会打印DeprecationWarning建议新项目直接统一用下面的格式from selenium.webdriver.common.by import By driver.find_element(By.CLASS_NAME, product) driver.find_element(By.XPATH, //div[classlist])批量迁移老代码时可以用正则替换把旧写法改掉但改完后务必逐个页面回归一遍防止转换过程引入错误。6.3 执行Chrome DevTools协议命令Selenium 4的新功能之一是直接通过execute_cdp_cmd执行Chrome DevTools协议CDP命令这下可以做很多老版本做不到的事比如模拟弱网、拦截图片资源加速加载、模拟地理位置# 模拟弱网环境 driver.execute_cdp_cmd(Network.enable, {}) driver.execute_cdp_cmd(Network.emulateNetworkConditions, { offline: False, latency: 200, downloadThroughput: 500 * 1024, uploadThroughput: 500 * 1024, }) # 拦截请求图片提升测试速度 driver.execute_cdp_cmd(Network.setBlockedURLs, {urls: [*.png, *.jpg]}) # 模拟定位 driver.execute_cdp_cmd(Emulation.setGeolocationOverride, { latitude: 31.23, longitude: 121.47, accuracy: 100, })这个能力在测试弱网场景、地图业务、复杂图片站时非常实用建议每个Selenium 4项目都早点研究起来。6.4 结合我的实战经验给你几个中肯的建议从最早用Selenium 2到现在的Selenium 4我自己的体会是不要一上来就想着搭一个“全世界最完整的自动化测试框架”。真实项目里最忌讳的是为了自动化而自动化把每个按钮、每个页面都写成用例结果维护成本比手工测试还高。先跑通“核心链路”这是我一直坚持的启动方式。把登录、主流程、核心数据提交这类的头部场景自动化起来先解决回归痛点再逐步扩充边缘场景。对于UI变化极其频繁、页面重做过好几次的项目自动化脚本不要急着全量覆盖建议先确认项目UI已经进入稳定期再动工。另外选择定位策略时能跟开发约定就一定要跟开发约定。给关键元素加上稳定的data-testid或id属性比你在CSS选择器里层层碰运气高效得多。很多时候自动化脚本难维护不是技术问题而是页面本身没有为自动化提供友好支持。Selenium这套框架入门容易但想在一个项目中稳定可靠地跑起来需要的是工程能力、对业务的理解和足够的耐心。希望这篇文章能帮你把基础打牢少踩几个我已经替你踩过的坑。
返回列表