Selenium 等待与超时(一)

概述

Selenium 作为模拟爬取的利器,可以绕过很多反爬策略。但爬虫在使用代理后超时是常用的事,正确等待和处理超时能改善我们的编程体验、提高爬取效率。

本文为该系列的第一篇,总结了 Python selenium 库中提供的各种设置等待超时的方法,提出了一些注意事项和建议,并澄清显式和隐式等待的概念。

本文所有示例代码均通过测试,测试环境为 Windows 10,Python 3.10.1, Selenium 4.1.0。

设置页面加载超时

利用 selenium 打开页面需要调用 WebDriver.get() 方法,但对于我们这些写爬虫的混蛋来说,为了绕过目标网站的反爬策略,一般都会使用代理,而代理的质量又参差不齐,可能会导致打开页面很慢或干脆打不开,selenium 默认的加载页面超时为 5 分钟,太长了,很多代理的有效期也就 5 分钟,为了提高爬取的效率就需要限制打开页面的时间,超时后立即更换代理。设置 WebDriver.get() 的超时需要调用 WebDriver.set_page_load_timeout()。例如,下面的示例将打开 google.com 页面的超时设为 3 秒。

from selenium import webdriver
from selenium.common.exceptions  import TimeoutException

chrome = webdriver.Chrome()
chrome.set_page_load_timeout(3)
    
try:
    chrome.get("https://google.com")
except TimeoutException:
    print("超时了")
finally:
    chrome.quit()
复制代码

注意:WebDriver.implicitly_wait() 只能设置查找元素和执行命令的超时,对页面加载操作的超时无效。

注意:selenium 默认的页面加载超时为 300 秒。

selenium 中所有超时方法的参数单位都为秒。内部为将参数乘1000 转成毫秒。

WebDriver.implicitly_wait() 设置的超时并不适用于用户操作导致的页面重新加载或页面脚本对 DOM 整体结构的改变,遇到这两种情况考虑适用其他等待条件。

设置查找元素和执行命令超时

在页面上查找元素时,如果事先未调用 WebDriver.implicitly_wait() 设置查找元素和执行命令的超时,则在找不到元素时会直接抛出 selenium.common.exceptions.NoSuchElementException;如果调用了 WebDriver.implicitly_wait() 则会等待指定的时间后再抛出异常。

下面的示例通过调用 WebDriver.implicitly_wait() 将查找元素的时间限制设为 5 秒。

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By

chrome = webdriver.Chrome()
chrome.implicitly_wait(5)

try:
    chrome.get("https://zhihu.com")
    chrome.find_element(by=By.ID, value='xxx')
except NoSuchElementException:
    print("找不到元素")
finally:
    chrome.quit()
复制代码

selenium 默认的 implicitly_wait 为 0 秒。

设置执行脚本超时

使用 selenium 打开页面后,还可以调用 WebDriver.execute_script() 在当前窗口或 frame 同步执行 JavaScript 脚本。我之前写爬虫时经常通过执行脚本来触发页面事件,或者提取变量值,或者修改页面 DOM 结构。执行 JavaScript 脚本的默认超时为 30 秒,但可以调用 WebDriver.set_script_timeout() 来主动设置。

下面的示例通过执行 JavaScript 脚本来获取页面标题,并将脚本超时设为 1 秒:

from selenium import webdriver
from selenium.common.exceptions import TimeoutException

chrome = chrome_init()

chrome.set_script_timeout(1)
try:
    chrome.get("https://zhihu.com")
    title = chrome.execute_script("return document.title")
    print(f"title={title}")
except TimeoutException:
    print("超时了")
finally:
    print(time.time())
    chrome.quit()
复制代码

输出:title=知乎 - 有问题,就会有答案

等待某些条件被满足

除了等待 WebDriver.get() 触发的页面加载、 WebDriver.execute_script() 触发的脚本执行、以及 WebDriver.find_element* 系列方法触发的元素查找外,selenium 还提供了一个类 WebDriverWait 来等待某些客观条件的出现。

WebDriverWait 的原理是每隔一段时间轮询一次,判断条件是否满足。构造函数参数如下:

  • driver:WebDriver 实例。
  • timeout:超时,单位为秒。
  • poll_frequency:轮询间隔(即每隔多少秒查询一次条件是否满足),默认为 0.5 秒。
  • ignored_exceptions:需要忽略的异常列表。

两个等待函数:

def until(self, method, message=''):
    """每次轮询时调用 method,直到 method 的返回不等价于 False。
    即等待 method 描述的条件被满足。
    """
    # pass
def until_not(self, method, message=''):
    """每次轮询时调用 method,直到 method 的返回等价于 False。
    即等待 method 描述的条件不被满足。
    """
    # pass
复制代码

参数:

  • method:每次轮询时要执行的方法。
  • message:轮询超时后传递给超时异常的文本消息。

例如,在用 selenium 模拟用户点击某”提交“按钮,然后等待页面出现成功通知。假设通知成功的控件 id 为 success。

from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(chrome, 10).until(EC.visibility_of_element((By.ID, 'success')))
复制代码

Selenium 提供的等待条件列表

等待条件由模块 selenium.webdriver.support import expected_conditions,一般将该模块导入为 EC,并将该模块提供的方法传递给 WebDriverWait.until()WebDriverWait.until_not()

from selenium.webdriver.support import expected_conditions as EC
复制代码

URL 规则

下表中的方法检测当前页面的链接是否符合特定条件。

