做Web自动化的人谁没被Selenium折磨过几回呢。我用这东西做了三年的自动化测试和页面巡检自认为已经是“老司机”了但每次新项目启动还是能踩出几个新坑来。Selenium就像一把瑞士军刀什么浏览器都能驱动什么页面都能点一点可它越是万能暗坑就越多。这篇文章不聊高大上的理论就聊聊我在实际做Web自动化、页面巡检、自动化测试框架时真正踩过、填过、又踩穿的那些坑。如果你是刚接触Selenium的小白这篇文章能帮你绕开80%的早期雷区如果你已经写了一阵子脚本那这篇文章里关于等待策略、动态元素定位、无头浏览器配置的细节大概率能解释你之前“明明没问题却一直报错”的困惑。我会从设计思路讲到元素定位再到完整跑通的巡检脚本最后给一份问题排查手册全程都是实操视角你直接抄作业就行。1. 先理清楚Selenium适合做什么不适合做什么1.1 它到底是什么以及常见的三种用法Selenium不是测试工具它是一个“浏览器自动化协议”的实现。它的本质是通过WebDriver协议去操控真实的浏览器模拟人的操作打开页面、点击按钮、输入文字、读取页面数据。所以它能做的事比“测试”大得多。我用它做过三类事情自动化测试最传统的用法跑回归用例代替人工点点点。Web页面自动化巡检这是最近两年用得最多的场景。每天早上自动登录后台系统检查首页是否正常、核心接口是否返回正确、关键按钮是否可点。巡检的重点不是“功能正确性”而是“页面可用性”所以脚本稳定性要求极高。数据采集/自动化脚本比如批量下载报表、自动录入数据、定时处理工单。这类需求本质上也是Web自动化只是不叫“测试”而已。这三类场景对Selenium的依赖方式不太一样。测试讲究断言准确巡检讲究长时间稳定运行采集讲究速度和容错。但坑是通用的下面要讲的每个坑三类场景都躲不开。1.2 为什么还用Selenium而不是Playwright很多人问我现在Playwright和Puppeteer这么火为什么还要用Selenium我承认Playwright确实好用它的自动等待机制、Shadow DOM穿透、移动端模拟都做得更现代。但Selenium有一个无可替代的优势生态和兼容性。你的团队里可能有人只会Python有人只会JavaSelenium对全语言的支持意味着你不需要换技术栈。另外很多企业内部系统是老旧架构浏览器还停留在Chrome 90甚至更老Playwright对这些老版本的支持远不如Selenium灵活。还有一个很现实的问题市面上大部分现成的测试框架、云测平台、报告工具都优先兼容Selenium和CI系统集成时资料最多遇到问题能搜到的解决方案也最多。一句话总结如果做全新项目且没有历史包袱Playwright确实可以优先考虑。但如果你的团队已经用Selenium跑了一堆用例或者需要兼容老浏览器老老实实把Selenium的坑填平比推翻重来划算得多。1.3 框架选型的思路不要一上来就追求“完美架构”我早期犯过一个错误做框架时脑子里全是“最佳实践”想着Page Object、数据驱动、关键字驱动、Allure报告、失败重跑样样都要。结果花了两个星期搭了一个很“漂亮”的框架真正写用例的时间反而被压缩了。后来想明白了Selenium脚本的核心不是框架多华丽而是定位稳定、等待正确、异常可追溯。先跑起来再逐步封装这才是做Web自动化的正路。一个最简单的巡检脚本只有几十行代码但它能稳定跑一个月不出问题比一个5000行的框架每周挂一次更有价值。2. 元素定位和等待策略一半的坑都在这两个话题里2.1 元素定位的优先级我踩出来的经验很多人学Selenium时习惯用XPath因为复制XPath最方便。但XPath是“最后一个选择”不是“第一个选择”。我见过太多人因为复制全路径Xpath导致脚本跑两次就废了——页面结构稍微变动路径就断了。我日常用定位的优先级排序是id - data属性/name - class/标签组合 - 相对XPath - 文本定位。为什么id最稳定页面设计者一般不会乱改data属性次之很多前端框架会自动生成随机id这时data属性是救命的class的问题是有多个值且经常变化文本定位问题更大。举个例子一个登录按钮如果是这么写的button classel-button el-button--primary btn-login-20240901登录/button如果你用//button[contains(class,login)]class里的日期变了照样能匹配。但你如果用了形如html/body/div/div/div/div/form/div[3]/button这种全路径下次页面加了个弹窗这条路径直接失效。2.2 iframe和Shadow DOM两个“找得到但点不到”的重灾区最气人的错误是NoSuchElementException但更气人的是ElementClickInterceptedException——明明代码能找到元素运行时就是点不到。这两个问题十有八九和iframe、Shadow DOM有关。iframe必须先用switch_to.frame切进去操作完再switch_to.default_content()切回来。嵌套iframe要一层一层切换跳层会直接报错。Shadow DOM是Selenium的老大难。老版Selenium面对Shadow DOM基本没辙只能靠execute_script用JS穿透shadow_host driver.find_element(By.CSS_SELECTOR, my-element) shadow_root driver.execute_script(return arguments[0].shadowRoot, shadow_host) target shadow_root.find_element(By.CSS_SELECTOR, .inner-button)新版Selenium 4支持用find_element(By.SHADOW_ROOT, ...)直接拿影子根节点好用多了。但如果遇到closed模式闭合的Shadow DOM用常规方式确实拿不到元素这种时候只能调整策略要么让前端加个data属性暴露出来要么改用坐标点击要么直接放弃这个元素的自动化。2.3 显式等待和隐式等待sleep大法害死人我必须真诚地说一句time.sleep(3)这种写法人人都干过但它是最不靠谱的等法。网络慢一点3秒不够网络快了又白等3秒整个脚本的耗时被无谓拉长。我见过最夸张的项目一个用例10个sleep跑完要5分钟实际操作只有30秒。正确的做法是用WebDriverWait显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10, poll_frequency0.5) wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, #submit-btn))).click()写等待条件时注意区分presence_of_element_located和visibility_of_element_located。前者只管DOM里有这个节点不管它是否可见后者要求元素在页面中可见且尺寸非零。如果你想点击应该等element_to_be_clickable它不但要求可见还检查元素是否被禁用。还有一个大坑隐式等待和显式等待不要混用。隐式等待是查找元素时全局生效的轮询显式等待是逻辑层的轮询两个叠加会导致等待时间不可预测。我的习惯是只用显式等待隐式等待一律设driver.implicitly_wait(0)。def wait_and_click(driver, locator, timeout10): el WebDriverWait(driver, timeout, poll_frequency0.2).until( EC.element_to_be_clickable(locator) ) el.click()这个简单的函数我基本每个项目都会写一遍。它能解决大量“元素存在但点击失败”的问题比如按钮一开始是disabled状态0.2秒轮询会在它变成可点击的第一时间点下去。2.4 动态ID、滚动加载、异步渲染的处理经验现在的Web应用全是Vue/React这种前端框架做出来的页面上充斥着随机生成的ID、异步加载的数据、滚动加载的列表。做这类页面的自动化核心思路是“别跟动态的东西较劲”。动态ID就跳过ID用相对XPath异步渲染就用等待条件而不是固定等待滚动加载可以先执行JS把它滚动到底再查找元素。我常用的滚动方法是driver.execute_script(window.scrollTo(0, document.body.scrollHeight))还有一种情况点击一个按钮之后页面会弹出一个loading遮罩层等接口返回后才消失此时你立刻去点下一个按钮大概率被遮罩拦截。我没找到太优雅的解法比较实用的做法是等遮罩层的display:none或者等它消失wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, .loading-mask)))这个经验我在做后台管理系统时经常用到遮罩层是自动化点击的头号杀手。3. 从零搭一套能长期运行的Web巡检脚本3.1 安装和版本匹配环境配置的深坑新手最常遇到的“SessionNotCreatedException”报错根本原因就是浏览器版本和driver版本不匹配。Chrome从100版本以后几乎每个大版本都对应一个独立的ChromeDriver版本版本不对直接报错。我早期每次Chrome自动更新脚本就跟着挂一次痛苦不堪。后来用了一个叫webdriver-manager的工具它会自动检测浏览器版本并下载对应的driver完美解决这个问题pip install selenium webdriver-managerfrom selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome( serviceService(ChromeDriverManager().install()) )另外注意如果你用Windows系统跑定时任务建议把driver放到一个固定目录并添加到系统PATH而不是每次现下。这样即使断网也能正常跑。3.2 无头模式和Docker容器里的参数选择巡检脚本通常跑在服务器上这就需要无头模式。Chrome 112版本以后推荐用--headlessnew我实测下来比老版稳定不少对新版本和性能追踪的支持也更好from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) # 新版无头模式 options.add_argument(--no-sandbox) # root用户运行时必须加否则报错 options.add_argument(--disable-dev-shm-usage) # 容器/低内存环境必备 options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) # 有些页面在窄屏下布局会崩这几个参数每一个都有故事。--no-sandbox是因为服务器上经常用root用户跑任务Chrome出于安全策略禁止root直接启动浏览器必须加这个参数跳过沙箱--disable-dev-shm-usage是Docker容器里常见的问题默认/dev/shm只有64MB跑了几个标签页就爆了加了参数让Chrome改用其他临时目录--disable-gpu在新版Chrome里加不加影响不大但老环境里不加会出渲染莫名其妙的bug所以我一律带上。3.3 一个完整的巡检脚本示例说一千道一万给一个我实际在用的巡检脚本每天自动登录后台检查首页状态并截图import time from datetime import datetime 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 from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options URL https://admin.example.com/login USERNAME jobber PASSWORD your_password_here def build_driver(): options Options() options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) return webdriver.Chrome( serviceService(ChromeDriverManager().install()), optionsoptions ) def check_login_and_dashboard(driver): wait WebDriverWait(driver, 15, poll_frequency0.5) driver.get(URL) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, #username))).send_keys(USERNAME) driver.find_element(By.CSS_SELECTOR, #password).send_keys(PASSWORD) wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, .login-btn))).click() # 登录成功后等待侧边栏中出现“首页”菜单 wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, .sidebar .dashboard-link))) # 断言关键数据区域渲染出来 data_panel wait.until( EC.visibility_of_element_located((By.CSS_SELECTOR, .stats-panel)) ) text data_panel.text.strip() assert len(text) 0, 首页核心数据区域为空巡检失败 # 截图留档 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) driver.save_screenshot(fdashboard_{timestamp}.png) driver build_driver() try: check_login_and_dashboard(driver) print(f巡检通过: {datetime.now()}) except Exception as e: driver.save_screenshot(ffailure_{datetime.now().strftime(%Y%m%d_%H%M%S)}.png) print(f巡检失败: {e}) finally: driver.quit()注意几个细节截图一定要放在finally之前或者异常分支里方便事后排查print的日志级别要分清楚我推荐直接接入logging模块方便以后对接日志系统最后一定要driver.quit()不要只写driver.close()否则ChromeDriver进程会残留跑几天之后机器上全是僵尸Chrome进程。3.4 用pytest和Allure把脚本变成测试框架巡检脚本基本稳定之后我把它迁移到了pytest框架里好处是可以自动收集失败信息、生成报告、接入CI。pytest的fixture机制天然适合做浏览器初始化和回收import pytest pytest.fixture(scopemodule) def driver(): d build_driver() yield d d.quit() def test_login(driver): check_login_and_dashboard(driver)Allure报告是Selenium自动化使用率最高的报告工具它能把每个用例的截图和日志归档到HTML报告里。我通常只做一件事在断言失败和用例结束时自动截图并attach到Allureimport allure def test_login(driver): try: check_login_and_dashboard(driver) except Exception as e: allure.attach(driver.get_screenshot_as_png(), namefailure_screenshot, attachment_typeallure.attachment_type.PNG) raise e这套组合Selenium pytest Allure Jenkins定时任务完全够支撑中小团队的日常巡检和回归测试。如果你不想搭Jenkins直接Windows计划任务或者crontab跑pytest命令也是一样的效果。3.5 一个偏门的场景Excel VBA里也能用Selenium有段时间我用的是Excel VBA做内部工具的自动化发现VBA也有Selenium的第三方封装叫SeleniumBasic可以直接在Excel里写Driver.FindElementById这样的代码。说实话这个库的年代比较久远基本处于停更状态只支持老版浏览器新Chrome版本兼容性很差。我的体验建议是除非是你只能使用Excel环境、且IT策略不允许安装Python否则不要在这条路上投入时间。VBA的字符串处理和异常处理远远不如Python方便调试一个定位问题的时间够你写十个Python用例了。4. Selenium问题排查手册报错对照表4.1 高频报错的根因和解决办法汇总很多朋友遇到报错就蒙圈其实Selenium的报错翻来覆去就那么几个我把它们整理成了一张排查表报错类型常见根因解决办法SessionNotCreatedExceptiondriver版本和浏览器版本不匹配用webdriver-manager自动匹配或手动下载对应版本NoSuchElementException元素定位符错误、页面没加载完成、在iframe里检查定位符改显式等待switch_to.frame切入ElementClickInterceptedException元素被遮罩层或弹窗遮挡等待遮罩层消失用JS点击绕过滚动到元素可见再点StaleElementReferenceException页面DOM刷新后原先的元素引用失效重新获取元素对象避免在循环中复用元素引用TimeoutException网速慢、等待时间太短、异步渲染没完成延长超时时间细化等待条件InvalidArgumentExceptionURL格式错误或参数类型不对检查driver.get()的地址是否带了合法协议http/httpsWebDriverException浏览器进程崩溃、端口被占用清理僵尸Chrome进程重启驱动换端口那我自己来说NoSuchElementException是出现频率最高的。排查它的思路是按照一条固定路径来的先确认定位符在当前页面DOM里真实存在再确认代码执行到那一步时页面是否已经加载完成最后检查元素是不是在iframe或Shadow DOM里。按这个顺序查90%的问题能快速定位。4.2 一个印象深刻的排坑经历诡异点击失效有一段时间我一个巡检脚本里的“保存”按钮总是偶发点击失败。当时页面是弹出一个抽屉式的表单填完信息点“保存”。用显式等待后element_to_be_clickable也通过了但点击就是不生效偶尔又成功像玄学一样。后来我发现问题的真正原因是抽屉里的“保存”按钮被一个透明的遮罩元素盖住了。虽然Chrome判断这个按钮是“可见”的但用户点击时鼠标真正落到的却是上层的遮罩元素。Selenium的点击是直接派发事件给目标元素而此时遮罩恰好拦截了事件或者被Chrome判定为不可点抛出ElementClickInterceptedException。我的解决方式很简单粗暴但有效driver.execute_script(arguments[0].click();, save_button)JS的click()不模拟真实用户鼠标而是直接调用该元素的DOM click事件绕过覆盖问题。但要注意它也不会触发浏览器对“元素是否真的可点”的校验所以用之前要自己确认按钮不在禁用状态。代码里可以先用is_enabled()检查一下。4.3 长时间运行的稳定性问题和定时任务的陷阱巡检脚本连续跑一段时间之后会遇到两类问题一是Chrome进程内存涨得飞快二是driver连接失效或卡死。内存问题通常可以用定期重启浏览器会话缓解比如每执行50个用例就重新创建一次driver实例。卡死问题则要注意driver的加载策略options.page_load_strategy eagereager模式只在DOMReady之后立即返回不等图片和样式表全部加载完。对于巡检场景页面主体元素一般都在DOM里这个策略能明显缩短单页等待时间减少因某个外部资源拖垮整个页面的情况。定时任务里还有一个特别容易踩的坑crontab或计划任务执行时环境变量和手动执行时不一样。ChromeDriver在PATH里的路径、Python解释器的路径、工作目录都可能不同导致定时任务失败但手动跑又正常。我的习惯是在脚本开头统一设置import os os.environ[PATH] /usr/local/bin: os.environ.get(PATH, )或者在shell脚本里显式设置PATH和PYTHONPATH这一步能省去无数“定时任务时灵时不灵”的烦恼。4.4 能不用Selenium的地方就别用最后一块内容聊聊Selenium的边界。它的优点也是它的缺点Selenium操纵的是真实浏览器所以它重、慢、吃资源。如果一个任务只是请求HTTP接口或者只需要检查接口返回的JSON完全没有必要用Selenium直接requests更轻。我的原则是能用HTTP请求解决的绝不开浏览器只有在接口不好模拟、需要执行JS、需要真实浏览器环境时才启动Selenium。比如做页面巡检时我会先写一个纯请求的预检检查服务器是否在线、接口是否返回200只有预检通过后才用Selenium做页面级的完整检查。这样能大大减少无头Chrome的启动次数降低整体资源开销。做自动化巡检或者测试Selenium的坑很多但绝大多数坑都是可以被规则化的。定位用相对路径、等待用显式等待、操作前先确认iframe和遮罩层、跑完必quit、定时任务固定PATH——把这些基本动作变成习惯脚本的稳定性会有一个质的提升。我个人的体会是做Web自动化最重要的不是写多少用例而是让每一个用例都跑得足够久、失败时给的信息足够清楚。因为巡检的价值在于每天跑完给你一个可信的结果而不是今天跑通明天就挂。这个方向做扎实了再去追求框架、平台、数据驱动才是有意义的。