5.3 渲染动态网页
对于搜索网页这个例子,我们可以很容易地对其运行过程实施逆向工程。但是,一些网站非常复杂,即使使用类似Firebug这样的工具也很难理解。比如,一个网站使用Google Web Toolkit (GWT )开发,那么它产生的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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论