爬虫爬取动态网页的三种方式简介
0x00 前言
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 进行查找
最终定位到了可能性最大的文件 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页面加载和解析流程:
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时同样要特别注意的是如何确定 网页是否加载完成