返回介绍

5.3 渲染动态网页

发布于 2024-02-05 23:37:18 字数 10829 浏览 0 评论 0 收藏 0

对于搜索网页这个例子,我们可以很容易地对其运行过程实施逆向工程。但是,一些网站非常复杂,即使使用类似Firebug这样的工具也很难理解。比如,一个网站使用Google Web ToolkitGWT )开发,那么它产生的JavaScript代码是机器生成的压缩版。生成的JavaScript代码虽然可以使用类似JSbeautifier 的工具进行还原,但是其产生的结果过于冗长,而且原始的变量名也已经丢失,这就会造成其结果难以处理。尽管经过足够的努力,任何网站都可以被逆向工程,但我们可以使用浏览器渲染引擎避免这些工作,这种渲染引擎是浏览器在显示网页时解析HTML、应用CSS样式并执行JavaScript语句的部分。在本节中,我们将使用WebKit渲染引擎,通过Qt框架可以获得该引擎的一个便捷Python接口。

什么是WebKit?

WebKit的代码源于1998年的KHTML项目,当时它是Konqueror浏览器的渲染引擎。2001年,苹果公司将该代码衍生为WebKit,并应用于Safari浏览器。Google在Chrome 27之前的版本也使用了WebKit内核,直到2013年转向利用WebKit开发的Blink内核。Opera在2003年到2012年使用的是其内部的Presto渲染引擎,之后切换到WebKit,但是不久又跟随Chrome转向Blink。其他主流渲染引擎还包括IE使用的Trident和Firefox的Gecko。

5.3.1 PyQt还是PySide

Qt框架有两种可以使用的Python库,分别是PyQt 和PySide 。PyQt 最初于1998年发布,但在用于商业项目时需要购买许可。由于该原因,开发Qt的公司(原先是诺基亚,现在是Digia)后来在2009年开发了另一个Python库PySide ,并且使用了更加宽松的LGPL许可。

虽然这两个库有少许区别,但是本章中的例子在两个库中都能够正常工作。下面的代码片段用于导入已安装的任何一种Qt库。

try:
    from PySide.QtGui import *
    from PySide.QtCore import *
    from PySide.QtWebKit import *
except ImportError:
    from PyQt4.QtGui import *
    from PyQt4.QtCore import *
    from PyQt4.QtWebKit import *

在这段代码中,如果PySide 不可用,则会抛出ImportError 异常,然后导入PyQt 模块。如果PyQt 模块也不可用,则会抛出另一个ImportError 异常,然后退出脚本。

下载和安装这两种Python库的说明可以分别参考`http://qt-project.org/wiki/Setting_up_PySide`和`http://pyqt.sourceforge.net/Docs/PyQt4/installation.html`。

5.3.2 执行JavaScript

为了确认WebKit能够执行JavaScript,我们可以使用位于http://example.`` ``webscraping.com/dynamic``上的 这个简单示例。

该网页只是使用JavaScript在div 元素中写入了Hello World 。下面是其源代码。

<html>
    <body>
        <div id="result"></div>
        <script>
        document.getElementById("result").innerText = 'Hello World';
        </script>
    </body>
</html>

使用传统方法下载原始HTML并解析结果时,得到的div 元素为空值,如下所示。

>>> url = 'http://example.webscraping.com/dynamic'
>>> html = D(url)
>>> tree = lxml.html.fromstring(html)
>>> tree.cssselect('#result')[0].text_content()
''

下面是使用WebKit的初始版本代码,当然还需事先导入上一节提到的PyQt 或PySide 模块。

>>> app = QApplication([])
>>> webview = QWebView()
>>> loop = QEventLoop()
>>> webview.loadFinished.connect(loop.quit)
>>> webview.load(QUrl(url))
>>> loop.exec_()
>>> html = webview.page().mainFrame().toHtml()
>>> tree = lxml.html.fromstring(html)
>>> tree.cssselect('#result')[0].text_content()
'Hello World'

因为这里有很多新知识,所以下面我们会逐行分析这段代码。

第一行初始化了QApplication 对象,在其他Qt对象完成初始化之前,Qt框架需要先创建该对象。

接下来,创建QWebView 对象,该对象是Web文档的容器。

创建QEventLoop 对象,该对象用于创建本地事件循环。

QWebView 对象的loadFinished 回调连接了QEventLoop 的quit 方法,从而可以在网页加载完成之后停止事件循环。然后,将要加载的URL传给QWebView 。PyQt 需要将该URL字符串封装在QUrl 对象当中,而对于PySide 来说则是可选项。

由于QWebView 的加载方法是异步的,因此执行过程会在网页加载时立即传入下一行。但我们又希望等待网页加载完成,因此需要在事件循环启动时调用loop.exec_() 。

网页加载完成后,事件循环退出,执行过程移到下一行,对加载得到的网页所产生的HTML进行数据抽取。

