内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

爬虫爬取动态网页的3种方式简介

2019-03-06 11:16 出处:清屏网 人气: 评论(0

最近在看类似的问题的时候找了一些资料,发现网上有一篇文章写得很详细(准确的说是分成三篇文章写的),特别是手工逆向的方式还是挺有趣的,我也照着他的方式尝试了一下,学到一点东西,下面是这三篇文章的部分内容(有删改,外加其它的一些理解),如果想看原文的话,我在本文最后会附上原文的链接,至于目前最流行的使用 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#=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页面加载和解析流程:

  1. 用户输入网址(假设是个html页面,并且是第一次访问),浏览器向服务器发出请求,服务器返回html文件
  2. 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件;
  3. 浏览器又发出CSS文件的请求,服务器返回这个CSS文件;
  4. 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了;
  5. 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码;
  6. 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
  7. 浏览器发现了一个包含一行Javascript代码的<script>标签,赶快运行它;
  8. Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个<div> (style.display=”none”)。突然少了这么一个元素,浏览器不得不重新渲染这部分代码;
  9. 终于等到了</html>的到来,浏览器泪流满面……
  10. 等等,还没完,用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下<link>标签的CSS路径
  11. 浏览器召集了在座的各位<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 模拟浏览器:这种方法是现代大型爬虫最常使用的模式


分享给小伙伴们:
本文标签: 爬虫

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.