关于Selenium里等待的理解

2020-11-25  本文已影响0人  YAN_随笔录

关于Selenium里等待的理解

首先使用selenium做自动化测试时有时需要等待元素的加载完成,常用的等待方式有三种,一种是强制等待,一种是隐式等待,还有一种是显式等待。
官网对于等待的章节地址:https://www.selenium.dev/documentation/zh-cn/webdriver/waits/
里面也说到了有三种方式,隐式等待,显式等待和流畅等待。

强制等待

强制等待可以说是最简单最粗暴的一种方式,实现方式如下

import time
# 程序休眠5秒钟
time.sleep(5);

这个是Pyhton程序自带的包提供的方法,意思是执行到这个time.sleep(5)的时候,程序就睡觉,睡一会再继续。
这种方法在其他语言中也都有实现,比如java中使用 Thread.sleep(5000) 来使当前线程休眠5秒钟后继续执行。
这个方法其实是和 selenium 或者干其他任何事都是不相关的,你无论程序是在干嘛,只要执行到sleep那就睡觉,睡醒了继续向下执行。

隐式等待

隐式等待先看实现方式

from selenium import webdriver

# 建立一个webdriver
driver = webdriver.Chrome()
# 设置隐式等待时间为30秒
driver.implicitly_wait(30)

我一开始以为那个隐式等待是,执行到那里的时候等待元素加载完成,但其实我纳闷的是,这方法并没有指定是什么元素加载完成。但是理解成是等待页面所有元素加载完成,那也有点不太科学,就是页面的元素到底加载完成不完成这个可能selenium真的无法知道,就连浏览器可能也无法使用一个标准来判定该页面是否加载完成。
我现在基本理解了,可以认为那个 driver.implicitly_wait(30) 方法只是一个设置方法。它的意思仅仅只是,为 driver这个对象设置一个 隐式等待 的时间,仅仅只是给这个对象设置而已。
然后这个时间什么时候会用呢?当我们使用 selenium 中的 webDriver 来与浏览器通信的时候,比如我们使用 driver.find_element(By.Id, "someId") 这个方法的时候。其实这个方法的本质就是,我们使用 selenium 向浏览器发了一条指令,指令的意思是我要找一个Id为'someId'的元素。
这个通信的过程大致是这样的, webDriver 发送指令给 driver ,然后 driver 调用浏览器如果找到了就告诉我,找不到的话,那么 隐式等待 的时间最大的时候,就超时返回异常,这个最大时间就是 driver.implicitly_wait(30) 这里设置的时间。可以理解为 Driver 会和浏览器建立一个 Session ,这个隐式等待的设置时间就是会话的最大生命周期。关系图如下(官网的图):

image.png

这个隐式等待时间的设置的作用范围是整个driver,就是只要设置一次,在使用这个driver的任何 find_element 操作生效,当然可能不仅限于 find_element 的操作,可能还有别的操作也会有。
所以这个时间叫 隐式等待 ,意思是说不是我每次都要指定的,默认每次都会有这个等待的操作,无论你设置不设置默认都有,不可见,所以说可以理解为是隐藏的,即使不设置,也有默认值。

显式等待

先看显式等待的用法

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait

# 建立一个webdriver
driver = webdriver.Chrome()

login_form = WebDriverWait(driver, timeout=5).until(
            lambda d: d.find_element(By.ID, "TANGRAM__PSP_4__form"))

WebDriverWait类的构造方法,


image.png

这个 WebDriverWait 类的构造方法可以指定 超时时间,轮询频率,还有需要忽略的异常,后面两者可以不设置。
根据这个方法可以大概理解,我要等待 timeout 秒,直到 until (这里的表达式返回一个True) ,或者可以把这个 until 换成 until_not ,后者是返回 False ,轮询那个表达式,直到条件成立,返回,否则,抛出异常超时。
两个方法的源码:

image.png

