每篇一句:
A strong man will struggle with the storms of fate.
前言:
上一篇文章中我们介绍了爬取动态网页的一种方式:逆向工程。
这种方式有一点美中不足:这种方式要求我们对JavaScript以及Ajax有一定的了解,而且当网页的JS代码混乱,难以分析的时候,上述过程会花费我们大量的时间和精力。
这时候,如果对爬虫的执行效率没有过多要求,又不想浪费太多时间在了解JavaScript代码逻辑、找寻Ajax请求链接上,我们可以尝试另一种方式——渲染动态网页 。
浏览器渲染引擎:
简介:
在介绍这种方式之前,我们需要首先了解一些浏览器渲染引擎的基本知识。
渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。浏览器向服务器发送请求,得到服务器返回的资源文件后,经过需要渲染引擎的处理,将资源文件显示在浏览器窗口中。
目前使用较为广泛的渲染引擎有两种:
- webkit——使用者有Chrome, Safari
- Geoko——使用者有Firefox
渲染主流程:
- 渲染引擎首先通过网络获得所请求文档的内容,通常以8K分块的方式完成。
- 下面是渲染引擎在取得内容之后的基本流程:
 解析html以构建dom树->构建render树->布局render树->绘制render树

- 渲染引擎开始解析html,并将标签转化为内容树中的dom节点。如果遇到JS,那么此时会启用单独连接进行下载,并且在下载后进行解析。
- 接着,它解析外部CSS文件及style标签中的样式信息。这些样式信息以及html中的可见性指令将被用来构建另一棵树——render树。
 Render树由一些包含有颜色和大小等属性的矩形组成,它们将被按照正确的顺序显示到屏幕上。
- Render树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标。
- 再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。
想要了解更多有关浏览器渲染引擎的知识,可以阅读这些:
前端文摘:深入解析浏览器的幕后工作原理
浏览器加载、解析、渲染的过程
浏览器加载和渲染html的顺序-css渲染效率的探究
思考:
了解了浏览器渲染引擎的基本原理,我们可以发现:
- 当浏览器渲染引擎完成了dom树以及render树的构建之后,树中就已经包含了我们在浏览器窗口中可以看到的所有数据。
那么我们就有了一种爬取动态网页的新思路:
- 在浏览器渲染引擎执行layout以及printing之前,得到dom树或者render树,从树中获取动态加载的数据。
渲染动态网页:
有两种选择:
- 自己从头实现一个浏览器渲染引擎,在合适的时机返回构建的dom树或render树:
 这需要进行大量的工作,需要考虑html、js、css等不同格式文件的解析方式以及解析顺序等。
本人参考以下资料进行了尝试,但最终没有成功,有兴趣的可以尝试下。
- 使用PyV8解析HTML 文档 (文中提到的demo可以从下面的链接得到)
- PyV8源码以及demo
- 利用已有的渲染引擎。
 接下来将使用WebKit 渲染引擎,通过 PySide 这个python库可以获得该引擎的一个便捷接口。
示例:
还是以 新浪读书——书摘 为例,可以发现:页面中文章列表的部分是动态加载的。
使用PySide库进行处理的示例代码如下:
# coding=utf-8
from PySide.QtGui import *
from PySide.QtCore import *
from PySide.QtWebKit import *
if __name__ == '__main__':
    url = "http://book.sina.com.cn/excerpt/rwws/"
    app = QApplication([])  # 完成其他Qt对象之前,必须先创建该对象
    webview = QWebView()  # 该对象是Web 对象的容器
    # 调用show方法显示窗口
    # webview.show()
    # 设置循环事件, 并等待网页加载完成
    loop = QEventLoop()
    webview.loadFinished.connect(loop.quit)
    webview.load(QUrl(url))
    loop.exec_()
    frame = webview.page().mainFrame()  # QWebFrame类有很多与网页交互的有用方法
    # 得到页面渲染后的html代码
    html = frame.toHtml()
    print html通过print语句,我们可以发现:页面的源码html中已经包含了动态加载的内容。
- 与网站交互:
 得到动态加载的内容后,需要解决的另一个问题是翻页问题。还好PySide库的QWebKit模块还有一个名为QWebFrame的类,支持很多与网页的交互操作。
