ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Selenium WebDriver实战:从环境配置到元素定位与自动化测试

Selenium WebDriver实战:从环境配置到元素定位与自动化测试 1. 为什么我最终选择了 Selenium WebDriver 做浏览器自动化做浏览器自动化这些年最早我也用过按键精灵、用过 Windows 平台上的各种“录制回放”工具但它们基本都有同一个毛病换台电脑、换个浏览器版本脚本就废了。后来切到 Selenium WebDriver算是一步到位。它通过官方提供的 WebDriver 协议直接驱动浏览器内核本质上是把浏览器当成一个“被代码控制的进程”而不是靠坐标、靠屏幕图像去模拟点击所以稳定性高出好几个量级。WebDriver 的定位很明确它是一套跨浏览器的自动化控制标准。你在代码里写driver.get(url)浏览器就会真的打开这个网址写driver.find_element(...)浏览器就会真的去 DOM 里找这个元素。它不关心你用的是 Chrome、Edge 还是 Firefox因为每一种浏览器都有自己的 driver 实现ChromeDriver、EdgeDriver、GeckoDriverSelenium 只是统一了调用入口。这种“统一入口 各浏览器自己实现”的设计让同一套脚本几乎不需要改动就能跑在不同的浏览器上这在做兼容性回归测试时非常香。再说得直白一点Selenium 能解决什么问题就是那些“需要打开网页、操作页面、断言结果”的场景。最常见的三大类第一类是 UI 自动化测试代替人去点击按钮、填写表单、检查页面是否正常第二类是数据采集比如需要登录后才能抓取的页面、需要翻页的动态表格第三类是自动化操作比如批量上传素材、批量下载文件、定时打卡这种日常重复劳动的替代。我自己的项目里三类都碰过每次用 Selenium 写完脚本心里都会有一种“这类活以后再也不用我亲手干了”的舒坦。适合谁来学如果你已经会一点点 Python 或 Java能看懂基本语法同时又受够了每天重复点网页那么 Selenium WebDriver 非常值得花一个周末系统过一遍。如果你是纯测试新手也没关系你只需要把浏览器当成一个“可以远程控制的机器人”先跑通最小例子再逐步加深很快就能上手。提示Selenium 4 是当前主流版本API 比 3 变化不小尤其是等待方式和窗口管理的部分。下面的代码示例均基于 Selenium 4.x如果你还在用 3.x建议先升级。2. 环境准备与第一次调用浏览器2.1 安装 Selenium 和配置浏览器驱动很多人第一步就卡在“驱动”上。Selenium 本身只是一个客户端库它要真正控制浏览器还需要一个中间的“翻译员”浏览器驱动。以 Chrome 为例你本机装的是 Chrome 浏览器但 Selenium 不能直接“抠”浏览器还得靠 ChromeDriver 去跟浏览器内的调试端口通信。所以最基础的三件套是浏览器本体、对应版本的驱动、Selenium 库。驱动版本和浏览器主版本必须匹配比如 Chrome 126 的浏览器最好也找 126 版本的 ChromeDriver否则大概率会报session not created或者This version of ChromeDriver only supports Chrome version xxx的错误。手动下载驱动的问题在于版本一多就乱而且要自己写一堆路径逻辑。更省心的方案是用webdriver-manager这个 Python 库它会自动检测浏览器版本并下载对应驱动基本一行代码搞定from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome(serviceService(ChromeDriverManager().install()))Java 项目里同样可以用 Selenium ManagerSelenium 4.6 以上版本自带会自动管理驱动版本。如果你用 Java 的 Maven 项目加上几个依赖就能开跑dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.21.0/version /dependency2.2 最快可运行的示例环境装好之后最快验证环境是否 OK 的方法是打开一个页面打印标题然后退出。以下用 Python 演示import time from selenium import webdriver driver webdriver.Chrome() try: driver.get(https://example.com) print(driver.title) time.sleep(2) finally: driver.quit()这里driver.get()是等待页面加载完成的绝大多数情况不需要手动 sleep。最后一句driver.quit()很多人会漏写写完脚本窗口不关时间久了电脑里全是僵尸浏览器进程。我建议不管脚本跑到哪一步都放在finally里确保退出。如果你跑的是 Java同样逻辑大概是这样import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; public class FirstScript { public static void main(String[] args) { WebDriver driver new ChromeDriver(); try { driver.get(https://example.com); System.out.println(driver.getTitle()); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { driver.quit(); } } }第一次跑通这个例子你的环境就已经完全没有问题了。2.3 安装时的三个关键陷阱第一关于驱动路径。如果你不想用自动管理工具手动配置时需要注意老写法System.setProperty(webdriver.chrome.driver, path)在 Selenium 4 里虽然还能用但官方更推荐用Service对象。第二Chrome 的自动更新会把版本顶上去导致驱动不匹配我经历过太多次“昨天还能跑今天一起来全红了”后来统一改用 webdriver-manager 就不再受这个折磨。第三如果你在公司内网环境下载驱动很可能因为网络受限超时这时可以手动下载驱动放到某个固定目录再用Service(executable_path本地路径)指定稳定可靠。注意不要把驱动下载这一环想得太复杂。它只是让代码和浏览器之间建立起通信的“门卫”版本匹配就是唯一的硬性要求。3. 常用方法按真实使用习惯排序3.1 元素定位find_element 的几种写法日常写 Selenium 脚本90% 的时间都在和“定位元素”纠缠。元素找得准后面全是体力活元素找得飘后面全是坑。WebDriver 提供的定位方式有id、name、class name、tag name、link text、partial link text、xpath、css selector。我个人的使用频率排序是 id css selector xpath 其余。id 是页面里最稳定的标识有 id 的元素直接用By.ID最省心from selenium.webdriver.common.by import By driver.find_element(By.ID, username).send_keys(admin)没有 id 的时候css selector 通常比 xpath 简洁且性能更好。比如某个按钮的唯一特征是有classbtn-primary那就可以写By.CSS_SELECTOR, button.btn-primary。xpath 则是最后的兜底方案尤其对于复杂层级结构xpath 的//和..可以非常灵活地“往上找祖先元素”。写 xpath 有一些小技巧。不要一上来就复制浏览器里生成的绝对路径/html/body/div[1]/div[2]/form/...那种路径只要页面多一个 div 就废了。要用相对路径比如//form[idlogin-form]//input[nameaccount]。再比如“找到包含指定文本的按钮”可以写成//button[contains(text(), 立即登录)]这种写法在中文页面上特别实用因为中文文本一般不会频繁变动。3.2 操作元素点击、输入、提交表单定位到元素后的操作其实套路固定。输入框用send_keys()点击用click()清空用clear()。但要特别注意一个场景有些输入框有前置校验直接send_keys会触发实时校验导致输入被截断稳妥做法是先click()聚焦再clear()清空最后send_keys()。表单提交也有讲究。如果表单里有提交按钮直接button.click()没问题如果没有按钮或者按钮藏在 iframe 里可以尝试找到表单元素后调用submit()方法。submit()会触发浏览器的原生表单提交逻辑在极端情况下比点击更可靠。不过 Selenium 4 里submit()被标记为过时所以我的建议是优先保证按钮定位的准确性。还有一个高频操作键盘按键和鼠标悬停。登录后需要按回车或者某个下拉菜单需要鼠标悬浮才展开这时候要用ActionChainsfrom selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys ActionChains(driver) \ .move_to_element(hover_menu) \ .perform() search_input.send_keys(Selenium) search_input.send_keys(Keys.RETURN)ActionChains相当于一个动作队列你可以把移动、点击、拖拽、按键都塞进去最后perform()一次性执行。图片滑块验证、拖拽排序这类操作后面会单独讲。3.3 同步等待隐式、显式、强制等待自动化脚本最容易出的问题不是“点不到”而是“页面还没加载完元素就去找”。这块必须建立正确的等待姿势。强制等待就是time.sleep(10)无脑但效率极低。隐式等待是driver.implicitly_wait(10)设置一次对整个 driver 生效它表示“查找元素时如果没找到最多等 10 秒”但它的副作用是即使元素早就出现每次查找也会等待固定轮询拖慢脚本。显式等待是我最推荐的用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()显式等待的好处是可以精确表达“我要等什么”等按钮可点击、等元素出现、等某个文本出现、等待 iframe 加载。它让脚本的语义非常清晰也方便排查问题。我在实际项目中的习惯是全局设一个 10 秒的隐式等待兜底关键操作再用显式等待精确定位。4. 进阶但真正必要的用法4.1 下拉框、Alert 弹窗、iframe 切换、多窗口处理这几个场景属于“不做不知道一踩一个坑”的类型。先说下拉框。原生select元素不能用普通的click加send_keys需要用Select类from selenium.webdriver.support.ui import Select select_element Select(driver.find_element(By.ID, province)) select_element.select_by_visible_text(广东省) select_element.select_by_value(440000) select_element.select_by_index(1)select_by_visible_text是最直观的但要注意完全匹配写错一个字就选不中。如果页面的下拉框是自定义组件比如那种点击后弹出一堆div的模拟下拉框Select就不管用了这时需要先点击触发下拉再用显式等待找到下拉列表项去逐个点击。再说 Alert。JavaScript 的alert()、confirm()、prompt()弹窗出现后页面操作会被阻断Selenium 需要切换到 alert 上处理driver.switch_to.alert.accept() # 确定 driver.switch_to.alert.dismiss() # 取消 text driver.switch_to.alert.text # 获取弹窗文本iframe 又是另一个大坑。如果页面里的元素被藏在iframe里直接find_element是找不到的因为 DOM 上下文不对。要先切进去driver.switch_to.frame(frameId) # 通过 id 或 name driver.switch_to.frame(driver.find_element(By.XPATH, //iframe[titlemain])) # 操作完记住切回主文档 driver.switch_to.default_content()多窗口处理常用于那种“点击后弹新窗口”的场景。处理逻辑是先记录当前的窗口句柄集合点击后再对比出新的句柄切换过去操作完再切回来handles_before driver.window_handles driver.find_element(By.LINK_TEXT, 新窗口链接).click() WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) len(handles_before)) new_handle [h for h in driver.window_handles if h not in handles_before][0] driver.switch_to.window(new_handle)4.2 文件下载如何等到下载完成再执行下一步热词里有一条“selenium怎样使文件下载完成之后才进行下一步”。这个问题非常典型因为driver.get()只等待页面跳转完成但文件下载是浏览器层面的事Selenium 根本不知道。解决思路有两个方向一是修改浏览器的下载设置让下载不再弹确认框、固定下载目录二是脚本主动去轮询文件系统判断文件是否下载完成。修改 Chrome 下载设置需要用Options和prefsimport time import os from selenium import webdriver DOWNLOAD_DIR os.path.abspath(./downloads) options webdriver.ChromeOptions() prefs { download.default_directory: DOWNLOAD_DIR, download.prompt_for_download: False, download.directory_upgrade: True, safebrowsing.enabled: True, profile.default_content_setting_values.automatic_downloads: 1, } options.add_experimental_option(prefs, prefs) driver webdriver.Chrome(optionsoptions) # 点击下载按钮 driver.find_element(By.ID, download_btn).click() # 等待文件出现且大小不再变化 def wait_for_download_complete(timeout60): end_time time.time() timeout while time.time() end_time: files [f for f in os.listdir(DOWNLOAD_DIR) if f.endswith(.crdownload) False] # 处理浏览器未完成时常见 .crdownload 临时文件 complete_files [f for f in files if not f.endswith(.part) and not f.endswith(.crdownload)] if complete_files: latest_file os.path.join(DOWNLOAD_DIR, complete_files[0]) size1 os.path.getsize(latest_file) time.sleep(1) size2 os.path.getsize(latest_file) if size1 size2: return os.path.join(DOWNLOAD_DIR, complete_files[0]) time.sleep(0.5) raise TimeoutError(download timeout) file_path wait_for_download_complete() print(下载完成:, file_path)这段脚本里最关键的是“文件大小稳定判断”。因为下载中的文件大小会一直变只有写入完成后大小才会稳定。如果你只想简单判断文件是否存在用os.path.exists就可以但会存在“下载还没结束就误判为完成”的情况。实测下来加大小判断能避免大量偶发问题尤其是大文件下载。4.3 图片滑块验证码的自动化思路热词里还提到“自动化 selenium 网页拼图验证怎么自动化”“selenium 图片滑块验证”这类需求本质是页面里有一张带缺口的背景图一个滑块按钮你需要把滑块拖动到缺口位置。先说清楚边界我只是站在技术研究的角度分析自动化可行性不鼓励把这类能力用在对真实平台的恶意破解上用于自己拥有的测试环境、内部系统或技术支持中是没问题的。滑块验证的核心难点有两点算距离拖到位。算距离的常见思路是用PIL对两张图片做像素级对比找到缺口位置。如果页面提供两张图原始图和带缺口的图那直接对比差异较大的区域即可如果只有一张带缺口的图就需要通过边缘检测算法找缺口边界。找到缺口横坐标后滑块初始位置一般是固定值两者的差值就是需要拖动的横向距离。拖动的关键是执行“先慢后快再慢”的轨迹直线一次性拖过去会被识别为机器操作。用ActionChains可以这样模拟from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By slider driver.find_element(By.CSS_SELECTOR, div.slider-btn) ActionChains(driver) \ .click_and_hold(slider) \ .move_by_offset(xoffsettarget_distance, yoffset0, duration1) \ .release() \ .perform()不过这里有个经验move_by_offset的duration参数在不同版本 Selenium 里支持度不一样稳妥方式是自己写一段轨迹逐步move_by_offset中间插入time.sleep短暂停顿模拟真人拖动时的加速减速过程。另外很多滑块验证码会校验“拖动轨迹是否符合人类习惯”所以不能匀速直线建议分段移动前 1/3 快速、中间 1/3 微调、最后 1/3 慢速对齐。这只是一个基础方案遇到更复杂的滑块可能还需要随机抖动、路径曲线模拟等处理那又是另一篇文章了。5. 实际项目中常遇到的问题与排查技巧5.1 高频报错对照表以下是我在多个项目里整理出来的报错对照非常适合贴在工位旁边报错信息原因排查方向NoSuchElementException元素定位不到确认定位表达式是否正确、是否在 iframe 中、页面是否未加载完成ElementNotInteractableException元素存在但不可交互是否被遮挡、是否隐藏、是否只读StaleElementReferenceException页面刷新后旧的元素引用失效重新查找元素不要缓存引用SessionNotCreatedException驱动版本与浏览器版本不匹配更新驱动或浏览器版本TimeoutException等待条件超时检查网络、元素是否真的会按预期出现InvalidSelectorException定位表达式语法错误去浏览器控制台先用$()或$x()验证表达式运维老手都知道StaleElementReferenceException是列表循环里的高频问题你拿到一个元素列表然后循环里做点击结果每次点击都会刷新页面循环第二次的时候之前缓存的元素引用已经失效了。解决方式很简单每次循环都重新find_element不要复用列表里的元素对象。5.2 截图与错误日志调试自动化脚本的两板斧脚本跑挂了最反直觉的做法是直接看代码猜。我强烈建议在异常处理里强制截图顺便打印当前页面源代码import datetime try: driver.find_element(By.ID, submit).click() except Exception as e: now datetime.datetime.now().strftime(%Y%m%d_%H%M%S) driver.save_screenshot(fdebug_{now}.png) with open(fpage_source_{now}.html, w, encodingutf-8) as f: f.write(driver.page_source) raise e这张截图和 HTML 源码基本能让你定位 80% 的失败场景是元素没找到还是点击被遮罩拦住了还是页面压根没跳到预期地址。尤其是和前端同事对需求的时候一张截图远比一段 log 有说服力。5.3 驱动配置与 WebDriverManager热词里有“ui 自动化浏览器驱动配置”值得展开说。现在很多项目里仍然能看到这样的老代码driver webdriver.Chrome(executable_path./chromedriver)但这个参数在新版本 Selenium 里已经废弃了我建议的写法是from selenium.webdriver.chrome.service import Service service Service(executable_path/opt/chromedriver/chromedriver) driver webdriver.Chrome(serviceservice)如果你经常换环境用webdriver-manager是最省心的。不过要注意自动下载驱动的过程依赖外网如果你的执行环境不能访问外网就需要把驱动放到固定的公共目录并通过环境变量或者 Service 参数统一指定。我们在 CI 服务器上就是这么干的先在构建脚本里把驱动下载好放到/usr/local/bin代码里直接Service(/usr/local/bin/chromedriver)。6. 接入测试框架与自动化框架设计6.1 最小可用的 Pytest 示例Selenium 本身不是测试框架它只是一个浏览器操作库。要做“自动化测试”通常还要配合 pytest、JUnit 这类测试框架来完成用例管理、断言和报告输出。以 Python 为例一个极其简单的用例长这样import pytest from selenium import webdriver pytest.fixture def browser(): driver webdriver.Chrome() yield driver driver.quit() def test_login(browser): browser.get(https://example.com/login) browser.find_element(By.ID, username).send_keys(tester) browser.find_element(By.ID, password).send_keys(123456) browser.find_element(By.ID, submit).click() assert browser.find_element(By.CSS_SELECTOR, .user-name).text testerpytest的 fixture 机制特别适合管理浏览器生命周期一个用例一个浏览器实例互不干扰。如果你在写更大型的框架可以把“启动浏览器”“读取配置”“失败截图”这些逻辑抽成公共模块用例层只关注具体的页面交互逻辑。6.2 页面对象模式告别堆代码写过几百行自动化脚本之后你就会发现重复代码开始变多每个用例里都在find_element、send_keys、click。这时候需要引入 Page Object 模式也就是把每个页面的元素定位和操作封装成类。比如登录页独立成一个LoginPage类用例里只需要调用login_page.login(user, pass)页面元素如果变了只改这个类里的定位器就行用例代码完全不用动。这个模式在 Java 和 Python 的 Selenium 项目里都是主流实践。6.3 无头模式与并发执行自动化在服务器上跑的时候没有图形界面这时需要开启 headless 模式options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) driver webdriver.Chrome(optionsoptions)无头模式跑得比有头模式快但个别前端动画、懒加载行为在无头模式下表现可能不一样所以我的建议是“本地调试用有头模式CI 上用无头模式”。如果你要做大规模回归建议考虑 Selenium Grid 或者用 pytest-xdist 做并行调度不过那已经是框架设计层面的内容了。7. 我踩过的一些坑和最终建议最后分享几个从实践中总结出来的体会。第一能用显式等待就不要用 fixed sleep。很多人一遇到加载慢就粗暴加time.sleep(5)当场能跑但换台电脑、换个网络就挂了。显式等待写起来多几行代码换来的是真正稳定的脚本。第二浏览器自动升级是隐形杀手。Chrome 的自动更新会让你的 ChromeDriver 隔一段时间就“过时”。要么用 webdriver-manager 自动化匹配要么在 CI 环境固定浏览器版本。别小看这个细节它可能消耗你大量不必要的排查时间。第三脚本里一定记得退出驱动并且最好在finally或上下文管理器中执行。资源泄漏这种事写一次脚本可能感觉不到但在服务器上跑多线程时就能体会到后果了。第四不要把 Selenium 当成万能的。有些页面用了极其复杂的 Shadow DOM有些页面上的元素是 canvas 绘制的这些场景 Selenium 处理起来很吃力。如果遇到这种情况可以换思路用 Playwright 或 CDP 协议直接调浏览器底层接口甚至在数据有限时直接用 HTTP 请求模拟反而更省事。我在实际项目中的习惯是先花半小时把页面最关键的 5 个元素用 id 定位方式固化下来遇到一时半会儿定位不准的元素就直接打开浏览器控制台用$()或$x()试表达式验证通过再写到代码里。用 Selenium 做浏览器自动化从来不是“写一遍就一劳永逸”而是“写一个稳定环境下的高效脚本同时为环境变化留好接口”。我希望这篇文章能把常用的那些方法串起来给你的自动化之路至少省下一整周的摸索时间。
返回列表