所以,官方文档中说的两者混用的意思就是,如果使用 until() 里面的表达式 会触发那个隐式等待的时间的话,那就是混用了。示例代码中就是两个等待都用到了。显式等待里面0.5秒轮询一次表达式,表达式又触发隐式等待时间,然后如果 表达式一直不成立,那这个超时时间就会比较长。

显式等待和隐式等待的混合使用的理解

官网的解释是这样的:

警告: 不要混合使用隐式和显式等待。这样做会导致不可预测的等待时间。例如,将隐式等待设置为10秒,将显式等待设置为15秒,可能会导致在20秒后发生超时。

我一开始读到这一段的时候很疑惑为啥是20秒,没有理解,但是后来结合源码看了一下,恍然大悟。
以官方说明为例,假设有这样一段代码:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

# 建立一个webdriver
driver = webdriver.Chrome()
# 设置隐式等待时间为10秒
driver.implicitly_wait(10)
driver.get("https://baidu.com")
# 查找一个ID为SomeId的元素,等待最长时间为15秒,1秒轮询一次
some_ele = WebDriverWait(driver, timeout=15, poll_frequency=1).until(
            lambda d: d.find_element(By.ID, "SomeId"))

假如这个目标元素根本不存在,那么该段代码中多少秒会抛出异常呢?
答案正如官网所说是:20秒,确切的说应该是21秒,有两次轮询间隔,轮询间隔时间不设置默认是0.5秒。那么为什么是20秒不是15秒呢?不应该是已最大的等待时间为准吗?
接下来分析一下wait类的源码,先直接看until方法:

    def until(self, method, message=''):
        """Calls the method provided with the driver as an argument until the \
        return value is not False."""
        # 源码注释的意思是“调用驱动程序提供的方法作为参数,直到返回值不是False。”
        screen = None
        stacktrace = None
        # 首先定义一个最大时间=当前时间+用户设置的超时时间,我们设置是15秒。
        end_time = time.time() + self._timeout
        # 开启一个循环
        while True:
            try:
                # 执行用户传入的匿名函数,我们这里指的就是查找元素,
                # d.find_element(By.ID, "SomeId")
                # 这个方法会触发隐式等待,我们设置的隐式等待时间最长是10秒,
                # 因为我们查找的元素不存在,所以该方法会在此处阻塞10秒,
                # 10秒后抛出异常NoSuchElementException,
                # 如果设置了NoSuchElementException为_ignored_exceptions,
                # 就会跳到except中捕获截图和堆栈
                value = method(self._driver)
                # 方法如果返回的值不为None,或False,直接返回这个值。
                # 这个判断要看一下python对于其他数据类型和bool之间的转换
                # 一般只要是非空,都为True
                if value:
                    return value
            except self._ignored_exceptions as exc:
                screen = getattr(exc, 'screen', None)
                stacktrace = getattr(exc, 'stacktrace', None)
            # 程序休眠 _poll 秒,这里的_poll是我们构建Wait类时传入的轮询间隔,我们设置为1秒。
            time.sleep(self._poll)
            # 判断当前时间是否已超出 最大等待时间,即上面定义的end_time,超出则跳出循环
            if time.time() > end_time:
                break
        # 跳出循环则抛出异常,超时
        raise TimeoutException(message, screen, stacktrace)

从代码中可以看出,如果这个元素根本不存在,那最大时间应该是 10秒+1秒+10秒+1秒=22秒。第一个10秒指的是隐式等待时间设置的超时,第一个1秒是我们自己设置的轮询间隔,然后这时候并没有超出显示等待的最大时间15秒,所以进入第二次循环,依旧是一个10秒的隐式等待,一个1秒的轮询间隔,然后这是已经超出显示等待最大时间15秒,所以跳出循环,抛出超时异常。这里我应该使用脚本做个试验来验证自己的猜想。。
好,来写个脚本验证一下:

import time
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

