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 描述的条件被满足。
def until_not(self, method, message=''):
"""每次轮询时调用 method,直到 method 的返回等价于 False。
即等待 method 描述的条件不被满足。
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
- 6424
-
程序员老鱼
掘金·日新计划
ChatGPT
OpenAI