一,js 逆向分析
对于动态加载的网页,我们想要获取其网页数据,需要了解网页是如何加载数据的,该过程就被成为逆向回溯。对于使用了Ajax 请求技术的网页,我们可以找到Ajax请求的具体链接,直接得到Ajax请求得到的数据。
需要注意的是,构造Ajax请求有两种方式:
- 原生的Ajax请求:会直接创建一个XMLHTTPRequest对象。
- 调用jQuery的ajax()方法:一般情况下,$.ajax()会返回其创建的XMLHTTPRequest对象;但是,如果$.ajax()的dataType参数指定了为script或jsonp类型,$.ajax()不再返回其创建的XMLHTTPRequest对象。
对于这两种方式,只要创建并返回了XMLHTTPRequest对象,就可以通过Chrome浏览器的调试工具在NetWork窗口设置过滤条件为 xhr ,直接筛选出Ajax请求的链接;如果是$.ajax()并且dataType指定了为script或jsonp(这种情况下NetWork 里面的 Type 都是 script,如果你懂得 jsonp 的原理的话就知道 jsonp 本质就是通过 script),则无法通过这种方式筛选出来(因为这两种方式是经典的跨域方法,而 XHR 是不能跨域的,所以设置 XHR 过滤)。
示例:
接下来以 新浪读书——书摘 为例,介绍如何得到无法筛选出来的Ajax请求链接:
在Chrome中打开网页,右键检查,会发现首页中书摘列表包含在一个id为subShowContent1_static的div中,而查看网页源代码会发现id为subShowContent1_static的div为空。
如图所示:
并且点击更多书摘或下一页时,网页URL并没有发生变化。
这与我们最前面所说的两种情况相同,说明这个网页就是使用 JS 动态加载数据的。
F12打开调试工具,打开NetWork窗口,F5刷新,可以看到浏览器发送以及接收到的数据记录(我们可以点击上面的 XHR 或者 JS 对这些请求进行过滤):
可以发现目前两种类型的请求都是存在的,暂时还不能判断我们 div 中内容的动态加载使用的是哪一种方式,不过没关系,我们可以进一步进行测试。
1.根据 id 进行查找
我们知道,js 操作页面的数据一定要进行定位,最常用的方法就是使用 id 定位,因为 id 在整个页面中是唯一的,那么我们第一步就是在所有的 js 文件中找和 subShowContent1_static 这个 id 相关的文件,于是我在 network 页面使用 ctrl+f 进行全局搜索
最终定位到了可能性最大的文件 feedlist.js
进入这个文件以后我就定位到了一个匿名函数 $(),这个函数将参数传入 Listmore() 函数
listmore() 函数调用了 Getmorelist() 函数
Getmorelist() 函数 调用了 getMore() 函数
getmore() 函数定义了我们的请求
2.设置断点进行动态捕获
可以看到这里使用的是 jsonp 的形式跨域传递数据的,然后 URL 是一个对象,是运行中生成的,我们可以在运行中对这个函数添加一个断点
然后 f5 刷新
断下来以后就能看到我们想要看到的 URL 以及后面跟着的参数了,这样就可以根据jQuery的ajax()用法构造正确的Ajax 请求链接:
http://feed.mix.sina.com.cn/api/roll/get?callback=xxxxxxxx&pageid=96&lid=560&num=20&page=1
那么这个 callback 是多少呢,我们现在还看不出来,但是,既然这个是一个请求,那么肯定会在 network 中有记录,我们找找看
我们现在就锁定了我们想要找的链接,得到Ajax请求链接之后,可以直接得到请求的数据,一般为json格式,处理后即可使用。
其实当你有了经验之后,对一些不是很复杂的网页,根本就不用进行这么复杂的逆向工程,凭URL形式可以很快的在NetWork窗口 选择-验证 出所需的Ajax请求。
二,使用WebKit 渲染引擎
渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。浏览器向服务器发送请求,得到服务器返回的资源文件后,需要经过渲染引擎的处理,将资源文件显示在浏览器窗口中。
目前使用较为广泛的渲染引擎有两种:
- webkit——使用者有Chrome, Safari
- Geoko——使用者有Firefox
渲染主流程:
渲染引擎首先通过网络获得所请求文档的内容,通常以8K分块的方式完成。
下面是渲染引擎在取得内容之后的基本流程:
解析html来构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树
- 渲染引擎开始解析html,并将标签转化为内容树中的dom节点。如果遇到JS,那么此时会启用另外的连接进行下载(下载过程中 dom
树的构建不会停止),并且在下载完成后立即执行(执行过程中会阻塞 浏览器的其他行为,因为 js 的运行可能会改变 dom
树的结构,为了不让刚刚构建好的 dom 树又被 js 改变,聪明的浏览器停止了 dom 树的构建)。 - 接着,它解析外部CSS文件及style标签中的样式信息。这些样式信息以及html中的可见性指令将被用来构建另一棵树——render树(其实这一步是和上一步同时进行的,为了页面显示更迅速,css
不会等到 dom 树构建完毕才开始构建 render树 )。 - Render树由一些包含有颜色和大小等属性的矩形组成,它们将被按照正确的顺序显示到屏幕上。
- Render树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标。
- 再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。
补充知识:
浏览器会解析三个东西:
- HTML/SVG/XHTML,解析这三种文件会产生一个 DOM Tree。
- CSS,解析 CSS 会产生 CSS 规则树(CSSOM)。
Javascript脚本,主要是通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree.
形象的HTML页面加载和解析流程:
- 用户输入网址(假设是个html页面,并且是第一次访问),浏览器向服务器发出请求,服务器返回html文件
- 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件;
- 浏览器又发出CSS文件的请求,服务器返回这个CSS文件;
- 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了;
- 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码;
- 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
- 浏览器发现了一个包含一行Javascript代码的<script>标签,赶快运行它;
- Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个<div> (style.display=”none”)。突然少了这么一个元素,浏览器不得不重新渲染这部分代码;
- 终于等到了</html>的到来,浏览器泪流满面……
- 等等,还没完,用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下<link>标签的CSS路径
- 浏览器召集了在座的各位<div><span><ul><li>们,“大伙儿收拾收拾行李,咱得重新来过……”,浏览器向服务器请求了新的CSS文件,重新渲染页面。
Javascript的加载和执行的特点:
- 载入后马上执行;
- 执行时会阻塞页面后续的内容(包括页面的渲染、其它资源的下载)。原因:因为浏览器需要一个稳定的DOM树结构,而JS中很有可能有代码直接改变了DOM树结构,比如使用 document.write或appendChild,甚至是直接使用的location.href进行跳转,浏览器为了防止出现JS修改DOM树,需要重新构建DOM树的情况,所以就会阻塞其他的下载和呈现。
当浏览器渲染引擎完成了dom树以及render树的构建之后,树中就已经包含了我们在浏览器窗口中可以看到的所有数据。所以爬虫可以在浏览器渲染引擎执行layout以及printing之前,得到dom树或者render树,从树中获取动态加载的数据。
示例:
接下来将使用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毕竟是一个为了Python的GUI 编程而开发的, 其功能对于爬虫来说实在是太过于庞大,所以我们可以把爬虫经常使用的功能进行封装,来提升编写爬虫的效率。
对PySide 常用功能的封装 —— ghost.py
ghost.py 是目前一个针对爬虫且功能比较完善的PySide的封装模块,使用它可以很方便的进行数据采集。
还是以获取列表页中每篇文章详情页地址为目标,
1.示例代码:
# 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(