方法 描述
url_contains(url: string) 链接包含子串 url
url_matches(pattern) 链接符合正则表达式描述的规则
url_to_be(url: string) 链接符合正则表达式描述的规则
url_changes(url: string) 链接变得和 url 不一样

标题规则

下表中的方法检测当前页面的标题是否符合特定条件。

方法 描述
title_is(title: string) 标题等于 title
title_contains(title: string) 标题包含 title

元素是否出现在 DOM 中

下表中的方法检测指定的元素是否出现在页面 DOM 结构中。注意,元素在 DOM 出现不一定代表该元素可被用户看见。

方法 描述
presence_of_element_located(locator) locator 指定的元素出现在 DOM 中
presence_of_all_elements_located(locator) locator 指定的所有元素出现在 DOM 中

locator 参数比较复杂,由模块 selenium.webdriver.common.by 提供。

元素可见性

下表中的方法检测指定的元素是否可见。

方法 描述
visibility_of_element_located(locator) locator 指定的某个元素可见
visibility_of(element) 元素 element 可见
visibility_of_any_elements_located(locator) locator 指定的元素中至少一个可见
invisibility_of_element_located(locator) locator 指定的某个元素不可见
invisibility_of_element(element) 元素 element 不可见

元素文本

下表中的方法检测指定的文本是否出现在元素上或熟悉中。

方法 描述
text_to_be_present_in_element(locator, text_) locator 指定的元素上包含文本 text_
text_to_be_present_in_element_value(locator, text_) locator 指定的元素的 value 属性值包含文本 text_
text_to_be_present_in_element_attribute(locator, attribute_, text_) locator 指定的元素的 attribute_ 指定的属性值中包括文本 text_

元素的其他状态

方法 描述
element_to_be_clickable(mark) 元素可见并可点击,mark 即可以是 locator,也可以是 WebElement
staleness_of(element) 元素 element 不再属于 DOM
element_to_be_selected(element) 元素 element 被选中
element_located_to_be_selected(locator) locator 指定的元素被选中
element_selection_state_to_be(element, is_selected) 元素 element 的选中状态符合 is_selected,True 表示被选中,False 表示未选中。
element_located_selection_state_to_be(locator, is_selected) locator 指定的元素的选中状态符合 is_selected,True 表示被选中,False 表示未选中。
element_attribute_to_include(locator, attribute_) locator 指定的元素包含attribute_指定的属性

窗口状态

方法 描述
number_of_windows_to_be(num_windows) 当前窗口数为 num_windows
new_window_is_opened(current_handles) 有新窗口被打开,current_handles 为当前(调用该方法前)的窗口句柄列表
alert_is_present() 出现 alert 窗口

复合条件

随着 2021 年 10 月 13 日 Python selenium 4.0 的发布,终于支持复合条件了。

符合条件即将上面列出的等待条件通过逻辑操作符连接起来,逻辑操作包括:与、或、非。

方法 描述
all_of(*expected_conditions) expected_conditions 列表中条件都被满足
any_of(*expected_conditions) expected_conditions 列表中任何一个条件被满足
none_of(*expected_conditions) expected_conditions 列表中任何一个条件都不符合

这些复合条件还可以相互嵌套哦。

澄清一下隐式等待、显式等待、强制等待

很多资料会把 WebDriver.implicitly_wait() 设置的超时称为”隐式等待“,把其他设置超时的方法称为”显式等待“。其实隐式等待是早期的概念,指的是设置等待超时的操作和真正等待的过程发生在不同地方。但后来等待的方法越来越多,从概念上来讲 WebDriver.set_page_load_timeout()WebDriver.set_script_timeout() 也属于隐式等待,虽然它们的方法名中却没有 implicitly

WebDriverWait 同时设置超时并等待,所以称为显式等待。

”强制等待“是网友自己发明的概念,一般是通过调用 time.time.sleep() 实现,因为 sleep 会强制让调用的线程休眠一段时间,和浏览器页面情况没关系。不建议使用 sleep 来等待浏览器页面满足预计的条件,即不保险,也不知道页面到底发生了什么,无论 sleep 多久都不能 100% 保证预计的条件会被满足,而且也不好决定该 sleep 多久。

个人建议,要么将 WebDriver.implicitly_wait()WebDriver.set_page_load_timeout()WebDriver.set_script_timeout() 都称为”隐式等待“,并将 WebDriverWait 称为显式等待;要么就不要提”显式等待“和”显式等待“。强制等待就更不要提了。

sleep 模仿用户行为

虽然不建议将 sleep 用于等待页面条件,但 sleep 可用于模拟用户行为,避免被模板网站识别成爬虫。有些网站可以会统计用户多个操作之间的时间间隔,以及请求之前的时间间隔,如果太快肯定是爬虫。这种情况就可以用 sleep 在操作之间停顿,以模拟用户操作的速度。

注意事项

  • WebDriver.implicitly_wait() 设置的超时不适用于页面加载。
  • WebDriver.implicitly_wait() 设置的超时只适用于通过调用 WebDriver.get() 触发的页面加载,并不适用于用户操作导致的页面重新加载或页面脚本对 DOM 整体结构的改变,遇到后两种情况考虑使用其他等待条件。
  • 阅读资料时,注意区分”隐式等待“、”显式等待“的概念。
  • 忽略网文中的”强制等待概念“。
  • 不要用time.time.sleep来等待浏览器页面满足预估的条件,但可以用于模拟用户的操作速度。

猜你喜欢

转载自juejin.im/post/7042481948865855495