ARTICLE DETAIL

资讯详情

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

Selenium三种等待方式全解析:告别NoSuchElementException

Selenium三种等待方式全解析:告别NoSuchElementException 写自动化脚本的人谁没被NoSuchElementException砸过几下脑袋。我最早入行的时候写出来的脚本就跟跟脆玻璃一样动不动就碎。后来才明白不是定位写错了也不是元素真的不存在而是selenium跑得比网页快太多程序去敲门的时候人家门还没开。网上关于selenium等待的帖子不少但大多分散得厉害。这标题一出来我就知道是刚被超时、找不到元素折腾过的新人在找救命的整理。selenium里有三种等待方式强制等待、隐式等待、显式等待也叫sleep等待、implicitly_wait和WebDriverWait。今天就把这三种方式一次讲透连原理带代码带踩坑记录给后来人铺条平坦一点的路。1. 为什么要专门聊selenium的等待机制1.1 看不见的竞态条件才是脚本脆弱的根源很多初学者以为find_element失败就是代码写错了其实大概率是页面元素还没渲染完成。现代网页早就不靠纯HTML一次性返回了大量内容依赖异步请求、JavaScript动态渲染。比如打开一个订单列表页HTML骨架先到表格里的数据还在等接口返回这段时间里你去抓某个td元素就像伸手去摸一个还没端上桌的菜不空手才怪。这个场景在计算机术语叫竞态条件脚本和页面在抢时间。网络慢、接口慢、图片加载慢都会导致元素出现的时机飘忽不定。等待机制就是用来消除这种随机性的让脚本在元素真正可用的时候才动手。1.2 等待写不好测试结果等于废纸等待写得太短脚本三天两头报错CI一跑红一大片。等待写得太长每次执行慢得像放PPT本来10分钟的回归测试能拖到40分钟。更扎心的是有人为了省事到处甩time.sleep(10)结果一换网络环境脚本要么超时要么空等白白浪费时间定位根本不存在的Bug。selenium作为自动化测试框架如果连“等”这件事都没处理好那整套case的可信度就要打问号。2. 三种等待方式的底层逻辑与正确姿势2.1 强制等待最简单也最暴力的time.sleep先来看这种最简单、新手最常用的方式import time time.sleep(5) driver.find_element(By.ID, submit).click()它的含义非常直白不管页面好了没有死等5秒再往下走。这个方案的核心逻辑就是“用固定时间换稳定”在脚本里属于一套组合拳里最没有技术含量但最立竿见影的一招。优点是好懂、不依赖任何其他条件适合调试期临时看看逻辑对不对。但缺点也一样明显时间给少了元素还没出给多了纯浪费时间。全局一等就是5秒一条case多几十个等待点总时长直接爆炸。页面早就加载完了还得干坐着发呆。实操心得我自己的习惯是只在调试阶段用time.sleep一旦确认逻辑通顺立刻替换成下面两种。把sleep留在正式代码里就像穿着拖鞋去跑马拉松能走但是走不远。2.2 隐式等待全局兜底但不够聪明的implicitly_wait隐式等待的用法很简单在创建driver之后设一次就行from selenium import webdriver driver webdriver.Chrome() driver.implicitly_wait(10)它的工作方式是设置一个全局最大等待时间。每次执行find_element或find_elements时如果元素没有立刻出现driver会主动轮询等待直到元素出现或者超过10秒上限然后抛出异常。用生活里的场景类比就是你给前台说好了“等客人最长10分钟”10分钟之内客人来了就直接接待10分钟不来就告诉对方“人没等到”。好处是不需要给每个元素单独配置等待一次设置全生命周期生效。但它有一个很大的局限它只关心元素存不存在关心不了元素可不可交互。比如一个按钮在DOM里待着但被遮罩层盖着或者还在disabled状态find_element照样能找到。这种情况隐式等待就失灵了你点下去照样报ElementClickInterceptedException。另一个隐藏很深的坑隐式等待设置不能无限叠加。有些代码写着写着在多个地方重复设置implicitly_wait导致等待时间被覆盖或者在脚本中途改小排查起来特别费劲。所以这个值最好固定写在一个地方别到处撒。2.3 显式等待最推荐的WebDriverWait配Expected Conditions第三种才是今天的主角——显式等待。写法上有等待对象条件超时时间三个要素from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By element WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submit)) ) element.click()这里WebDriverWait(driver, 10)创建一个最多等10秒的等待器until里面的EC.element_to_be_clickable((By.ID, submit))指定“按钮可点击”才算等到。在10秒之内selenium会默认每0.5秒检查一次条件满足就立即返回元素超时才抛TimeoutException。它的本质是轮询条件判断上一个等待是“先睡起来再说”这个是“睡的时候还竖着耳朵听动静”。优势就非常明显了对动态页面的掌控力最强可以精确表达“等这个元素能点击”“等文本变成某个值”“等元素消失”等各种复杂场景。而且只要条件一满足立刻往下走不会多浪费一毫秒。经验补充EC类里有十多种现成条件常用的有这些条件写法用途presence_of_element_located元素出现在DOM里visibility_of_element_located元素可见element_to_be_clickable元素可点击text_to_be_present_in_element元素文本包含指定文字staleness_of元素脱离DOM常用来判断页面刷新实际写case时我90%的场景都用element_to_be_clickable和visibility_of_element_located前者应对按钮点击后者应对页面数据加载完成。3. 实操过程三种等待混用的正确比例3.1 核心原则能用显式等待就不用隐式等待能别sleep就别sleep从架构上看合适的等待策略应该是分层组合的。基础的页面跳转用一层隐式等待做兜底关键的交互节点用显式等待精准控制time.sleep只放在极少数的异步渲染场景里做补充。三者互相配合既不慢也不脆。实战里最容易跑通的组合是启动时设置一次implicitly_wait(5)兜底然后在关键操作上全部用WebDriverWait。为什么是5秒因为隐式等待设太大会拖慢整体失败反馈速度设太小等于没有5秒是一个折中的默认值具体根据业务调整。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(5) wait WebDriverWait(driver, 10) # 登录页加载等用户名输入框可输入 username wait.until(EC.visibility_of_element_located((By.ID, username))) username.send_keys(tester01) # 点击登录后等首页某个核心模块出现 wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, .user-avatar))) driver.find_element(By.CSS_SELECTOR, .user-avatar).click()这段代码把等待重点放在了登录和数据加载这两个最容易出问题的环节上。username等待的是可见性用户头像等待的是可点击性全是交互真正发生前的关键节点。3.2 显式等待的完整技术拆解到底等的是什么WebDriverWait的核心结构其实是until方法里传的那个条件对象。selenium封装好的EC条件本质上都是一个实现了__call__方法的类。当WebDriverWait轮询时它反复调用这个对象的__call__方法传入driver对象方法内部自己做判断返回True或者元素本身。比如element_to_be_clickable的一个简化逻辑大体是先调用presence_of_element_located确认元素存在于DOM。接着判断元素is_displayed()和is_enabled()是否都为True。两个都成立返回元素对象否则返回False继续轮询直到超时抛出异常。理解了这一点你就能明白为什么显式等待能等“可点击”而隐式等待做不到了本质上是因为element_to_be_clickable把判断逻辑封装进了轮询过程而隐式等待只是driver在查找元素时多了几次重试而已。3.3 自定义等待条件当内置条件不够用的时候内置EC虽然完善但总有边缘场景。比如你需要等一个元素的某个CSS类被移除或者等一个元素的属性值变成特定内容这时候可以自己写个条件函数from selenium.webdriver.support.ui import WebDriverWait def my_condition(driver): element driver.find_element(By.ID, status) return loading not in element.get_attribute(class) WebDriverWait(driver, 10).until(my_condition)自定义条件函数接收driver参数满足需求时返回True不满足返回False。selenium会按轮询间隔反复调用这个函数直到返回True或超时。这也是显式等待最强大的地方——等待逻辑完全由你掌控。实操中我还遇到过一种场景点击某个按钮后页面会出现一个loading遮罩层等遮罩层消失才算加载完成。内置的EC.invisibility_of_element_located可以直接实现wait.until(EC.invisibility_of_element_located((By.ID, loading-mask)))这个写法非常值得记下来比sleep(3)赌运气不知道高到哪里去了。3.4 轮询间隔的调优poll_frequency不是摆设WebDriverWait的构造参数里还有一个poll_frequency默认0.5秒。在默认配置下selenium每500毫秒检查一次条件。这个参数多数时候不用改但特殊场景值得留意。比如你等待的是某个经过长时间计算的接口返回这个接口可能在5秒内根本不会有结果轮询再快也没意义。而有的内部系统响应极快元素从出现到消失只有几百毫秒默认0.5秒轮询可能错过高峰期。可以将poll_frequency0.1缩短到100毫秒更容易捕捉到短时出现的元素。wait WebDriverWait(driver, 10, poll_frequency0.1)不过也要提示一下轮询越频繁脚本对浏览器的调用就越多性能上会有一点点损耗在跑大批量case时会更明显。所以这个参数按需调整别全局整一个0.1。4. 常见问题排查一堆人说等了但还在报错4.1 用time.sleep太多导致脚本执行时间爆炸最典型的问题是脚本能跑通但是慢得离谱。一百条case每条平均多出十几秒一圈跑下来就是几十分钟白扔。你以为是网络问题实际上是代码里布满了几十处sleep(3)。排查方式非常粗暴全局搜索time.sleep看看总共加起来多少秒。大概率你会发现一条case光睡就睡了20秒。解决方案就是把大多数sleep替换成显式等待留下少数真正必要的。4.2 设置了implicitly_wait但没生效这种情况多半是设置顺序不对。如果你先执行了find_element再去调用driver.implicitly_wait(10)那前一次查找是不会带等待效果的。设置必须放在创建driver之后、任何查找操作之前。还有一种情况是脚本里有多个driver实例你只在A driver上设置了隐式等待实际在操作B driver自然没效果。检查的时候把driver变量名跟着扫一遍就行。4.3 显式等待超时但元素肉眼可见这个坑很迷惑。页面里明明能看到元素WebDriverWait却报TimeoutException。优先级最高的问题通常是元素在iframe里。WebDriver在默认情况下只能访问当前文档流里的元素。如果你要操作iframe里的内容先得用driver.switch_to.frame()切进去否则无论怎么等selenium都“看不见”那个元素。另一种原因是元素存在DOM里但这个元素可能被一个透明遮罩层盖住了visibility_of_element_located其实只关注CSSvisibility和display属性。肉眼可见可能是另一个元素可见目标元素本身可能不可见或不可交互换个EC.element_to_be_clickable或者检查页面的遮罩层就能解决。4.4 隐式等待和显式等待混用产生的坑到这一步要说一个非常容易被忽略的细节隐式等待和显式等待设置在一起时隐式等待对元素查找的全局影响依然存在。Selenium官方文档其实有提醒这一点两者一起用时等待时间并不是简单的相加但很多人还是会遇到这样一个问题driver设置了implicitly_wait(10)WebDriverWait设置了超时5秒理论上5秒就报错退出。但现场你会发现有的版本组合下等待时长明显超过5秒有时能达到十几秒。原因出在隐式等待会影响到显式等待内部的元素查询过程。比如element_to_be_clickable内部可能先调用了find_element而find_element本身受隐式等待影响一卡就是10秒结果整体超时被拉长。目前官方更推荐的方案是尽量别混用或者在显式等待的关键分支前临时把隐式等待设为0有需要再恢复。Selenium 4.x版本里这个现象有所改善但版本差异导致的行为变化还是值得留意。我在自己框架里的做法是全局只用显式等待不设置隐式等待错误信息更干净行为更好预测。4.5 排查等待问题的通用思路你遇到了元素偶尔能找到、偶尔找不到的情况按以下顺序排查优先确认等待的是不是最外层页面内容有没有iframe。用driver.page_source或者浏览器F12看看元素真实状态是没出现还是出现但不可用。把异常消息里的时间戳和页面加载时间对一下判断是等待不够还是等待条件选错。如果条件没问题把time.sleep临时加上做对照试验确认真的是时序问题。稳定复现后再把sleep改成对应的显式等待条件。这套流程我用了很久宁可多花十分钟看一眼页面源码也别靠“猜”去改等待时间。5. 不同场景下的等待选择速查根据实际业务场景可以按下面这个思路做决策。页面跳转后用隐式等待兜底没问题但交互前的元素必须用显式等待。举几个我经常遇到的情况点击按钮后弹出新窗口用wait.until(EC.number_of_windows_to_be(2))等窗口数量变化比sleep靠谱得多。加载更多按钮触发列表滚动先等element_to_be_clickable再点击点击后等列表项数量增多wait.until(lambda d: len(d.find_elements(By.CLASS_NAME, item)) 10)接口返回后页面出现Toast提示等文本出现用text_to_be_present_in_element精准匹配。这三种情况用sleep写很容易出现“时好时坏”换成显式等待之后基本一次稳定。另外要说一下不要过度依赖等待来处理脚本不稳定。等待机制解决的是时序问题解决不了定位冲突、数据错误这些根因。如果是脚本本身有根本性的问题靠堆等待时间只会把问题掩盖得更深等真正上线跑批的时候一样会爆发。6. 踩过这么多次坑我沉淀下来的写法6.1 我个人的等待统一封装方案项目里我现在通常封装一层工具方法避免每个page object里重复写一大串WebDriverWaitfrom selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class WaitUtils: def __init__(self, driver, timeout10): self.wait WebDriverWait(driver, timeout) def clickable(self, locator): return self.wait.until(EC.element_to_be_clickable(locator)) def visible(self, locator): return self.wait.until(EC.visibility_of_element_located(locator)) def invisible(self, locator): return self.wait.until(EC.invisibility_of_element_located(locator))这样在page对象里用起来就一行WaitUtils(driver).clickable((By.ID, submit)).click()可读性好很多后续如果要调整全局等待时间只需要在WaitUtils类里改一个值。6.2 最后一个小技巧超时信息里要带上下文默认的TimeoutException只告诉你哪个元素超时了不会告诉你当时页面是什么状态。我习惯在捕获异常时把当前页面标题和一部分page_source片段打出来这样能快速判断是页面崩了还是元素真的慢try: wait.until(EC.element_to_be_clickable((By.ID, submit))) except Exception: print(页面标题:, driver.title) print(当前URL:, driver.current_url) raise这种调试习惯在排查问题上非常实用。不要嫌多写几行代码麻烦线上排障时这几行信息能省下大半天的抓头时间。等待这件事做好了是自动化的地基做不好就是背锅侠的温床。三种等待方式各有各的适用场景真正吃透了就会发现那些“偶现失败”的case八成都是等待策略出了问题。网上说selenium好入门但能把等这件事做精细的人才算真正入了门。
返回列表