相关文章推荐
玩滑板的凉面  ·  Selenium ...·  5 天前    · 
瘦瘦的野马  ·  message ...·  1 年前    · 
低调的梨子  ·  Hive ...·  1 年前    · 

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来等待浏览器页面满足预估的条件,但可以用于模拟用户的操作速度。
  • 掘金·日新计划 RabbitMQ
    私信