Python3网络爬虫实战35-Ajax数据爬取

29次阅读

共计 9890 个字符,预计需要花费 25 分钟才能阅读完成。

上一篇文章:Python3 网络爬虫实战 —34、数据存储:非关系型数据库存储:Redis
下一篇文章:

有时候我们在用 Requests 抓取页面的时候,得到的结果可能和在浏览器中看到的是不一样的,在浏览器中可以看到正常显示的页面数据,但是使用 Requests 得到的结果并没有,这其中的原因是 Requests 获取的都是原始的 HTML 文档,而浏览器中的页面则是页面又经过 JavaScript 处理数据后生成的结果,这些数据的来源有多种,可能是通过 Ajax 加载的,可能是包含在了 HTML 文档中的,也可能是经过 JavaScript 经过特定算法计算后生成的。

对于第一种情况,数据的加载是一种异步加载方式,原始的页面最初不会包含某些数据,原始页面加载完后会会再向服务器请求某个接口获取数据,然后数据再被处理才呈现到网页上,这其实就是发送了一个 Ajax 请求。

照 Web 发展的趋势来看,这种形式的页面越来越多,网页原始 HTML 文档不会包含任何数据,数据都是通过 Ajax 来统一加载然后再呈现出来,这样在 Web 开发上可以做到前后端分离,而且降低服务器直接渲染页面带来的压力。

所以如果我们遇到这样的页面,如果我们再利用 Requests 等库来抓取原始页面是无法获取到有效数据的,这时我们需要做的就是分析网页的后台向接口发送的 Ajax 请求,如果我们可以用 Requests 来模拟 Ajax 请求,那就可以成功抓取了。

所以本章我们的主要目的是了解什么是 Ajax 以及如何去分析和抓取 Ajax 请求。

1、什么是 Ajax

Ajax,全称为 Asynchronous JavaScript and XML,即异步的 JavaScript 和 XML。
Ajax 不是一门编程语言,而是利用 JavaScript 在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。
对于传统的网页,如果想更新其内容,那么必须要刷新整个页面,但有了 Ajax,我们便可以实现在页面不被全部刷新的情况下更新其内容。在这个过程中,页面实际是在后台与服务器进行了数据交互,获取到数据之后,再利用 JavaScript 改变网页,这样网页内容就会更新了。
可以到 W3School 上体验几个 Demo 来感受一下:http://www.w3school.com.cn/aj…。

1. 实例引入

我们在浏览网页的时候会发现很多网页都有上滑查看更多的选项,比如拿微博来说,我们以马云的主页为例:https://m.weibo.cn/u/2145291155,切换到微博页面,一直下滑,可以发现下滑几个微博之后,再向下就没有了,转而会出现一个加载的动画,不一会儿下方就继续出现了新的微博内容,那么这个过程其实就是 Ajax 加载的过程,如图 6-1 所示:

evernotecid://D603D29C-DFBA-4C04-85E9-CCA3C33763F6/appyinxiangcom/23852268/ENResource/p142

图 6-1 页面加载过程
我们注意到页面其实并没有整个刷新,也就意味着页面的链接是没有变化的,但是这个过程网页中却又多了新的内容,也就是后面刷出来的新的微博。这就是通过 Ajax 获取新的数据并呈现而实现的过程。

2. 基本原理

初步了解了 Ajax 之后我们再来详细了解一下它的基本原理,发送 Ajax 请求到网页更新的这个过程可以简单分为三步:
发送请求解析内容渲染网页下面我们分别来详细介绍一下这几个过程。

发送请求

我们知道 JavaScript 可以实现页面的各种交互功能,那么 Ajax 也不例外,它也是由 JavaScript 来实现的,实际它就是执行了类似如下的代码:

var xmlhttp;
if (window.XMLHttpRequest) {
    // code for IE7+, Firefox, Chrome, Opera, Safari
    xmlhttp=new XMLHttpRequest();} else {// code for IE6, IE5
    xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function() {if (xmlhttp.readyState==4 && xmlhttp.status==200) {document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
    }
}
xmlhttp.open("POST","/ajax/",true);
xmlhttp.send();

这是 JavaScript 对 Ajax 最底层的实现,实际上就是新建了 XMLHttpRequest 对象,然后调用了 onreadystatechange 属性设置了监听,然后调用 open() 和 send() 方法向某个链接也就是服务器发送了一个请求,我们在前面用 Python 实现请求发送之后是可以得到响应结果的,只不过在这里请求的发送变成了 JavaScript 来完成,由于设置了监听,所以当服务器返回响应时,onreadystatechange 对应的方法便会被触发,然后在这个方法里面解析响应内容即可。

解析内容

得到响应之后,onreadystatechange 属性对应的方法便会被触发,此时利用 xmlhttp 的 responseText 属性便可以取到响应的内容。这也就是类似于 Python 中利用 Requests 向服务器发起了一个请求,然后得到响应的过程。那么返回内容可能是 HTML,可能是 Json,接下来只需要在方法中用 JavaScript 进一步处理即可。比如如果是 Json 的话,可以进行解析和转化。

渲染网页

JavaScript 有改变网页内容的能力,解析完响应内容之后,就可以调用 JavaScript 来针对解析完的内容对网页进行下一步的处理了。比如通过 document.getElementById().innerHTML 这样的操作便可以对某个元素内的源代码进行更改,这样网页显示的内容就改变了,这样的操作也被称作 DOM 操作,即对 Document 网页文档进行操作,如更改、删除等。
如上例中,document.getElementById(“myDiv”).innerHTML=xmlhttp.responseText 便将 ID 为 myDiv 的节点内部的 HTML 代码更改为服务器返回的内容,这样 myDiv 元素内部便会呈现出服务器返回的新数据,网页的部分内容看上去就更新了。
以上就是 Ajax 的三个步骤。
我们观察到,以上的步骤其实都是由 JavaScript 来完成的,它完成了整个请求、解析、渲染的过程。
所以再回想微博的下拉刷新,这其实就是 JavaScript 向服务器发送了一个 Ajax 请求,然后获取新的微博数据,将其解析,并将其渲染在网页中。

3. 结语

因此我们可以知道,真实的数据其实都是一次次 Ajax 请求得到的,我们如果想要抓取这些数据,就需要知道这些请求到底是怎么发送的,发往哪里,发了哪些参数。如果我们知道了这些,不就可以用 Python 来模拟这个发送操作,获取到这其中的结果了吗?
所以在下一节我们就来了解下到哪可以看到这些后台 Ajax 操作,去了解它到底是怎么发送的,发送了什么参数。

2 Ajax 分析方法

还是以上文中的微博为例,我们已经知道了拖动刷新的内容是由 Ajax 加载的,而且页面的 URL 没有变化,那么我们应该到哪去查看这些 Ajax 请求呢?

1. 查看请求

在这里我们还是需要借助于浏览器的开发者工具,我们以 Chrome 浏览器为例来看一下怎样操作。
首先用 Chrome 浏览器打开微博的链接:https://m.weibo.cn/u/2145291155,随后在页面中点击鼠标右键,会出现一个检查的选项,点击它便会弹出开发者工具,如图 6-2 所示:

图 6-2 开发者工具
那么在 Elements 选项卡便会观察到网页的源代码,右侧便是节点的样式。
不过这不是我们想要寻找的内容,我们切换到 Network 选项卡,随后重新刷新页面,可以发现在这里出现了非常多的条目,如图 6-3 所示:

图 6-3 Network 面板结果
前文我们也提到过,这里其实就是在页面加载过程中浏览器与服务器之间发送 Request 和接收 Response 的所有记录。
Ajax 其实有其特殊的请求类型,它叫做 xhr,在上图中我们可以发现一个名称为 getIndex 开头的请求,其 Type 为 xhr,这就是一个 Ajax 请求,我们鼠标点击这个请求,可以查看这个请求的详细信息,如图 6-4 所示:

图 6-4 详细信息
我们在右侧可以观察到其 Request Headers、URL 和 Response Headers 等信息,如图 6-5 所示:

图 6-5 详细信息
其中 Request Headers 中有一个信息为 X-Requested-With:XMLHttpRequest,这就标记了此请求是 Ajax 请求。
随后我们点击一下 Preview,即可看到响应的内容,响应内容是 Json 格式,在这里 Chrome 为我们自动做了解析,我们可以点击箭头来展开和收起相应内容,如图 6-6 所示:

图 6-6 Json 结果
观察可以发现,这里的返回结果是马云的个人信息,如昵称、简介、头像等等,这也就是用来渲染个人主页所使用的数据,JavaScript 接收到这些数据之后,再执行相应的渲染方法,整个页面就被渲染出来了。
另外也可以切换到 Response 选项卡,可以观察到真实的返回数据,如图 6-7 所示:

图 6-7 Response 内容
接下来我们切回到第一个请求,观察一下它的 Response 是什么,如图 6-8 所示:

图 6-8 Response 内容
这是最原始的链接 https://m.weibo.cn/u/2145291155 返回的结果,其代码只有五十行,结构也非常简单,只是执行了一些 JavaScript。
所以说,我们所看到的微博页面的真实数据并不是最原始的页面返回的,而是后来执行 JavaScript 后再次向后台发送了 Ajax 请求,拿到数据后再进一步渲染出来的。

2. 过滤请求

接下来我们再利用 Chrome 开发者工具的筛选功能筛选出所有的 Ajax 请求,在请求的上方有一层筛选栏,我们可以点击 XHR,这样在下方显示的所有请求便都是 Ajax 请求了,如图 6-9 所示:

图 6-9 Ajax 请求
再接下来我们我们不断滑动页面,可以看到在页面底部有一条条新的微博被刷出,而开发者工具下方也一个个地出现 Ajax 请求,这样我们就可以捕获到所有的 Ajax 请求了。
随意点开一个条目都可以清楚地看到其 Request URL、Request Headers、Response Headers、Response Body 等内容,想要模拟请求和提取就非常简单了。
如图所示内容便是马云某一页微博的列表信息,如图 6-10 所示:

图 6-10 微博列表信息

3. 结语

到现在为止我们已经可以分析出来 Ajax 请求的一些详细信息了,接下来我们只需要用程序来模拟这些 Ajax 请求就可以轻松提取我们所需要的信息了。
所以在下一节我们来用 Python 实现 Ajax 请求的模拟,从而实现数据的抓取。

3 Ajax 结果提取

仍然是拿微博为例,我们接下来用 Python 来模拟这些 Ajax 请求,把马云发过的微博爬取下来。

1. 分析请求

我们打开 Ajax 的 XHR 过滤器,然后一直滑动页面加载新的微博内容,可以看到会不断有 Ajax 请求发出。
我们选定其中一个请求来分析一下它的参数信息,点击该请求进入详情页面,如图 6-11 所示:

图 6-11 详情页面
可以发现这是一个 GET 类型的请求,请求链接为:https://m.weibo.cn/api/contai…;value=2145291155&containerid=1076032145291155&page=2,请求的参数有四个:type、value、containerid、page。
随后我们再看一下其他的请求,观察一下这些请求,发现它们的 type、value、containerid 始终如一。type 始终为 uid,value 的值就是页面的链接中的数字,其实这就是用户的 id,另外还有一个 containerid,经过观察发现它就是 107603 然后加上用户 id。所以改变的值就是 page,很明显这个参数就是用来控制分页的,page=1 代表第一页,page=2 代表第二页,以此类推。
以上的推断过程可以实际观察参数的规律即可得出。

2. 分析响应

随后我们观察一下这个请求的响应内容,如图 6-12 所示:

图 6-12 响应内容
它是一个 Json 格式,浏览器开发者工具自动为做了解析方便我们查看,可以看到最关键的两部分信息就是 cardlistInfo 和 cards,将二者展开,cardlistInfo 里面包含了一个比较重要的信息就是 total,经过观察后发现其实它是微博的总数量,我们可以根据这个数字来估算出分页的数目。
cards 则是一个列表,它包含了 10 个元素,我们展开其中一个来看一下,如图 6-13 所示:

图 6-13 列表内容
发现它又有一个比较重要的字段,叫做 mblog,继续把它展开,发现它包含的正是微博的一些信息。比如 attitudes_count 赞数目、comments_count 评论数目、reposts_count 转发数目、created_at 发布时间、text 微博正文等等,得来全不费功夫,而且都是一些格式化的内容,所以我们提取信息也更加方便了。
这样我们可以请求一个接口就得到 10 条微博,而且请求的时候只需要改变 page 参数即可,目前总共 138 条微博那么只需要请求 14 次即可,也就是 page 最大可以设置为 14。
这样我们只需要简单做一个循环就可以获取到所有的微博了。

3. 实战演练

在这里我们就开始用程序来模拟这些 Ajax 请求,将马云的所有微博全部爬取下来。
首先我们定义一个方法,来获取每次请求的结果,在请求时 page 是一个可变参数,所以我们将它作为方法的参数传递进来,代码如下:

from urllib.parse import urlencode
import requests
base_url = 'https://m.weibo.cn/api/container/getIndex?'

headers = {
    'Host': 'm.weibo.cn',
    'Referer': 'https://m.weibo.cn/u/2145291155',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
    'X-Requested-With': 'XMLHttpRequest',
}

def get_page(page):
    params = {
        'type': 'uid',
        'value': '2145291155',
        'containerid': '1076032145291155',
        'page': page
    }
    url = base_url + urlencode(params)
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()
    except requests.ConnectionError as e:
        print('Error', e.args)

首先在这里我们定义了一个 base_url 来表示请求的 URL 的前半部分,接下来构造了一个参数字典,其中 type、value、containerid 是固定的参数,只有 page 是可变参数,接下来我们调用了 urlencode() 方法将参数转化为 URL 的 GET 请求参数,即类似于 type=uid&value=2145291155&containerid=1076032145291155&page=2 这样的形式,随后 base_url 与参数拼合形成一个新的 URL,然后我们用 Requests 请求这个链接,加入 headers 参数,然后判断响应的状态码,如果是 200,则直接调用 json() 方法将内容解析为 Json 返回,否则不返回任何信息,如果出现异常则捕获并输出其异常信息。
随后我们需要定义一个解析方法,用来从结果中提取我们想要的信息,比如我们这次想保存微博的 id、正文、赞数、评论数、转发数这几个内容,那可以先将 cards 遍历,然后获取 mblog 中的各个信息,赋值为一个新的字典返回即可。

from pyquery import PyQuery as pq

def parse_page(json):
    if json:
        items = json.get('cards')
        for item in items:
            item = item.get('mblog')
            weibo = {}
            weibo['id'] = item.get('id')
            weibo['text'] = pq(item.get('text')).text()
            weibo['attitudes'] = item.get('attitudes_count')
            weibo['comments'] = item.get('comments_count')
            weibo['reposts'] = item.get('reposts_count')
            yield weibo

在这里我们借助于 PyQuery 将正文中的 HTML 标签去除掉。
最后我们遍历一下 page,一共 14 页,将提取到的结果打印输出即可。

if __name__ == '__main__':
    for page in range(1, 15):
        json = get_page(page)
        results = parse_page(json)
        for result in results:
            print(result)

另外我们还可以加一个方法将结果保存到 MongoDB 数据库。

from pymongo import MongoClient

client = MongoClient()
db = client['weibo']
collection = db['weibo']

def save_to_mongo(result):
    if collection.insert(result):
        print('Saved to Mongo')

最后整理一下,最后的程序如下:

import requests
from urllib.parse import urlencode
from pyquery import PyQuery as pq
from pymongo import MongoClient

base_url = 'https://m.weibo.cn/api/container/getIndex?'
headers = {
    'Host': 'm.weibo.cn',
    'Referer': 'https://m.weibo.cn/u/2145291155',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
    'X-Requested-With': 'XMLHttpRequest',
}
client = MongoClient()
db = client['weibo']
collection = db['weibo']
max_page = 14


def get_page(page):
    params = {
        'type': 'uid',
        'value': '2145291155',
        'containerid': '1076032145291155',
        'page': page
    }
    url = base_url + urlencode(params)
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()
    except requests.ConnectionError as e:
        print('Error', e.args)


def parse_page(json):
    if json:
        items = json.get('cards')
        for item in items:
            item = item.get('mblog')
            weibo = {}
            weibo['id'] = item.get('id')
            weibo['text'] = pq(item.get('text')).text()
            weibo['attitudes'] = item.get('attitudes_count')
            weibo['comments'] = item.get('comments_count')
            weibo['reposts'] = item.get('reposts_count')
            yield weibo


def save_to_mongo(result):
    if collection.insert(result):
        print('Saved to Mongo')


if __name__ == '__main__':
    for page in range(1, max_page + 1):
        json = get_page(page)
        results = parse_page(json)
        for result in results:
            print(result)
            save_to_mongo(result)

运行程序后样例输出结果如下:

{'id': '3938863363932540', 'text': '我们也许不能解决所有的问题,但我们可以尽自己的力量去解决一些问题。移动互联网不能只是让留守孩子多了一个隔空说话的手机,移动互联网是要让父母和孩子一直在一起。过年了,回家吧…… 农村淘宝 2016 团圆贺岁片《福与李》', 'attitudes': 21785, 'comments': 40232, 'reposts': 2561}
Saved to Mongo
{'id': '3932968885900763', 'text': '跟来自陕甘宁云贵川六省的 100 位优秀乡村教师共度了难忘的两天,接下来我又得出远门了。。。为了 4000 万就读于乡村学校的孩子,所以有了这么一群坚毅可爱的老师,有了这么多关注乡村教育的各界人士,这两天感动、欣喜、振奋!我们在各自的领域里,一直坚持和努力吧!', 'attitudes': 32057, 'comments': 7916, 'reposts': 2332}
Saved to Mongo

查看一下 MongoDB,相应的数据也被保存到 MongoDB,如图 6-14 所示:

图 6-14 保存结果

4. 本节代码

本节代码地址:https://github.com/oldmarkfac…

5. 结语

本节实例的目的是为了演示 Ajax 的模拟请求过程,爬取的结果不是重点,该程序仍有很多可以完善的地方,如页码的动态计算、微博查看全文等,如感兴趣可以尝试一下。
通过这个实例我们主要是为了学会怎样去分析 Ajax 请求,怎样用程序来模拟抓取 Ajax 请求,了解了相关抓取原理之后,下一节的 Ajax 实战演练会更加得心应手。

正文完
 0