从最后一行可以看出,我们成功执行了JavaScript,div 元素果然抽取出了Hello World 。

这里使用的类和方法在C++的Qt框架网站中都有详细的文档,其网址为http://qt-project.org/doc/qt-4.8/ 。虽然PyQt 和PySide 都有其自身的文档,但是原始C++版本的描述和格式更加详尽,一般的Python开发者可以用它替代。

5.3.3 使用WebKit与网站交互

我们用于测试的搜索网页需要用户修改后提交搜索表单,然后单击页面链接。而前面介绍的浏览器渲染引擎只能执行JavaScript,然后访问生成的HTML。要想抓取搜索页面,我们还需要对浏览器渲染引擎进行扩展,使其支持交互功能。幸运的是,Qt包含了一个非常棒的API,可以选择和操纵HTML元素,使交互操作变得简单。

对于之前的AJAX搜索示例,下面给出另一个实现版本。该版本已经将搜索条件设为'.' ,每页显示数量设为'1000' ,这样只需一次请求就能获取到全部结果。

app = QApplication([])
webview = QWebView()
loop = QEventLoop()
webview.loadFinished.connect(loop.quit)
webview.load(QUrl('http://example.webscraping.com/search'))
loop.exec_()
webview.show()
frame = webview.page().mainFrame()
frame.findFirstElement('#search_term').
        setAttribute('value', '.')
frame.findFirstElement('#page_size option:checked').
        setPlainText('1000')
frame.findFirstElement('#search').
        evaluateJavaScript('this.click()')
app.exec_()

最开始几行和之前的Hello World 示例一样,初始化了一些用于渲染网页的Qt对象。之后,调用QWebView GUI的show() 方法来显示渲染窗口,这可以方便调试。然后,创建了一个指代框架的变量,可以让后面几行代码更短。QWebFrame 类有很多与网页交互的有用方法。接下来的两行使用CSS模式在框架中定位元素,然后设置搜索参数。而后表单使用evaluateJavaScript() 方法进行提交,模拟点击事件。该方法非常实用,因为它允许我们插入任何想要的JavaScript代码,包括直接调用网页中定义的JavaScript方法。最后一行进入应用的事件循环,此时我们可以对表单操作进行复查。如果没有使用该方法,脚本将会直接结束。

图5.4所示为脚本运行时的显示界面。

1.等待结果

实现WebKit爬虫的最后一部分是抓取搜索结果,而这又是最难的一部分,因为我们难以预估完成AJAX事件以及准备好国家数据的时间。有三种方法可以处理这一问题,分别是:

等待一定时间,期望AJAX事件能够在此时刻之前完成;

重写Qt的网络管理器,跟踪URL请求的完成时间;

轮询网页,等待特定内容出现。

图5.4

第一种方案最容易实现,不过效率也最低,因为一旦设置了安全的超时时间,就会使大多数请求浪费大量不必要的时间。而且,当网络速度比平常慢时,固定的超时时间会出现请求失败的情况。第二种方案虽然更加高效,但是如果延时出现在客户端而不是服务端时,则无法使用。比如,已经完成下载,但是需要再单击一个按钮才会显示内容这种情况,延时就出现在客户端。第三种方案尽管存在一个小缺点,即会在检查内容是否加载完成时浪费CPU周期,但是该方案更加可靠且易于实现。下面是使用第三种方案的实现代码。

>>> elements = None
>>> while not elements:
...     app.processEvents()
...     elements = frame.findAllElements('#results a')
...
>>> countries = [e.toPlainText().strip() for e in elements]
>>> print countries
 [u'Afghanistan', u'Aland Islands', ... , u'Zambia', u'Zimbabwe']

如上实现中,代码不断循环,直到国家链接出现在results 这个div元素中。每次循环,都会调用app.processEvents() ,用于给Qt事件循环执行任务的时间,比如响应点击事件和更新GUI。

2.渲染类

为了提升这些功能后续的易用性,下面会把使用到的方法封装到一个类中,其源代码可以从https://bitbucket.org/wswp/code/src/tip/ chapter05/browser_render.py 获取。

import time

