爬虫爬取动态网页的三种方式简介
0x00 前言
最近在看类似的问题的时候找了一些资料,发现网上有一篇文章写得很详细(准确的说是分成三篇文章写的),特别是手工逆向的方式还是挺有趣的,我也照着他的方式尝试了一下,学到一点东西,下面是这三篇文章的部分内容(有删改,外加其它的一些理解),如果想看原文的话,我在本文最后会附上原文的链接,至于目前最流行的使用 chrome headless 写动态爬虫的方法,由于原作者写的也不是很仔细,所以我还要再找些资料仔细研究一下,后面再写一篇文章总结。
0X01 动态网页简介:
在我们编写爬虫时,可能会碰到以下两种问题:
1.我们所需要爬取的数据在网页源代码中并不存在;
2.点击下一页跳转页面时,网页的 URL 并没有发生变化;
造成这种问题原因是,你所正在爬取的页面采取了 js 动态加载的方式,是一个动态网页。
所谓的动态网页,是指跟静态网页相对的一种网页编程技术。静态网页,随着html代码生成,页面的内容和显示效果就不会发生变化了。而动态网页则不然,其显示的页面则是经过Javascript处理数据后生成的结果,可以发生改变。这些数据的来源有多种,可能是经过Javascript计算生成的,也可能是通过Ajax加载的。
动态网页经常使用的一种技术是Ajax请求技术。
Ajax = Asynchronous JavaScript and XML(异步的 JavaScript 和XML),其最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页的内容。
目前,越来越多的网站采取的是这种动态加载网页的方式,一来是可以实现web开发的前后端分离,减少服务器直接渲染页面的压力;二来是可以作为反爬虫的一种手段。
0X02 动态网页抓取
(1)逆向回溯法
对于动态加载的网页,我们想要获取其网页数据,需要了解网页是如何加载数据的,该过程就被成为逆向回溯。
对于使用了Ajax 请求技术的网页,我们可以找到Ajax请求的具体链接,直接得到Ajax请求得到的数据。
需要注意的是,构造Ajax请求有两种方式:
1.原生的Ajax请求:会直接创建一个XMLHTTPRequest对象。
2.调用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请求。
(2)渲染动态网页法
1.浏览器渲染引擎:
(1)简介:
在介绍这种方式之前,我们需要首先了解一些浏览器渲染引擎的基本知识。
渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。浏览器向服务器发送请求,得到服务器返回的资源文件后,需要经过渲染引擎的处理,将资源文件显示在浏览器窗口中。
目前使用较为广泛的渲染引擎有两种:
webkit——使用者有Chrome, Safari
Geoko——使用者有Firefox
(2)渲染主流程:
渲染引擎首先通过网络获得所请求文档的内容,通常以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后端层绘制每个节点。
补充知识:
1.浏览器会解析三个东西:
(1) HTML/SVG/XHTML,解析这三种文件会产生一个 DOM Tree。
(2) CSS,解析 CSS 会产生 CSS 规则树(CSSOM)。
(3) Javascript脚本,主要是通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree.2.形象的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文件,重新渲染页面。
3.Javascript的加载和执行的特点:
(1)载入后马上执行;
(2)执行时会阻塞页面后续的内容(包括页面的渲染、其它资源的下载)。原因:因为浏览器需要一个稳定的DOM树结构,而JS中很有可能有代码直接改变了DOM树结构,比如使用 document.write 或appendChild,甚至是直接使用的location.href进行跳转,浏览器为了防止出现JS修改DOM树,需要重新构建DOM树的情况,所以就会阻塞其他的下载和呈现。
(3)思考:
了解了浏览器渲染引擎的基本原理,我们可以发现:
当浏览器渲染引擎完成了dom树以及render树的构建之后,树中就已经包含了我们在浏览器窗口中可以看到的所有数据。
那么我们就有了一种爬取动态网页的新思路:
在浏览器渲染引擎执行layout以及printing之前,得到dom树或者render树,从树中获取动态加载的数据。
2.渲染动态网页:
(1)有两种选择:
1.自己从头实现一个浏览器渲染引擎,在合适的时机返回构建的dom树或render树:这需要进行大量的工作,需要考虑html、js、css等不同格式文件的解析方式以及解析顺序等。
2.接下来将使用WebKit 渲染引擎,通过 PySide 这个python库可以获得该引擎的一个便捷接口。
由于相当于第一种方法来说,第二种方法稍微简单一些,于是这里以第二种为例
(2)示例:
还是以 新浪读书——书摘 为例,可以发现:页面中文章列表的部分是动态加载的。
使用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准备数据的时间。
对于这个问题有两种解决思路:
(1)等待固定的一段时间,比如time.sleep(3):这种方法容易实现,但效率较低。
(2)轮询网页,等待特定内容出现:这种方法虽然会在检查是否加载完成时浪费CPU周期,但更加可靠。
以下是一个简单的实现:
elem = None
while not elem:
app.processEvents()
elem = frame.findAllElemnets('#pattern')
代码循环,直到出现特定元素。每次循环,调用app.processEvents()方法,用于给Qt事件循环执行任务的时间,比如响应点击事件。
但是PySide毕竟是一个为了Python的GUI 编程而开发的, 其功能对于爬虫来说实在是太过于庞大,所以我们可以把爬虫经常使用的功能进行封装,来提升编写爬虫的效率。
(3)对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('#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()
2.代码地址:
https://github.com/linbo-lin/dynamic-web-process
3.补充:
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)
(3)模拟浏览器行为法
前面的例子中,我们使用WebKit库,可以自定义浏览器渲染引擎,这样就可以完全控制想要执行的行为。如果不需要那么高的灵活性,那么还有一个不错的替代品 Selenium 可以选择,它提供了使浏览器自动化的API 接口。
1.Selenium 简介:
Selenium 是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持市面上几乎所有的主流浏览器。
本来打算使用的是selenium + PhantomJS(由于内部 webkit 组件无人维护并且会出现各种各样的问题,所以作者也已经不再维护)的组合,但发现Chrome以及FireFox也相继推出无头 ( headless ) 浏览器模式,个人比较倾向Chrome。本文采用的是Selenium+Chrome的组合。
2.示例:
运用到爬虫中的思路是:
使用Selenium 渲染网页,解析渲染后的网页源码,或者直接通过Selenium 接口获取页面中的元素。
还是以 新浪读书——书摘 这个网站为例,目标是获取列表中每篇文章详情页的地址
示例代码:
# coding=utf-8
import time
from selenium import webdriver
class SinaBookSpider(object):
# 创建可见的Chrome浏览器, 方便调试
driver = webdriver.Chrome()
# 创建Chrome的无头浏览器
# opt = webdriver.ChromeOptions()
# opt.set_headless()
# driver = webdriver.Chrome(options=opt)
driver.implicitly_wait(10)
total = 1526 # 预先计算的总数据量
count = 0 # 已爬取的数据量
# 记录解析以及翻页位置
location = 0
click_times = 0
def run(self):
"""
开始爬虫
:return:
"""
# get方式打开网页
self.driver.get("http://book.sina.com.cn/excerpt/rwws/")
self.parselist()
while self.count < self.total:
if self.click_times is 2:
self.driver.find_element_by_css_selector('#subShowContent1_page > span:nth-child(6) > a').click()
# 等待页面加载完成
time.sleep(5)
self.click_times = 0
self.location = 0
else:
self.driver.find_element_by_css_selector('#subShowContent1_loadMore').click()
# 等待页面加载完成
time.sleep(3)
self.click_times += 1
# 分析加载的新内容,从location开始
self.parselist()
self.driver.quit()
def parselist(self):
"""
解析列表
:return:
"""
divs = self.driver.find_elements_by_class_name("item")
for i in range(self.location, len(divs)):
link = divs[i].find_element_by_tag_name('a').get_attribute("href")
print link
self.location += 1
self.count += 1
print self.count
if __name__ == '__main__':
spider = SinaBookSpider()
spider.run()
代码地址:https://github.com/linbo-lin/dynamic-web-process
如果你想实际运行上述代码,请在运行之前确定:安装了与浏览器版本对应的驱动,并正确的添加到了环境变量中。
3.使用selenium时同样要特别注意的是如何确定 网页是否加载完成
有三种方式:
(1)强制等待
(2)隐形等待
(3)显性等待
有关这三种方式的讲解可以看这里:Python selenium —— 一定要会用selenium的等待,三种等待方式解读 —— 灰蓝的博客
(4)总结:
到此,我们介绍了动态页面处理的一些思路:
1.逆向回溯 : 该方法属于手工方法,不适合自动检测
2.渲染动态页面 : 使用PySide或ghost.py,但是由于太过久远已经被时代淘汰了,所以这种方法并不优雅
3.selenium 模拟浏览器: 这种方法是现代大型爬虫最常使用的模式
0X03 参考链接
https://blog.csdn.net/ha_hha/article/details/80324343
https://blog.csdn.net/ha_hha/article/details/80324582
https://blog.csdn.net/Ha_hha/article/details/80324707
https://github.com/linbo-lin/dynamic-web-process
https://docs.seleniumhq.org/
https://ghost-py.readthedocs.io/en/latest/#
http://pyside.github.io/docs/pyside/
https://huilansame.github.io/huilansame.github.io/
https://blog.csdn.net/xiaozhuxmen/article/details/52014901
http://www.cnblogs.com/lhb25/p/how-browsers-work.html#Resources
http://book.sina.com.cn/excerpt/
https://blog.csdn.net/u010378313/article/details/51435992