
Selenium 自动化测试这个系列写到第九篇终于聊到大家问得最多的话题常用函数。说句实在话能用 Selenium 跑起脚本的人不少能把 Selenium 用利索的人其实不多。我见过不少测试新人把整个脚本写成 500 行的线性流程定位元素只用 id等待只用 sleep上传文件靠手动敲回车换台机器、换套测试环境脚本就全黄了。去翻 Selenium 的 API 文档又太长英文文档读起来费劲网上博客东一篇西一篇抄过来还各种版本混用。这篇文章我想做一件比较实在的事情把 Selenium 自动化测试里最高频、最常用的函数按照实战场景一条条捋清楚。从元素定位的八个入口到点击、输入、下拉、弹窗、iframe 切换、JS 执行从三种等待机制的取舍到文件上传下载的完整方案尽量做到拿来就能用、用的时候知道为什么。适合刚入行想系统掌握 Selenium 的测试同学也适合写过一阵子脚本但代码越写越乱、想重整思路的同行。先说明一点我不会把官方文档里所有函数都列一遍只挑实战里真正用得到的。官方文档是字典这篇是实战地图。1. 先从全局看Selenium 常用函数到底在解决什么问题1.1 为什么说“能跑通”和“会使用”是两回事很多同学刚接触 Selenium 时觉得无非就是 find_element 找到元素然后 click、send_keys。这想法不能算错但过于粗糙。拿一个最简单的登录场景举例打开页面输入用户名和密码点击登录按钮等待跳转最后断言页面上出现了“欢迎回来”这几个字。这短短几步里你实际用到的函数包括元素定位函数、send_keys、click、显式等待里的 expected_conditions、获取文本的 text 属性、断言方法。任何一个环节用错版本或选错策略脚本都可能在中途崩掉。所以 Selenium 的常用函数本质上是在解决一个核心问题如何稳定地与真实浏览器中的页面元素进行交互。这个“稳定”两个字很关键。测试脚本区别于一次性爬虫脚本它要反复运行要在不同的测试环境、不同的数据状态下都能给出一致的结果。这就要求你不仅要会用某个函数还要知道它适合哪种场景、不适合哪种场景。比如 click() 遇到被遮挡的元素会抛异常而 execute_script 可以绕过遮挡强制点击但强制点击又不一定能验证用户真实路径下的交互。这些权衡就是“会使用”和“能跑通”的区别。另外有一点必须提醒Selenium 从 4.0 开始已经彻底移除了find_element_by_id、find_element_by_class_name这样一类旧式定位方法。现在官方推荐统一使用By类来指定定位策略。代码差异很小但影响面很大——网上大量老博客和旧项目还在用旧写法如果你在跑新版本 Selenium照抄就会直接报错。# Selenium 3 的老写法新版本已不可用 driver.find_element_by_id(username) # Selenium 4 的标准写法 from selenium.webdriver.common.by import By driver.find_element(By.ID, username)这不是为了折腾而折腾而是框架层把“查找策略”和“查找方法”解耦了。以前我们想用 CSS 定位就要换一个方法名现在只需要换 By 的第一个参数。理解和接受这个变化你后面看文档、搜代码时会轻松很多。1.2 按行为功能把函数归类四个抽屉我自己在带新人时习惯把 Selenium 的常用函数装进四个抽屉遇到问题先判断该开哪个抽屉再去查具体方法名。这个思路能避免死记硬背也方便后续封装。定位抽屉find_element、find_elements配合By.ID、By.CSS_SELECTOR、By.XPATH等策略解决“元素在哪”的问题。状态抽屉等待函数、is_displayed()、is_enabled()、is_selected()解决“元素此刻能不能操作”的问题。操作抽屉click()、send_keys()、clear()、Select类、ActionChains、switch_to解决“如何对元素做动作”的问题。控制抽屉get()、back()、forward()、refresh()、execute_script()、get_screenshot_as_file()解决“驱动浏览器整体行为”的问题。这个分类的价值在于你遇到一个没写过的场景时先判断它属于哪一类。比如突然要处理一个文件上传你第一反应就知道这是“操作抽屉”然后很快联想到上传文件本质上是给input[typefile]输入一个路径那自然就想到了send_keys。再比如脚本运行到某一步总是找不到元素你会想到这是“状态抽屉”的问题——元素还没加载出来需要用等待机制而不是换一个更复杂的定位表达式。我见过很多同学花大量时间背 XPath 语法遇到元素定位不到就疯狂换表达式其实根子的问题是没做好等待。这就是分类意识的重要性。1.3 Selenium 4 之后值得留意的对象变化讲 Selenium 常用函数绕不开环境适配。除了上面说的By类Selenium 4 还引入了相对定位器也就是通过位置关系来找元素。比如“某个按钮左边的输入框”“某个元素下方的链接”这类需求以前只能写复杂 XPath现在可以直接用with_tag_name加left、above、below、near来相对定位。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) )这个特性很有用尤其是面对一些布局规整、但没有 id 和稳定 class 的页面。不过要提醒的是相对定位器对页面布局敏感响应式页面下很容易失效所以生产脚本里我一般还是会优先用 XPath 结合文本和属性定位相对定位器主要用于快速调试。2. 元素定位八大策略与真实项目里的选择逻辑2.1 八大定位方式速查与优缺点对照元素定位是自动化测试的基础因为后续所有操作都得先拿到元素对象。Selenium 的定位策略一共有八种我把它们整理成一张对照表方便你快速决策定位方式用法示例优势劣势IDBy.ID, username唯一性强、性能好开发不一定给 idNAMEBy.NAME, email表单标签常见可能重复出现CLASS_NAMEBy.CLASS_NAME, btn-login简单直观多类名时容易写错TAG_NAMEBy.TAG_NAME, div可用于批量查找命中范围太大LINK_TEXTBy.LINK_TEXT, 登录精确定位超链接只适用于 a 标签PARTIAL_LINK_TEXTBy.PARTIAL_LINK_TEXT, 登适合链接文本过长不够精确CSS_SELECTORBy.CSS_SELECTOR, #login .btn语法简洁、性能好复杂关系可读性差XPATHBy.XPATH, //input[nameusername]表达能力最强相对较慢、易写乱实际项目中我的选择顺序大致是IDCSS_SELECTORXPath 其他。ID 能唯一定位就优先用 ID但别迷信 ID——很多前端框架会生成动态 ID比如user_1637588392这种带时间戳的这种情况 ID 反而最不可靠。CSS 选择器性能好、语法简洁适合定位有稳定 class 或属性结构的元素。XPath 虽然性能上略逊于 CSS但它的文本匹配和轴功能在元素没有稳定属性时几乎是唯一解。顺带一提现在很多团队也开始用 Playwright 做 Web UI 自动化两者在定位策略上有不少相似之处。Selenium 里用 By.XPATH、By.CSS_SELECTORPlaywright 里也有 locator(css...)、locator(xpath...) 的对应写法移动端用 Appium Inspector 拿到的同样是 xpath、id、accessibility id 这类定位信息。所以把 Selenium 的定位逻辑吃透换工具时成本是很低的。2.2 动态属性与复杂 DOMXPath 的几种实战写法说到动态属性很多前端框架生成的页面id 每次刷新都会变。比如一个订单列表的“查看详情”按钮id 可能是detail_20250101第二天跑脚本就变成detail_20250102如果用绝对的 ID 定位就完蛋了。这种场景下 XPath 的contains、starts-with、ends-with能救命。# 通过 contains 匹配属性片段 detail_btn driver.find_element( By.XPATH, //button[contains(id, detail_)] ) # 通过 contains 匹配文本内容 login_text driver.find_element( By.XPATH, //a[contains(text(), 立即登录)] ) # 当前选中项与条件叠加 city_option driver.find_element( By.XPATH, //ul[classcity-list]/li[contains(text(), 上海)] )还有一种很实用的组合用and同时约束多个条件比单纯写一个超长属性更稳固。比如某按钮的 class 是btn submit-btn页面里可能还有别的btn你就可以写成//button[contains(class, submit-btn) and typesubmit]遇到列表类的结构XPath 索引也很有用。例如一个商品列表里每个商品卡片都是div.product-item你想点第三个的“加入购物车”可以写(//div[contains(class, product-item)])[2]//button[contains(text(), 加入购物车)]注意这里外层括号很重要XPath 的索引是从 1 开始的而且(//div)[2]和//div[2]含义完全不同前者是第 2 个 div 元素后者是每个父节点下的第 2 个子 div。这个细节很容易踩坑。另外如果元素在深层级结构里尽量用相对路径而不是从/html/body/...开始的绝对 XPath。绝对 XPath 又长又脆弱页面加一个 div 层级就全断了。我常用的做法是找到离目标元素最近的、有稳定 id 的祖先节点从那里往下相对定位。2.3 find_elements 的用法和边界find_elements返回的是一个 WebElement 列表很多新手看到这个名字以为只是“多个元素”的意思其实它在实战里还有几个特殊用处。第一批量操作同一类元素。比如页面上有一组勾选框你想全选可以直接遍历checkboxes driver.find_elements(By.CSS_SELECTOR, input[typecheckbox]) for checkbox in checkboxes: if not checkbox.is_selected(): checkbox.click()第二对列表数量做断言。比如搜索一个关键词后验证结果数量大于 0results driver.find_elements(By.CSS_SELECTOR, .search-result-item) assert len(results) 0, 搜索结果为空第三利用它“找不到元素时返回空列表”的特性做元素是否存在的前置判断。注意这里的差异find_element找不到元素会直接抛NoSuchElementException而find_elements返回[]。所以有时候写成toast driver.find_elements(By.CSS_SELECTOR, .el-message--success) if toast: print(操作成功提示出现)这比 try/except 更轻量适合做一次性的软检查。2.4 定位不到元素时的第一反应清单我在带项目时发现绝大多数“定位不到元素”的问题并不是定位表达式写错了而是元素的状态不对。所以我把排除思路整理成了一个固定顺序每次遇到问题照着过一遍元素是否加载出来了页面是异步渲染的话元素可能在几秒后才出现——此时应该加显式等待而不是继续找表达式。元素是否在 iframe 里如果页面里有内嵌框架Selenium 默认只操作主文档必须先switch_to.frame()才能看到 iframe 内部元素。元素是否在新窗口/新标签页点击某些“查看详情”“跳转外部链接”按钮后页面可能开了新窗口需要先切换到对应的 window_handle。元素属性是不是动态的如果 id、class 每次刷新都在变就需要换用contains、text()这类模糊匹配。元素是否可见、可点击如果元素被弹窗、遮罩层挡住了即使定位到了click 也会报ElementNotInteractableException这时候要考虑先关闭遮罩或者用 JS 滚动到元素。这五条里前三条的效率提升最明显。尤其是 iframe我见过太多同学卡在 iframe 上两小时最后发现只是缺少一行切换代码。3. 常用操作函数点击、输入、下拉框、弹窗、窗口切换3.1 基础交互函数的细节差异定位到元素之后日常操作离不开几个基础函数。先说说 click()。它模拟的是真实用户鼠标点击WebDriver 会先把鼠标移到元素中心位置再点击。这个设计有好处也有坏处好处是贴近真实用户路径坏处是如果元素被别的元素遮挡或者元素不在当前可视区域内click 就会抛异常。元素被遮挡时我通常会先看为什么遮挡——是弹窗没关还是页面有悬浮层而不是无脑用 JS 强制点击因为强制点击无法验证真实用户能不能点得到。send_keys() 相比 click 要直白得多它往元素里输入字符串。但有几个隐藏细节from selenium.webdriver.common.keys import Keys search_box driver.find_element(By.ID, search) search_box.send_keys(自动化测试) search_box.send_keys(Keys.ENTER) # 模拟回车替代点击搜索按钮用Keys.ENTER代替按钮点击在某些场景下更稳尤其是按钮在动态加载列表里位置不稳定的时候。另外 send_keys 也可以模拟快捷键组合比如Keys.CONTROL, a是全选、Keys.CONTROL, c是复制这在处理某些富文本输入框时有用。clear() 很好理解清空输入框。不过有的自定义输入框是通过 JS 控制的clear() 可能清不掉这时可以send_keys(Keys.CONTROL, a)再send_keys(Keys.DELETE)相当于先全选再删除。至于 submit()它是模拟表单提交。但现在绝大多数前端都是 AJAX 提交点击登录按钮根本不会走原生 submit 事件所以这个函数在传统页面上还有点用在现代前端框架下基本被 click 取代了。3.2 下拉框、复选/单选、弹窗的优雅处理下拉选择框在不同框架里长得完全不一样处理方式也要分开。原生 HTML 的select标签Selenium 提供了专门的Select类支持按可见文本、按 value 属性、按索引三种方式选择from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.ID, city)) select.select_by_visible_text(上海) # 按显示文本 select.select_by_value(sh) # 按 value 属性 select.select_by_index(2) # 按选项索引从0开始这里最常见的坑是页面用的是自定义下拉组件也就是 div ul 模拟的下拉框根本没有select标签。这时候用Select类直接报错。要处理这种下拉框正确思路是模拟真实用户的操作先点击下拉框让它展开再点击对应的选项。比如driver.find_element(By.ID, city-select).click() driver.find_element(By.XPATH, //li[contains(text(), 上海)]).click()复选和单选一般也是直接 click 就行。但要注意复选如果已经选中再 click 会变成取消勾选。所以更稳妥的做法是先判断is_selected()根据业务意图决定要不要点击。我之前写过一段全选逻辑只看数量、不做状态判断结果第二次跑脚本就翻车了。弹窗也是自动化测试里的固定客。Selenium 处理的是原生 alert、confirm、prompt代码很简单alert driver.switch_to.alert print(alert.text) # 拿到弹窗文本 alert.accept() # 点击确定 alert.dismiss() # 点击取消需要提醒的是现在的页面弹窗大多数是 DOM 元素模拟的比如 div 弹层switch_to.alert根本接管不到有的确认动作甚至有两个按钮你要根据业务去选择点击哪个。所以在写脚本时先分清这是原生弹窗还是页面弹层再去选择处理方式。3.3 iframe 与多窗口切换最容易卡住的两个人iframe 是 Selenium 实战里非常磨人心态的东西。你明明看到页面上有那个元素find_element却始终报找不到多半就是元素藏在 iframe 里。处理方式很简单三步切进去、操作、切出来。# 按 id 或 name 切换 driver.switch_to.frame(mainFrame) # 按索引切换适用于页面上第几个 iframe driver.switch_to.frame(0) # 操作完成后回到主文档 driver.switch_to.default_content()嵌套 iframe 时需要一层一层切进去切回上层用driver.switch_to.parent_frame()。我见过有些同学切进一层 iframe 后忘了回主文档结果下一段代码找不到主页面元素排查了半天。多窗口切换同样常踩。点击某个链接新开一个标签页后WebDriver 默认还在原来的页面上下文不切换句柄的话你操作的是旧页面。代码长这样handles driver.window_handles # 新打开的标签页通常在列表末尾 driver.switch_to.window(handles[-1]) # 操作完再切回第一个窗口 driver.switch_to.window(handles[0])更靠谱的方式是按条件找窗口比如循环判断 url 或 title找到特定窗口再切换。因为window_handles的顺序在某些浏览器里不一定稳定纯粹靠索引有一定的随机风险。这个坑我在多标签业务里踩过不止一次。3.4 execute_script 与 ActionChains绕过交互限制的两把刀execute_script是 Selenium 里非常实用的逃生通道它的作用是在当前页面执行 JavaScript。很多“无法操作”的元素用 JS 一下就解决了。最常见的三个场景# 场景一元素被遮挡点击不到 driver.execute_script(arguments[0].click();, btn) # 场景二日期输入框 readonly禁止手动输入 driver.execute_script(arguments[0].removeAttribute(readonly), date_input) date_input.send_keys(2025-06-01) # 场景三滚动到页面底部触发懒加载 driver.execute_script(window.scrollTo(0, document.body.scrollHeight))不过我还是建议JS 强制点击只作为兜底方案。自动化测试的核心价值之一是回归真实用户的操作路径如果一个按钮在真实界面里被挡住了点不到那这本身就是一个值得上报的缺陷而不是用 JS 绕过就完事。ActionChains则用来处理需要连续动作的交互比如鼠标悬停、拖拽、右键菜单、长按。典型场景是电商网站的商品分类鼠标移到“手机数码”上会弹出子菜单再点里面的“手机壳”。from selenium.webdriver.common.action_chains import ActionChains menu driver.find_element(By.ID, category-menu) sub_item driver.find_element(By.XPATH, //a[contains(text(), 手机壳)]) ActionChains(driver).move_to_element(menu).click(sub_item).perform()perform()是 ActionChains 的触发动作忘记调用它前面的所有动作都不会执行。这个细节我一开始也经常漏。4. 等待机制三种等待的取舍与工程化落地4.1 三种等待方式的本质区别很多脚本的稳定性问题根源不在定位也不在操作而是页面加载节奏没控制好。Selenium 提供了三种等待机制我一个个说清楚。第一种是time.sleep()强制让脚本休眠指定秒数。它的问题很明显时间短了元素没出来时间长了白白浪费。如果你用习惯性 sleep 3 秒去等一个通常 1 秒就出现的元素整个脚本跑下来会慢得让人抓狂。而且测试环境网络一旦抖动sleep 不到位的脚本就开始飘红。所以现在我几乎不在正式代码里写整数 sleep最多在极少数纯异步渲染场景做一个保底。第二种是implicitly_wait()隐式等待。设置一次作用于整个 WebDriver 生命周期。它的机制是每次定位元素时如果元素没有立刻出现WebDriver 会轮询等待一段时间直到超时。driver.implicitly_wait(10) # 全局最多等10秒隐式等待的优点是省事缺点是它只能等“元素出现在 DOM 里”不能区分元素是否可见、是否可点击。比如一个元素已经在 DOM 里但被遮罩挡住find_element能返回对象但 click 还是会失败。第三种是WebDriverWait显式等待这是工程化脚本里最推荐的方式。它允许你设置明确的等待条件和超时时间能精准表达“我要等这个元素变成可点击状态”。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()这里建议显式等待和隐式等待不要混用太深。虽然现代 Selenium 版本里两者可以共存但混用时等待策略会互相叠加排查问题时会多一层干扰。我个人的做法是脚本里统一用显式等待把隐式等待只作为初始化时的兜底设置不依赖它解决业务等待。4.2 expected_conditions 常用条件的实战场景expected_conditions大家一般简写叫 EC它提供了十几个常用的等待条件。把它用熟了脚本稳定性会有一个明显提升。几个使用频率最高的条件条件写法作用典型使用场景presence_of_element_located元素出现在 DOM 中等待某个动态插入的节点visibility_of_element_located元素可见等待页面上的内容展示出来element_to_be_clickable元素可见且可点击点击按钮前的等待text_to_be_present_in_element元素的文本包含指定内容等待操作结果文案出现staleness_of元素已从 DOM 中移除等待旧列表被刷新以element_to_be_clickable为例它和visibility_of_element_located的区别在于一个元素可能是可见的但被半透明遮罩覆盖导致不可点击或者处于禁用状态。如果直接用 visibility 条件等待通过了但 click 还是报错。所以凡是下一步要点击的元素我统一用element_to_be_clickable。等待条件里的参数一定是一个元组而不是传 By 对象进去。我见过很多新手写成# 错误写法EC.element_to_be_clickable((By.ID, btn)) # 正确写法注意这里是一个两层括号的元组 EC.element_to_be_clickable((By.ID, btn))把元组写成By.ID直接放进去会抛异常提示你 expected_condition 需要的是 locator。这个括号细节很不起眼但几乎每个入行的人都会遇到一回。4.3 自己封装等待逻辑的思路判断一个业务真正就绪有时比“元素可点击”更复杂。比如登录后要等首页的某个模块渲染出来同时还要确保页面上没有 loading 遮罩。这时候就可以自定义等待条件。WebDriverWait.until()接受的可不只是 EC 里的现成条件它也可以接一个返回布尔值的函数。比如等页面 readyState 变成 completewait.until(lambda d: d.execute_script(return document.readyState) complete)甚至可以封装一个通用方法把“等待 loading 消失且目标元素可见”作为组合条件def wait_for_clickable(driver, locator, timeout10): wait WebDriverWait(driver, timeout) wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, .loading))) return wait.until(EC.element_to_be_clickable(locator))这样业务代码里就不用到处写 WebDriverWait 模板代码出错时只需要修这一处封装。维护成本低很多这也是项目里防止等待逻辑写乱的一个实用习惯。5. 文件上传下载从 send_keys 到系统级交互5.1 最省力的方案file 输入框直接 send_keys文件上传是自动化测试里绕不开的场景也是很多人一上来就想用自动化工具模拟操作系统文件选择框的误区。其实绝大多数网页的上传控件都是input[typefile]而 WebDriver 对这类控件有一个非常优雅的支持直接send_keys传文件绝对路径根本不需要打开系统对话框。upload_input driver.find_element(By.CSS_SELECTOR, input[typefile]) upload_input.send_keys(rD:\test_data\supplier_list.xlsx)这个方案之所以有效是因为浏览器出于安全考虑不允许脚本直接设置 file input 的 value但 Selenium 通过 WebDriver 协议可以突破这个限制效果等同于用户手动选择文件。几个实操细节路径用绝对路径不要用相对路径。脚本工作时的工作目录可能会变相对路径很容易失效。Windows 路径中的反斜杠要用原始字符串比如上面代码里的rD:\test_data\supplier_list.xlsx或者把路径里的反斜杠改成双反斜杠。支持多文件上传时多个路径之间用换行符拼接send_keys(path1 \n path2)。上传后不要立刻断言优先用显式等待等上传完成提示比如“文件上传成功”的 toast 文案出现。这个方法对 90% 的上传场景都够用而且不依赖操作系统类型跨 Windows/Linux/macOS 都稳定是文件上传的首选方案。5.2 非标准上传控件剪贴板加键盘事件的组合拳有些系统用自定义组件做上传页面上看不到input[typefile]只有一个大大的 div 写着“点击上传”。点击它才会唤起操作系统的文件选择框而 WebDriver 无法直接操作系统原生弹窗。这种场景下比较实用的方案是配合 pyperclip 和 pyautogui 模拟键盘操作。基本思路是点击上传按钮触发系统文件选择框用剪贴板把文件路径复制进去然后用快捷键把路径粘贴到文件名输入框最后按回车确认。import time import pyperclip import pyautogui driver.find_element(By.ID, upload-btn).click() time.sleep(1.5) # 等待系统弹窗完全出现 pyperclip.copy(rD:\test_data\hf_image.png) pyautogui.hotkey(ctrl, v) time.sleep(0.5) pyautogui.press(enter)这个方案的缺点是强依赖操作系统和弹窗界面Windows 下的行为跟 macOS 下不一定一致所以只作为备选。真正项目里我更倾向于推动开发把隐藏的input[typefile]暴露出来或者增加一个测试专用的上传接口让脚本直接调接口上传绕过 UI 操作。自动化不是越暴力越好能找到更稳定的路径才是更优解。5.3 下载文件的浏览器偏好设置文件下载虽然不属于“上传”范畴但文件导入导出测试经常成对出现。Selenium 默认会把文件下载到浏览器的默认下载目录这样做脚本不方便做断言因为下载位置不可控。正确做法是在浏览器启动参数里设置下载目录。以 Chrome 为例from selenium.webdriver.chrome.options import Options options Options() prefs { download.default_directory: rD:\downloads, download.prompt_for_download: False, safebrowsing.enabled: True, } options.add_experimental_option(prefs, prefs) driver webdriver.Chrome(optionsoptions)关键点说明download.default_directory指定的目录必须真实存在否则浏览器会忽略它。download.prompt_for_download设置为 False避免每次下载都弹出确认框。safebrowsing.enabled设置为 True是为了防止某些文件类型被安全策略拦截。下载后做断言时我习惯写一个简单的等待函数轮询目标文件夹里是否出现了指定文件名的文件。文件系统操作不能用 WebDriverWait要用普通的 while 循环加重试直到超时或文件存在。5.4 上传下载测试中的边界场景处理上传下载除了正常路径建议把边界场景也保留几条固定的用例。比如上传超 2GB 的大文件、上传空文件、上传不符合扩展名的文件、上传同名文件看是否覆盖。这些用例一方面能暴露系统的校验逻辑缺陷另一方面也能验证前端是否有合理的错误提示。这类测试脚本的特点是耗时较长建议把它们从日常 Smoke 集里抽出来放到独立的回归集里避免拖慢 CI 主流程。同时下载文件占用的磁盘空间需要及时清理不然频繁跑测试很容易把 CI 机器的磁盘占满。6. 高频异常与排查实录我踩过的那些坑6.1 四大高频异常速查表Selenium 脚本运行不稳定的很大一部分原因来自几个特定异常。我整理了一个速查表基本覆盖了日常 90% 的问题异常典型报错信息片段根因方向解决策略NoSuchElementExceptionUnable to locate element元素不存在、未加载、在 iframe 里加显式等待、检查 iframe、换定位策略ElementNotInteractableExceptionelement not interactable元素存在但不可交互比如隐藏、禁用、遮挡等待可见、滚动到可视区、必要时 JS 处理StaleElementReferenceExceptionelement is not attached to the page document元素被页面刷新或列表重建旧引用失效重新获取元素对象不要在跳转后复用旧对象TimeoutExceptionExpected condition failed等待条件超时检查条件是否写错、元素是否在 iframe 或新窗口里以StaleElementReferenceException为例。之前我写一个批量操作脚本逻辑是循环遍历一组任务点击“处理”弹窗操作后点“保存”返回列表再处理下一个。第一次循环很顺利第二次报错就是它。原因很简单处理完一个任务后页面列表刷新了之前缓存的旧元素对象已经脱离了 DOM再用它点击当然报错。解决办法是在每次循环里重新获取元素而不是缓存整个元素列表。for index in range(task_count): # 每次都重新定位避免元素过期 task_row driver.find_elements(By.CSS_SELECTOR, .task-row)[index] task_row.find_element(By.CSS_SELECTOR, .handle-btn).click() # ...后续操作这一个改动直接让原本 60% 通过率的脚本变成了接近 100%。6.2 headless 模式下的怪异表现无头浏览器是 CI 环境里很常见的做法。Chrome 的 headless 模式下脚本跑得快、不依赖图形界面但它也有一些怪异表现我挑两个比较典型的说说。第一个是部分页面在 headless 下不会渲染某些 DOM 节点或者 CSS 布局计算有差异。比如有些图表组件检测到无头环境直接不初始化导致页面里根本没有 canvas 元素有些懒加载图片在无头模式下不触发加载导致元素一直处于未呈现状态。遇到这种情况首先确认页面是不是对 user-agent 做了判断。你可以通过add_argument(--user-agent...)手动设置一个真实浏览器的 UA也可以在调试时先用有头模式确认页面行为再切到 headless 对比。第二个是 headless 模式下窗体尺寸默认偏小可能影响到某些依赖于可见区域的元素展示。建议显式设置--window-size1920,1080让视口更接近真实用户环境。我的实践原则是本地调试一律用有头模式看得到界面才好定位问题提交到 CI 才切 headless。不要一开始就为追求“跑得快”而全程无头调试成本会高出很多。6.3 截图、日志与页面源码的排查组合遇到脚本失败最怕的就是只有一行异常栈完全不知道当时页面上发生了什么。所以我在封装框架时一定会加一个失败现场捕获的公共逻辑异常发生时自动截图、保存页面 HTML 源码、打印当前 URL 和窗口标题。import os import time def capture_failure(driver, tag): ts time.strftime(%Y%m%d_%H%M%S) driver.get_screenshot_as_file(ffails_{tag}_{ts}.png) with open(fpage_{tag}_{ts}.html, w, encodingutf-8) as f: f.write(driver.page_source)这段逻辑看似简单但非常救命。很多时候你看着截图就能立刻发现是弹窗还开着、页面跳错了、还是数据没加载出来排查时间从几小时缩短到几分钟。另一个小技巧是在脚本的关键步骤里顺手把当前的 URL 和 title 打印出来。比如点击登录按钮之后、断言页面跳转之前打印一下当前 URL如果 URL 没变说明登录请求没发出去如果 URL 变了但 title 不对可能是导航到了异常页面。这种“先看状态、再查代码”的习惯能帮你快速锁定问题层。脚本的日志输出也要有点层次不要一行 print 打天下。我通常用 logging 模块按 info 记录操作步骤、按 warning 记录非致命异常、按 error 记录断言失败。这样一旦出问题翻日志就能还原整个执行路径。最后再分享一点我自己的体会这个系列写到这里我想说点框架之外的东西。Selenium 常用函数本身不算难背下来也就一页纸的事。真正决定脚本质量的是两件事一是对 DOM 结构的理解深度二是错误处理的兜底意识。同样的一个功能有人写出来的脚本跑一个月不黄有人跑三天就各种报错差异往往不是 API 熟不熟而是有没有在每个可能出错的节点留下“后手”。我个人的习惯是页面元素尽量用 CSS 或相对 XPath 定位少用绝对路径需要等元素就用显式等待不靠 sleep 硬扛所有超过一分钟的长脚本都会在关键节点留截图每次定位到的元素如果后面要重复使用先想清楚页面会不会刷新。这套习惯不是一天形成的而是在一次次的 CI 失败中磨出来的。这篇的内容覆盖了从元素定位到文件上传的主要场景下一篇我打算聊聊 WebDriver 的分布式运行和 Selenium Grid 配置把单机脚本扩展到多浏览器并行执行的工程细节。如果你在日常使用 Selenium 时也遇到过什么奇特的坑欢迎在评论里一起交流。