可靠地检测页面加载或超时,Selenium 2

Posted

技术标签:

【中文标题】可靠地检测页面加载或超时,Selenium 2【英文标题】:Reliably detect page load or time out, Selenium 2 【发布时间】:2013-09-14 18:52:51 【问题描述】:

我正在使用 Selenium 2(2.33 版 Python 绑定,Firefox 驱动程序)编写一个通用的网络爬虫。它应该采用 任意 URL,加载页面并报告所有出站链接。由于 URL 是任意的,因此我无法对页面内容做出任何假设,因此通常的建议(等待特定元素出现)是不适用的。

我的代码应该轮询 document.readyState,直到它达到“完成”或 30 秒超时,然后继续:

def readystate_complete(d):
    # AFAICT Selenium offers no better way to wait for the document to be loaded,
    # if one is in ignorance of its contents.
    return d.execute_script("return document.readyState") == "complete"

def load_page(driver, url):
    try:
        driver.get(url)
        WebDriverWait(driver, 30).until(readystate_complete)
    except WebDriverException:
        pass

    links = []
    try:
        for elt in driver.find_elements_by_xpath("//a[@href]"):
            try: links.append(elt.get_attribute("href"))
            except WebDriverException: pass
    except WebDriverException: pass
    return links

这种方法有效,但在大约五分之一的页面上,.until 调用永远挂起。发生这种情况时,通常浏览器实际上还没有完成页面加载(“颤动”仍在旋转),但可能会经过数十分钟并且不会触发超时。但有时页面确实似乎已经完全加载,脚本仍然没有继续。

什么给了?如何使超时可靠地工作?是否有更好的方法来请求等待页面加载(如果无法对内容做出任何假设)?

注意:WebDriverException 的强迫性捕获和忽略已被证明是必要的,以确保它从页面中提取尽可能多的链接,无论页面内的 javascript 是否正在使用 DOM 做有趣的事情(例如,我用于在提取 HREF 属性的循环中获取“陈旧元素”错误)。

注意:这个问题在这个网站和其他地方都有很多变化,但它们都有一个微妙但关键的区别,使得答案(如果有的话)对我,或者我已经尝试了这些建议,但它们不起作用。 准确地回答我提出的问题。

【问题讨论】:

如果您使用的是 WebDriverWait,那么您使用的是 Selenium 2,而不是 Selenium RC。 @RossPatterson 我的印象是 Selenium 2 和 Selenium RC 是一回事,而 Selenium IDE 是旧的 QuicKeys 风格的东西。谢谢指正。 你最后做了什么? @KnewB 我放弃了。我的代码现在设置了一个全局一分钟超时,然后执行driver.get(url),紧接着是driver.find_elements_by_xpath("//a[@href]")。这似乎在报告链接之前等待页面加载。它仍然时不时地永远挂起,所以我还写了一个看门狗进程,如果它在五分钟内没有报告任何进展,它将杀死并重新启动整个浏览器。它经常触发,足以让人头疼,但不值得我花时间尝试进一步调试它。我还是希望有更多线索的人来这里。 您可以使用 pageLoadTimeOut() 方法。这需要浏览器等待页面加载的最长时间。如果页面在最大时间之前加载,则脚本继续执行。如果页面在最大时间后未加载,您可以捕获异常并关闭浏览器。希望这对您有所帮助。 【参考方案1】:

当我使用 Selenium 为一个相当知名的网站服务编写屏幕截图系统时,我遇到了类似的情况,并且遇到了同样的困境:我对正在加载的页面一无所知。

在与一些 Selenium 开发人员交谈后,答案是各种 WebDriver 实现(例如 Firefox 驱动程序与 IEDriver)对于何时考虑加载页面或不考虑 WebDriver 返回控制权做出不同的选择。

如果您深入研究 Selenium 代码,您可以找到尝试做出最佳选择的地方,但由于有很多事情会导致正在查找的状态失败,例如多个帧,其中一个没有” t 完成及时,有司机明显只是不返回的情况。

有人告诉我,“这是一个开源项目”,它可能不会/无法针对所有可能的情况进行纠正,但我可以在适用的情况下进行修复并提交补丁。

从长远来看,这对我来说有点多,和你一样,我创建了自己的超时过程。由于我使用 Java,我创建了一个新线程,在达到超时时,它会尝试做几件事来让 WebDriver 返回,即使有时只是按下某些键来让浏览器响应也有效。如果它没有返回,那么我会终止浏览器并再次尝试。

再次启动驱动程序已经为我们处理了大多数情况,好像浏览器的第二次加载允许它处于更稳定的状态(请注意,我们是从虚拟机启动并且浏览器不断地想要检查更新并运行最近没有启动的某些例程)。

另一部分是我们首先启动一个已知的 url 并确认浏览器的某些方面,并且我们实际上能够在继续之前与它进行交互。通过这些步骤,失败率非常低,大约 3%,在所有浏览器/版本/操作系统(FF、IE、CHROME、Safari、Opera、iosandroid 等)上进行了 1000 次测试

最后但并非最不重要的一点是,对于您的情况,听起来您只需要捕获页面上的链接,而不需要完全的浏览器自动化。我可能会采取其他方法,即 cURL 和 linux 工具。

【讨论】:

这很有趣。不过,为了记录,我实际上记录的不仅仅是链接,我需要使用一个在网络行为方面尽可能模仿“真实浏览”的设置,因此使用 FFDriver。【参考方案2】:

    “推荐”(但仍然丑陋)的解决方案可能是使用explicit wait:

    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait 
    from selenium.webdriver.support import expected_conditions
    
    old_value = browser.find_element_by_id('thing-on-old-page').text
    browser.find_element_by_link_text('my link').click()
    WebDriverWait(browser, 3).until(
        expected_conditions.text_to_be_present_in_element(
            (By.ID, 'thing-on-new-page'),
            'expected new text'
        )
    )
    

    天真的尝试是这样的:

    def wait_for(condition_function):
        start_time = time.time()
        while time.time() < start_time + 3:
            if condition_function():
                return True
            else:
                time.sleep(0.1)
        raise Exception(
            'Timeout waiting for '.format(condition_function.__name__)
        )
    
    
    def click_through_to_new_page(link_text):
        browser.find_element_by_link_text('my link').click()
    
        def page_has_loaded():
            page_state = browser.execute_script(
                'return document.readyState;'
            ) 
            return page_state == 'complete'
    
        wait_for(page_has_loaded)
    

    另一个更好的是(感谢@ThomasMarks):

    def click_through_to_new_page(link_text):
        link = browser.find_element_by_link_text('my link')
        link.click()
    
        def link_has_gone_stale():
            try:
                # poll the link with an arbitrary call
                link.find_elements_by_id('doesnt-matter') 
                return False
            except StaleElementReferenceException:
                return True
    
        wait_for(link_has_gone_stale)
    

    最后一个示例包括如下比较页面 id(这可能是防弹的):

    class wait_for_page_load(object):
    
        def __init__(self, browser):
            self.browser = browser
    
        def __enter__(self):
            self.old_page = self.browser.find_element_by_tag_name('html')
    
        def page_has_loaded(self):
            new_page = self.browser.find_element_by_tag_name('html')
            return new_page.id != self.old_page.id
    
        def __exit__(self, *_):
            wait_for(self.page_has_loaded)
    

    现在我们可以这样做了:

    with wait_for_page_load(browser):
        browser.find_element_by_link_text('my link').click()
    

以上代码示例来自Harry's blog。

【讨论】:

不幸的是,对于我的用例来说,这些都不够好:(1)不是初学者,因为我不知道任何“新页面上的东西”,这必须适用于 任意 内容未知的页面。 (2) 与我原来的问题中的代码有同样的问题(很多时候工作,但有时会永远挂起)。 (3, 4) 将在页面实际加载之前触发 感谢您的尝试!正如我在其他地方提到的,我完全放弃了 Selenium,因为这太难了。【参考方案3】:

据我所知,您的 readystate_complete 没有做任何事情,因为 driver.get() 已经在检查这种情况。无论如何,我已经看到它在很多情况下都不起作用。您可以尝试的一件事是通过代理路由您的流量,并将其用于 ping 任何网络流量。即browsermob 有wait_for_traffic_to_stop 方法:

def wait_for_traffic_to_stop(self, quiet_period, timeout):
"""
Waits for the network to be quiet
:Args:
- quiet_period - number of seconds the network needs to be quiet for
- timeout - max number of seconds to wait
"""
    r = requests.put('%s/proxy/%s/wait' % (self.host, self.port),
        'quietPeriodInMs': quiet_period, 'timeoutInMs': timeout)
    return r.status_code

【讨论】:

你说得对,readystate_complete 一点用处不大;正如我上面提到的,当我把它拿出来时,它开始工作得更好了。由于其他原因已经有了代理,所以我会考虑您的建议。【参考方案4】:

如果页面仍在无限期加载,我猜 readyState 永远不会达到“完成”。如果您使用的是 Firefox,您可以通过调用 window.stop() 来强制停止页面加载:

try:
    driver.get(url)
    WebDriverWait(driver, 30).until(readystate_complete)
except TimeoutException:
    d.execute_script("window.stop();")

【讨论】:

仅供参考,这是该问题其他变体的建议之一,我提到我尝试过但没有奏效。具体来说,它并不能防止永远挂起的现象,尽管它可能降低了它的频率。【参考方案5】:

这是Tommy Beadle 提出的解决方案(使用staleness 方法):

import contextlib
from selenium.webdriver import Remote
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.expected_conditions import staleness_of

class MyRemote(Remote):
    @contextlib.contextmanager
    def wait_for_page_load(self, timeout=30):
        old_page = self.find_element_by_tag_name('html')
        yield
        WebDriverWait(self, timeout).until(staleness_of(old_page))

【讨论】:

... 这与您的其他答案中的 (3, 4) 存在相同的问题:在旧页面被销毁后不久,触发太早 ,这甚至可能在浏览器完成 HTML 解析之前发生。

以上是关于可靠地检测页面加载或超时,Selenium 2的主要内容,如果未能解决你的问题,请参考以下文章

在 Selenium Python 绑定中设置页面加载超时

python和php使用selenium捕获超时异常无法继续问题

Selenium WebDriver的使用

FireFox 位于“从...传输数据”或“读取...”

selenium常用的API设置get方法最大加载时间

检测页面是不是在 JavaScript 中的 WKWebView 中加载