# 建立一个webdriver
driver = webdriver.Chrome()
# 设置隐式等待时间为10秒
driver.implicitly_wait(10)
# 打开百度
driver.get("https://baidu.com")
# 记录开始时间
start = time.time()
print("开始查找一个不存在的元素,开始时间为" + str(start))
try:
    # 查找一个ID为SomeId的元素,等待最长时间为15秒,1秒轮询一次
    some_ele = WebDriverWait(driver, timeout=15, poll_frequency=1).until(
        lambda d: d.find_element(By.ID, "SomeId"))
except TimeoutException as exc:
    # 捕获 TimeoutException 异常
    print("查找元素超时。。。")
finally:
    # 记录结束时间,
    end = time.time()
    # 打印耗时
    print("查找结束,结束时间为:" + str(end) + ",耗时:" + str(end - start))
    # 关闭Driver
    driver.close()

以上代码中我们打开百度来查找一个不存在的元素,看执行结果:

开始查找一个不存在的元素,开始时间为1606379817.432131
查找元素超时。。。
查找结束,结束时间为:1606379839.466272,耗时:22.034141063690186

控制台输出结果证实了我上面的猜想,假如我们这时将轮询间隔时间改为2秒呢?
那最长等待时间可能就会变成24秒。
由此可见,这个两者混用后的 最大等待时间和三个因素有关,显示等待时间、隐式等待时间和显示轮询间隔
最大等待时间的计算公式应该是这样的:

# 假设 x=隐式等待时间, y=轮询间隔时间, z=显式等待时间, r=最长等待时间
if x + y >= z:
    r = x + y
else:
    r = (x + y) * (z / (int) (x + y) + 1) or r = (x + y) * z / (int) (x + y) + x + y

这个公式并不能直接简写成 x + y + z 因为在程序中 操作是会省略余数的。
下面说明一下,那个 NoSuchElementException 为啥会被捕获。
贴一下WebDriverWait类的源码截图:

image.png

源码的注释中也可以看到,如果不指定 ignored_exceptions 这个参数,默认就会只包含 NoSuchElementException 这个异常,所以这个异常在等待中是默认会被忽略的。从源码第 23 和 50 行中可以看出。
其实显式等待可以简单的理解成是一个简单的封装,其中也用到了 time.sleep() 方法。

流畅等待

流畅等待 在官网的介绍中与显式等待并无区别,只是更多的添加了轮询间隔与可忽略异常参数的使用。
JavaSelenium 实现中显示等待和流畅等待是使用了两个不同的类,后者是使用了一个叫 FluentWait 的子类,但在 Python 实现中并没有区别。只是由于语言的特性不同,在 Java 中将两中使用定义为了两个不同的子类。
这里就不在继续介绍这种方式。

总结:

那么最终我们到底应该使用哪种方式呢?可能需要结合实际情况考虑,比如我们需要控制操控页面的频率时,故意停顿时需要使用到 强制等待 的方式 time.sleep() ;比如在检索元素时防止元素未加载完成而抛出 NoSuchElementException ,就需要使用到显式等待和隐式等待。
显式等待 相对更加灵活一些,可以指定条件表达式,超时时间,轮询频率和要忽略的异常;但是 隐式等待 作用范围更广,一个 Driver 的生命周期只需要设置一次就可以了。
通常来说来给 Driver 设置一个相对合适的 隐式等待 时间就可以满足大多数需求了,除非在需求中可能并非所有的元素需要等待的时间都那么的相对平均,就需要给一些特殊的元素使用显式等待的方式来做。
两种等待方式混用其实 并非是禁用 的,只是在使用的时候需要考虑到最大的等待时间可能会超出预期,这个就需要注意。但是一般情况下只要元素是确定存在的并不会超出设置的超时时间,除非网页加载过慢,或者网络不佳的情况。

上一篇下一篇

猜你喜欢

热点阅读