關(guān)于Selenium里等待的理解
首先使用selenium做自動(dòng)化測(cè)試時(shí)有時(shí)需要等待元素的加載完成晨炕,常用的等待方式有三種蒸播,一種是強(qiáng)制等待匿醒,一種是隱式等待场航,還有一種是顯式等待。
官網(wǎng)對(duì)于等待的章節(jié)地址:https://www.selenium.dev/documentation/zh-cn/webdriver/waits/
里面也說(shuō)到了有三種方式廉羔,隱式等待溉痢,顯式等待和流暢等待。
強(qiáng)制等待
強(qiáng)制等待可以說(shuō)是最簡(jiǎn)單最粗暴的一種方式憋他,實(shí)現(xiàn)方式如下
import time
# 程序休眠5秒鐘
time.sleep(5);
這個(gè)是Pyhton程序自帶的包提供的方法适室,意思是執(zhí)行到這個(gè)time.sleep(5)的時(shí)候,程序就睡覺(jué)举瑰,睡一會(huì)再繼續(xù)捣辆。
這種方法在其他語(yǔ)言中也都有實(shí)現(xiàn),比如java中使用 Thread.sleep(5000)
來(lái)使當(dāng)前線程休眠5秒鐘后繼續(xù)執(zhí)行此迅。
這個(gè)方法其實(shí)是和 selenium
或者干其他任何事都是不相關(guān)的汽畴,你無(wú)論程序是在干嘛,只要執(zhí)行到sleep那就睡覺(jué)耸序,睡醒了繼續(xù)向下執(zhí)行忍些。
隱式等待
隱式等待先看實(shí)現(xiàn)方式
from selenium import webdriver
# 建立一個(gè)webdriver
driver = webdriver.Chrome()
# 設(shè)置隱式等待時(shí)間為30秒
driver.implicitly_wait(30)
我一開(kāi)始以為那個(gè)隱式等待是,執(zhí)行到那里的時(shí)候等待元素加載完成坎怪,但其實(shí)我納悶的是罢坝,這方法并沒(méi)有指定是什么元素加載完成。但是理解成是等待頁(yè)面所有元素加載完成搅窿,那也有點(diǎn)不太科學(xué)嘁酿,就是頁(yè)面的元素到底加載完成不完成這個(gè)可能selenium真的無(wú)法知道,就連瀏覽器可能也無(wú)法使用一個(gè)標(biāo)準(zhǔn)來(lái)判定該頁(yè)面是否加載完成男应。
我現(xiàn)在基本理解了闹司,可以認(rèn)為那個(gè) driver.implicitly_wait(30)
方法只是一個(gè)設(shè)置方法。它的意思僅僅只是沐飘,為 driver這個(gè)對(duì)象設(shè)置一個(gè) 隱式等待
的時(shí)間游桩,僅僅只是給這個(gè)對(duì)象設(shè)置而已。
然后這個(gè)時(shí)間什么時(shí)候會(huì)用呢耐朴?當(dāng)我們使用 selenium
中的 webDriver
來(lái)與瀏覽器通信的時(shí)候借卧,比如我們使用 driver.find_element(By.Id, "someId")
這個(gè)方法的時(shí)候。其實(shí)這個(gè)方法的本質(zhì)就是筛峭,我們使用 selenium
向?yàn)g覽器發(fā)了一條指令铐刘,指令的意思是我要找一個(gè)Id為'someId'的元素。
這個(gè)通信的過(guò)程大致是這樣的蜒滩, webDriver
發(fā)送指令給 driver
滨达,然后 driver
調(diào)用瀏覽器如果找到了就告訴我奶稠,找不到的話,那么 隱式等待
的時(shí)間最大的時(shí)候捡遍,就超時(shí)返回異常锌订,這個(gè)最大時(shí)間就是 driver.implicitly_wait(30)
這里設(shè)置的時(shí)間』辏可以理解為 Driver
會(huì)和瀏覽器建立一個(gè) Session
辆飘,這個(gè)隱式等待的設(shè)置時(shí)間就是會(huì)話的最大生命周期。關(guān)系圖如下(官網(wǎng)的圖):
這個(gè)隱式等待時(shí)間的設(shè)置的作用范圍是整個(gè)driver谓传,就是只要設(shè)置一次蜈项,在使用這個(gè)driver的任何 find_element
操作生效,當(dāng)然可能不僅限于 find_element
的操作续挟,可能還有別的操作也會(huì)有紧卒。
所以這個(gè)時(shí)間叫 隱式等待
,意思是說(shuō)不是我每次都要指定的诗祸,默認(rèn)每次都會(huì)有這個(gè)等待的操作跑芳,無(wú)論你設(shè)置不設(shè)置默認(rèn)都有,不可見(jiàn)直颅,所以說(shuō)可以理解為是隱藏的博个,即使不設(shè)置,也有默認(rèn)值功偿。
顯式等待
先看顯式等待的用法
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
# 建立一個(gè)webdriver
driver = webdriver.Chrome()
login_form = WebDriverWait(driver, timeout=5).until(
lambda d: d.find_element(By.ID, "TANGRAM__PSP_4__form"))
WebDriverWait類(lèi)的構(gòu)造方法盆佣,
這個(gè) WebDriverWait
類(lèi)的構(gòu)造方法可以指定 超時(shí)時(shí)間,輪詢(xún)頻率械荷,還有需要忽略的異常共耍,后面兩者可以不設(shè)置。
根據(jù)這個(gè)方法可以大概理解养葵,我要等待 timeout
秒征堪,直到 until (這里的表達(dá)式返回一個(gè)True)
,或者可以把這個(gè) until
換成 until_not
关拒,后者是返回 False
,輪詢(xún)那個(gè)表達(dá)式,直到條件成立庸娱,返回着绊,否則,拋出異常超時(shí)熟尉。
兩個(gè)方法的源碼:
所以归露,官方文檔中說(shuō)的兩者混用的意思就是,如果使用 until()
里面的表達(dá)式 會(huì)觸發(fā)那個(gè)隱式等待的時(shí)間的話斤儿,那就是混用了剧包。示例代碼中就是兩個(gè)等待都用到了恐锦。顯式等待里面0.5秒輪詢(xún)一次表達(dá)式,表達(dá)式又觸發(fā)隱式等待時(shí)間疆液,然后如果 表達(dá)式一直不成立一铅,那這個(gè)超時(shí)時(shí)間就會(huì)比較長(zhǎng)。
顯式等待和隱式等待的混合使用的理解
官網(wǎng)的解釋是這樣的:
警告: 不要混合使用隱式和顯式等待堕油。這樣做會(huì)導(dǎo)致不可預(yù)測(cè)的等待時(shí)間潘飘。例如,將隱式等待設(shè)置為10秒掉缺,將顯式等待設(shè)置為15秒卜录,可能會(huì)導(dǎo)致在20秒后發(fā)生超時(shí)。
我一開(kāi)始讀到這一段的時(shí)候很疑惑為啥是20秒眶明,沒(méi)有理解艰毒,但是后來(lái)結(jié)合源碼看了一下,恍然大悟搜囱。
以官方說(shuō)明為例现喳,假設(shè)有這樣一段代碼:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
# 建立一個(gè)webdriver
driver = webdriver.Chrome()
# 設(shè)置隱式等待時(shí)間為10秒
driver.implicitly_wait(10)
driver.get("https://baidu.com")
# 查找一個(gè)ID為SomeId的元素,等待最長(zhǎng)時(shí)間為15秒犬辰,1秒輪詢(xún)一次
some_ele = WebDriverWait(driver, timeout=15, poll_frequency=1).until(
lambda d: d.find_element(By.ID, "SomeId"))
假如這個(gè)目標(biāo)元素根本不存在嗦篱,那么該段代碼中多少秒會(huì)拋出異常呢?
答案正如官網(wǎng)所說(shuō)是:20秒幌缝,確切的說(shuō)應(yīng)該是21秒灸促,有兩次輪詢(xún)間隔,輪詢(xún)間隔時(shí)間不設(shè)置默認(rèn)是0.5秒涵卵。那么為什么是20秒不是15秒呢浴栽?不應(yīng)該是已最大的等待時(shí)間為準(zhǔn)嗎?
接下來(lái)分析一下wait類(lèi)的源碼轿偎,先直接看until方法:
def until(self, method, message=''):
"""Calls the method provided with the driver as an argument until the \
return value is not False."""
# 源碼注釋的意思是“調(diào)用驅(qū)動(dòng)程序提供的方法作為參數(shù)典鸡,直到返回值不是False』祷蓿”
screen = None
stacktrace = None
# 首先定義一個(gè)最大時(shí)間=當(dāng)前時(shí)間+用戶(hù)設(shè)置的超時(shí)時(shí)間萝玷,我們?cè)O(shè)置是15秒。
end_time = time.time() + self._timeout
# 開(kāi)啟一個(gè)循環(huán)
while True:
try:
# 執(zhí)行用戶(hù)傳入的匿名函數(shù)昆婿,我們這里指的就是查找元素球碉,
# d.find_element(By.ID, "SomeId")
# 這個(gè)方法會(huì)觸發(fā)隱式等待,我們?cè)O(shè)置的隱式等待時(shí)間最長(zhǎng)是10秒仓蛆,
# 因?yàn)槲覀儾檎业脑夭淮嬖谡龆栽摲椒〞?huì)在此處阻塞10秒,
# 10秒后拋出異常NoSuchElementException看疙,
# 如果設(shè)置了NoSuchElementException為_(kāi)ignored_exceptions豆拨,
# 就會(huì)跳到except中捕獲截圖和堆棧
value = method(self._driver)
# 方法如果返回的值不為None直奋,或False,直接返回這個(gè)值施禾。
# 這個(gè)判斷要看一下python對(duì)于其他數(shù)據(jù)類(lèi)型和bool之間的轉(zhuǎn)換
# 一般只要是非空脚线,都為T(mén)rue
if value:
return value
except self._ignored_exceptions as exc:
screen = getattr(exc, 'screen', None)
stacktrace = getattr(exc, 'stacktrace', None)
# 程序休眠 _poll 秒,這里的_poll是我們構(gòu)建Wait類(lèi)時(shí)傳入的輪詢(xún)間隔拾积,我們?cè)O(shè)置為1秒殉挽。
time.sleep(self._poll)
# 判斷當(dāng)前時(shí)間是否已超出 最大等待時(shí)間,即上面定義的end_time,超出則跳出循環(huán)
if time.time() > end_time:
break
# 跳出循環(huán)則拋出異常拓巧,超時(shí)
raise TimeoutException(message, screen, stacktrace)
從代碼中可以看出斯碌,如果這個(gè)元素根本不存在,那最大時(shí)間應(yīng)該是 10秒+1秒+10秒+1秒=22秒肛度。第一個(gè)10秒指的是隱式等待時(shí)間設(shè)置的超時(shí)傻唾,第一個(gè)1秒是我們自己設(shè)置的輪詢(xún)間隔,然后這時(shí)候并沒(méi)有超出顯示等待的最大時(shí)間15秒承耿,所以進(jìn)入第二次循環(huán)冠骄,依舊是一個(gè)10秒的隱式等待,一個(gè)1秒的輪詢(xún)間隔加袋,然后這是已經(jīng)超出顯示等待最大時(shí)間15秒凛辣,所以跳出循環(huán),拋出超時(shí)異常职烧。這里我應(yīng)該使用腳本做個(gè)試驗(yàn)來(lái)驗(yàn)證自己的猜想扁誓。。
好蚀之,來(lái)寫(xiě)個(gè)腳本驗(yàn)證一下:
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
# 建立一個(gè)webdriver
driver = webdriver.Chrome()
# 設(shè)置隱式等待時(shí)間為10秒
driver.implicitly_wait(10)
# 打開(kāi)百度
driver.get("https://baidu.com")
# 記錄開(kāi)始時(shí)間
start = time.time()
print("開(kāi)始查找一個(gè)不存在的元素蝗敢,開(kāi)始時(shí)間為" + str(start))
try:
# 查找一個(gè)ID為SomeId的元素,等待最長(zhǎng)時(shí)間為15秒足删,1秒輪詢(xún)一次
some_ele = WebDriverWait(driver, timeout=15, poll_frequency=1).until(
lambda d: d.find_element(By.ID, "SomeId"))
except TimeoutException as exc:
# 捕獲 TimeoutException 異常
print("查找元素超時(shí)寿谴。。失受。")
finally:
# 記錄結(jié)束時(shí)間讶泰,
end = time.time()
# 打印耗時(shí)
print("查找結(jié)束,結(jié)束時(shí)間為:" + str(end) + "贱纠,耗時(shí):" + str(end - start))
# 關(guān)閉Driver
driver.close()
以上代碼中我們打開(kāi)百度來(lái)查找一個(gè)不存在的元素峻厚,看執(zhí)行結(jié)果:
開(kāi)始查找一個(gè)不存在的元素,開(kāi)始時(shí)間為1606379817.432131
查找元素超時(shí)谆焊。。浦夷。
查找結(jié)束辖试,結(jié)束時(shí)間為:1606379839.466272辜王,耗時(shí):22.034141063690186
控制臺(tái)輸出結(jié)果證實(shí)了我上面的猜想,假如我們這時(shí)將輪詢(xún)間隔時(shí)間改為2秒呢罐孝?
那最長(zhǎng)等待時(shí)間可能就會(huì)變成24秒呐馆。
由此可見(jiàn),這個(gè)兩者混用后的 最大等待時(shí)間和三個(gè)因素有關(guān)莲兢,顯示等待時(shí)間汹来、隱式等待時(shí)間和顯示輪詢(xún)間隔 。
最大等待時(shí)間的計(jì)算公式應(yīng)該是這樣的:
# 假設(shè) x=隱式等待時(shí)間, y=輪詢(xún)間隔時(shí)間, z=顯式等待時(shí)間, r=最長(zhǎng)等待時(shí)間
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
這個(gè)公式并不能直接簡(jiǎn)寫(xiě)成 x + y + z
因?yàn)樵诔绦蛑?除 操作是會(huì)省略余數(shù)的改艇。
下面說(shuō)明一下收班,那個(gè) NoSuchElementException
為啥會(huì)被捕獲。
貼一下WebDriverWait類(lèi)的源碼截圖:
源碼的注釋中也可以看到谒兄,如果不指定 ignored_exceptions
這個(gè)參數(shù)摔桦,默認(rèn)就會(huì)只包含 NoSuchElementException
這個(gè)異常,所以這個(gè)異常在等待中是默認(rèn)會(huì)被忽略的承疲。從源碼第 23 和 50 行中可以看出邻耕。
其實(shí)顯式等待可以簡(jiǎn)單的理解成是一個(gè)簡(jiǎn)單的封裝,其中也用到了 time.sleep()
方法燕鸽。
流暢等待
流暢等待 在官網(wǎng)的介紹中與顯式等待并無(wú)區(qū)別兄世,只是更多的添加了輪詢(xún)間隔與可忽略異常參數(shù)的使用。
在 Java 的 Selenium
實(shí)現(xiàn)中顯示等待和流暢等待是使用了兩個(gè)不同的類(lèi)啊研,后者是使用了一個(gè)叫 FluentWait
的子類(lèi)御滩,但在 Python 實(shí)現(xiàn)中并沒(méi)有區(qū)別。只是由于語(yǔ)言的特性不同悲伶,在 Java 中將兩中使用定義為了兩個(gè)不同的子類(lèi)艾恼。
這里就不在繼續(xù)介紹這種方式。
總結(jié):
那么最終我們到底應(yīng)該使用哪種方式呢麸锉?可能需要結(jié)合實(shí)際情況考慮钠绍,比如我們需要控制操控頁(yè)面的頻率時(shí),故意停頓時(shí)需要使用到 強(qiáng)制等待 的方式 time.sleep()
花沉;比如在檢索元素時(shí)防止元素未加載完成而拋出 NoSuchElementException
柳爽,就需要使用到顯式等待和隱式等待。
顯式等待 相對(duì)更加靈活一些碱屁,可以指定條件表達(dá)式磷脯,超時(shí)時(shí)間,輪詢(xún)頻率和要忽略的異常娩脾;但是 隱式等待 作用范圍更廣赵誓,一個(gè) Driver
的生命周期只需要設(shè)置一次就可以了。
通常來(lái)說(shuō)來(lái)給 Driver
設(shè)置一個(gè)相對(duì)合適的 隱式等待 時(shí)間就可以滿足大多數(shù)需求了,除非在需求中可能并非所有的元素需要等待的時(shí)間都那么的相對(duì)平均俩功,就需要給一些特殊的元素使用顯式等待的方式來(lái)做幻枉。
兩種等待方式混用其實(shí) 并非是禁用 的,只是在使用的時(shí)候需要考慮到最大的等待時(shí)間可能會(huì)超出預(yù)期诡蜓,這個(gè)就需要注意熬甫。但是一般情況下只要元素是確定存在的并不會(huì)超出設(shè)置的超時(shí)時(shí)間,除非網(wǎng)頁(yè)加載過(guò)慢蔓罚,或者網(wǎng)絡(luò)不佳的情況椿肩。