- 如“点击”:
# 根据CSS Selector 找到所需“进行翻页”的元素
elem = frame.findFirstElement('#subShowContent1_loadMore')
# 点击:通过evaluateJavaScript()函数可以执行Js代码
elem.evaluateJavaScript('this.click()')- 除了点击事件,还可以进行填充表单,滚动窗口等操作
- 需要注意的是,在进行了翻页、或者获取更多内容时,一个最大的难点在于如何确定页面是否完成了加载,因为我们难以估计Ajax事件或者Js准备数据的时间。对于这个问题有两种解决思路:
- 等待固定的一段时间,比如time.sleep(3):这种方法容易实现,但效率较低。
- 轮询网页,等待特定内容出现:
 这种方法虽然会在检查是否加载完成时浪费CPU周期,但更加可靠。
 以下是一个简单的实现:
elem = None
while not elem:
 app.processEvents()
 elem = frame.findAllElemnets('#pattern')代码循环,直到出现特定元素。每次循环,调用app.processEvents()方法,用于给Qt事件循环执行任务的时间,比如响应点击事件。
更多有关PySide的内容请看这里:PySide官方文档
但是PySide毕竟是一个为了Python的GUI 编程而开发的, 其功能对于爬虫来说实在是太过于庞大,所以我们可以把爬虫经常使用的功能进行封装,来提升编写爬虫的效率。
对PySide 常用功能的封装 —— ghost.py
ghost.py 是目前一个针对爬虫且功能比较完善的PySide的封装模块,使用它可以很方便的进行数据采集。
还是以获取列表页中每篇文章详情页地址为目标,直接看示例代码:
# coding=utf-8
import re
import time
from ghost import Ghost, Session
class SinaBookSpider(object):
    # 初始化相关参数
    gh = Ghost()
    ss = Session(gh, display=True)  # 设置display为true, 方便调试
    total = 1526  # 预先计算的总数据量
    count = 0  # 已爬取的数据量
    # 记录解析以及翻页位置
    location = 0
    click_times = 0
    def run(self):
        """
        开始爬虫
        :return:
        """
        # 打开网页
        self.ss.open("http://book.sina.com.cn/excerpt/rwws/")
        # 等待数据加载完成
        self.ss.wait_for_selector('#subShowContent1_static > div:nth-child(20)')
        self.parselist()
        while self.count < self.total:
            if self.click_times is 0:
                # 点击加载更多
                self.ss.click('#subShowContent1_loadMore')
                # 每次翻页,或加载更多,要等待至加载完成
                self.ss.wait_for_selector('#subShowContent1_static > div:nth-child(21)')
                self.click_times += 1
                self.parselist()
            elif self.click_times is 1:
                self.ss.click('#subShowContent1_loadMore')
                self.ss.wait_for_selector('#subShowContent1_static > div:nth-child(41)')
                self.click_times += 1
                self.parselist()
            elif self.click_times is 2:
                self.ss.click('#subShowContent1_page .pagebox_next a')
                self.ss.sleep(2)
                self.click_times = 0
                self.location = 0
                self.parselist()
    def parselist(self):
        """
        解析列表页
        :return:
        """
        html = self.ss.content.encode('utf8')
        # print html
        pattern = re.compile(r'<div class="item"><h4><a href="(.*?)" target="_blank">', re.M)
        links = pattern.findall(html)
        for i in range(self.location, len(links)):
            print links[i]
            self.count += 1
            self.location += 1
        print self.count
if __name__ == '__main__':
    spider = SinaBookSpider()
    spider.run()补充:
- ghost.py对直接获取元素支持的不是很好,但可以借助- BeautifulSoup或- 正则表达式来解决。
- ghost.py支持与网页的简单交互,如点击,填充表单等
- set_field_value(*args, **kwargs)
- fill(*args, **kwargs)
- click(*args, **kwargs)- ghost.py很好的解决了确定元素加载完成的问题,通过以下方法可以让爬虫等待,直到满足设置的条件。
- wait_for(condition, timeout_message, timeout=None)
- wait_for_page_loaded(timeout=None)
- wait_for_selector(selector, timeout=None)
- wait_for_text(text, timeout=None)
- wait_while_selector(selector, timeout=None)有关更多
ghost.py的方法请看这里:Ghost.py
文中有什么错误或不足之处,欢迎指出!
                
 
 
                     
            
        













 
                    

 
                 
                    