class BrowserRender(QWebView):
    def __init__(self, show=True):
        self.app = QApplication(sys.argv)
        QWebView.__init__(self)
        if show:
            self.show() # show the browser

    def download(self, url, timeout=60):
        """Wait for download to complete and return result"""
        loop = QEventLoop()
        timer = QTimer()
        timer.setSingleShot(True)
        timer.timeout.connect(loop.quit)
        self.loadFinished.connect(loop.quit)
        self.load(QUrl(url))
        timer.start(timeout * 1000)

        loop.exec_() # delay here until download finished
        if timer.isActive():
            # downloaded successfully
            timer.stop()
            return self.html()
        else:
            # timed out
            print 'Request timed out: ' + url

    def html(self):
        """Shortcut to return the current HTML"""
        return self.page().mainFrame().toHtml()

    def find(self, pattern):
        """Find all elements that match the pattern"""
        return self.page().mainFrame().findAllElements(pattern)

    def attr(self, pattern, name, value):
        """Set attribute for matching elements"""
        for e in self.find(pattern):
            e.setAttribute(name, value)

    def text(self, pattern, value):
        """Set attribute for matching elements"""
        for e in self.find(pattern):
            e.setPlainText(value)

    def click(self, pattern):
        """Click matching elements"""
        for e in self.find(pattern):
            e.evaluateJavaScript("this.click()")

    def wait_load(self, pattern, timeout=60):
        """Wait until pattern is found and return matches"""
        deadline = time.time() + timeout
        while time.time() < deadline:
            self.app.processEvents()
            matches = self.find(pattern)
            if matches:
                return matches
        print 'Wait load timed out'

你可能已经注意到,在download() 和wait_load() 方法中我们增加了一些代码用于处理定时器。定时器用于跟踪等待时间,并在截止时间到达时取消事件循环。否则,当出现网络问题时,事件循环就会无休止地运行下去。

下面是使用这个新实现的类抓取搜索页面的代码。

>>> br = BrowserRender()
>>> br.download('http://example.webscraping.com/search')
>>> br.attr('#search_term', 'value', '.')
>>> br.text('#page_size option:checked', '1000')
>>> br.click('#search')
>>> elements = br.wait_load('#results a')
>>> countries = [e.toPlainText().strip() for e in elements]
>>> print countries
 [u'Afghanistan', u'Aland Islands', ..., u'Zambia', u'Zimbabwe']

5.3.4 Selenium

使用前面例子中的WebKit库,我们可以自定义浏览器渲染引擎,这样就能完全控制想要执行的行为。如果不需要这么高的灵活性,那么还有一个不错的替代品Selenium可以选择,它提供了使浏览器自动化的API接口。Selenium可以通过如下命令使用pip 安装。

pip install selenium

**

为了演示Selenium是如何运行的,我们会把之前的搜索示例重写成Selenium的版本。首先,创建一个到浏览器的连接。

>>> from selenium import webdriver
>>> driver = webdriver.Firefox()

当运行该命令时,会弹出一个空的浏览器窗口,如图5.5所示。

该功能非常方便,因为在执行每条命令时,都可以通过浏览器窗口来检查Selenium是否依照预期运行。尽管这里我们使用的浏览器是Firefox,不过Selenium也提供了连接其他常见浏览器的接口,比如Chrome和IE。需要注意的是,我们只能使用系统中已安装浏览器的Selenium接口。

图5.5

如果想在选定的浏览器中加载网页,可以调用get() 方法:

>>> driver.get('http://example.webscraping.com/search')

然后,设置需要选取的元素,这里使用的是搜索文本框的ID。此外,Selenium也支持使用CSS选择器或XPath来选取元素。当找到搜索文本框之后,我们可以通过send_keys() 方法输入内容,模拟键盘输入。

>>> driver.find_element_by_id('search_term').send_keys('.')

为了让所有结果可以在一次搜索后全部返回,我们希望把每页显示的数量设置为1000。但是,由于Selenium的设计初衷是与浏览器交互,而不是修改网页内容,因此这种想法并不容易实现。要想绕过这一限制,我们可以使用JavaScript语句直接设置选项框的内容。

>>> js = "document.getElementById('page_size').options[1].text ='1000'"
>>> driver.execute_script(js);

此时表单内容已经输入完毕,下面就可以单击搜索按钮执行搜索了。

>>> driver.find_element_by_id('search').click()

现在,我们需要等待AJAX请求完成之后才能加载结果,在之前讲解的WebKit实现中这里是最难的一部分脚本。幸运的是,Selenium为该问题提供了一个简单的解决方法,那就是可以通过implicitly_wait() 方法设置超时时间。

>>> driver.implicitly_wait(30)

此处,我们设置了30秒的延时。如果我们要查找的元素没有出现,Selenium至多等待30秒,然后就会抛出异常。要想选取国家链接,我们依然可以使用WebKit示例中用过的那个CSS选择器。

>>> links = driver.find_elements_by_css_selector('#results a')

然后,抽取每个链接的文本,并创建一个国家列表。

>>> countries = [link.text for link in links]
>>> print countries
 [u'Afghanistan', u'Aland Islands', ..., u'Zambia', u'Zimbabwe']

最后,调用close() 方法关闭浏览器。

>>> driver.close()

本示例的源代码可以从https://bitbucket.org/wswp/code/src/ tip/chapter05/selenium_search.py 获取。如果想进一步了解Selenium这个Python库,可以通过https://selenium-python.readthedocs.org/ 获取其文档。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文