不知道大家有没有遇到这种情况:当我们requests发出请求采集页面信息的时候,得到的结果肯能会跟在浏览器中看到的不一样,在浏览器中看到的数据,使用requests请求时可能会没有。
1.前言
上面这种情况的原因就是requests获取的都是静态的HTML文档内容,而浏览器中看到的页面,其中的部分数据可能是JavaScript处理后生成的数据,这种数据也有很多种生成方式:有Ajax加载生成的,也有经过JavaScript和一定的计算方式生成的。
那对于Ajax,这里简单介绍一下:Ajax是一种异步数据加载方式,就是原始的页面生成之后,开始不会包含这部分数据,之后会通过再次向服务器端请求某个接口获取,然后再经过一定处理显示再页面上。
所以,我们以后遇到这种页面时,我们直接发送requests请求是无法获取到一些数据的,这时候我们就需要找到这部分数据的源头:也就是这个Ajax请求,在进行模拟,就可以成功获取到数据了,比如我们最开始实现的例子:爬虫开发实战1.1 解决JS加密。没有看的或者不记得的,可以返回去仔细的看一下。
这篇主要是通过一个小例子来了解一下Ajax以及如何去解析采集这类的数据。
至于什么是Ajax,如果需要了解其原理的话,可以去W3School上看下几个示例
http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp
或者去崔老师的博客
[Python3网络爬虫开发实战] 6.1-什么是Ajax
2.Ajax分析
我们先去找个博主。作为吃货大军中的一员,果断去了美食栏目,就拿第一个博主为例吧,名字也很接地气啊:365道菜:https://weibo.com/u/1558473534?refer_flag=1087030701_2975_2023_0&is_hot=1
先看下他的主页,这不是在打广告哈。。。
右键检查,弹出开发者工具界面,我们打开Network选项,然后重新刷新页面,就可以看到目前所有请求返回之后渲染HTML的信息了。
然后我们选择Ajax相关的请求,对应的请求类型是
XHR
,这里注意一下:刚开始选择XHR
选项时是没有内容的,然后鼠标滚轮往下滚,直到出现第一条请求为止,见下图:接下来我们→_→,看一下他的一些选项,首先Headers
这里包含了请求的地址,请求的方式是get请求,请求的code是200表示成功,下面是请求头,返回头,还有请求的参数,可以说这里包含了一个请求所有的内容了,先不看具体字段的意思。
这时候一个请求可能看不出什么,可以继续往下滚动滚轮,直到最下面,这里出现了分页, 就先不管了:
我们再看下请求,这里有多出现了一条,下面就根据这两条Ajax请求来分析一下:
刚才已经了解了Headers了,下面看下Preview跟Response,两者都是响应的信息,只是Preview是标准的Json格式的,好看一点:
这里就比较清楚了,返回了三个参数:
code
, data
, msg
,genuine意思我们就可以猜测:data
中就是我们需要的数据了,把鼠标移到
Show more(400KB)
上,发现是一个HTML的代码块,看起来不是很清晰。可以通过右边的copy,把这段复制出来,然后在编辑器中新建一个html,粘贴到这里面,
ctrl+alt+L
整洁下代码,呈现一下,看进度条还是挺多内容的:点击右上角的google浏览器,看一下页面:
样式没有渲染出来,但是我们根据图片可以在原页面上找出对应的内容:
大致的数了一下,总共十五篇的信息。
下面的Ajax请求,响应的数据跟这个是一样的分析方式,这里就不再多说了。
3.Ajax数据采集
Ajax的分析已经完成了,下面就是开始进行采集了,首先先把基本架子写好:
import requests
class WeiboSpider(object):
def __init__(self):
self._headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'weibo.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
}
def run(self):
pass
if __name__ == '__main__':
wb_spider = WeiboSpider()
wb_spider.run()
这里注意一点:现在微博采集数据是要在请求头中带上cookie的,所以在
self._headers
中还要加上cookie
这个属性。
现在来看一下请求参数是什么,因为目前一页中就只有两次ajax数据加载,所以我们可以看一下他们的共性:
通过对比两次的请求参数,不难发现,其中的
pagebar
跟__rnd
两个参数会有些变化,pagebar
这个比较简单,就分0, 1。 __rnd
参数发现:这个就是个时间戳,不过python中的时间戳是10位,而且是小数,这里的是13位,这样可以自己去测试一下:当前时间的时间戳再拼接上3位的随机数:
def get_response(self, req_url, params_dict=None):
if params_dict:
response = requests.get(req_url, params=params_dict, headers=self._headers)
else:
response = requests.get(req_url, headers=self._headers)
if response.status_code == 200:
return response.content.decode('utf-8')
return None
def run(self, pagebar, rnd):
params_dict = {
"ajwvr": 6,
"domain": 100505,
"refer_flag": "1087030701_2975_2023_0",
"is_hot": 1,
"pagebar": pagebar,
"pl_name": "Pl_Official_MyProfileFeed__20",
"id": 1005051558473534,
"script_uri": "/u/1558473534",
"feed_type": 0,
"page": 1,
"pre_page": 1,
"domain_op": 100505,
"__rnd": rnd,
}
start_url = "https://weibo.com/p/aj/v6/mblog/mbloglist"
response = self.get_response(start_url, params_dict)
print(response)
if __name__ == '__main__':
wb_spider = WeiboSpider()
dtime = datetime.datetime.now()
un_time = time.mktime(dtime.timetuple())
rnd = int(f'{int(un_time)}{rd.randint(100, 999)}')
其实params_dict
中的部分参数可能也是不需要的,这里想去实验的可以去尝试一下
现在请求已经完成了,看下打印的结果,由于内容较多,就贴个图:
接下来就是对获取到的内容进行分析,拿到我们想要的数据了,这里就随便取几个数据了:博主、博主头像、时间、文字内容、图片内容、评论数、点赞数。
之前的几篇文中已经实际应用了一些解析的用法了,这里就不仔细写了,大概写一下思路吧:
首先我们要取的是一篇一篇的博客内容,上面的内容可能是一篇文字内容对应多个图片,所以在解析的时候需要对应起来,我们看下之前复制出来的Html块:
通过左边的 + -
符号可以很清晰的展现出每一篇博客的html块,每一块是由一个div
组成,这样我们可以先取div块,然后再从每个div块中再获取我们所需要的数据,可以这样处理:
首先获取每篇博客的div块,也就是博客列表,列表中是每篇博客div块的Element对象
# 相应信息中获取加载的数据信息
data_dict = json.loads(response)
html_content = etree.HTML(data_dict['data']) # 转为Element对象
# 获取每篇博客的div块
blog_list = html_content.xpath('//div[@action-type="feed_list_item"]')
看下结果:
[<Element div at 0x39801c8>, <Element div at 0x3980b48>, <Element div at 0x3980bc8>, <Element div at 0x3980b88>, <Element div at 0x3980c48>, <Element div at 0x3980d08>, <Element div at 0x3980cc8>, <Element div at 0x3980d48>, <Element div at 0x3980d88>, <Element div at 0x3980c08>, <Element div at 0x3980dc8>, <Element div at 0x3980e08>, <Element div at 0x3980e48>, <Element div at 0x395a988>, <Element div at 0x395aa08>]
然后再遍历解析每篇博客,获取我们所需要的数据:
for blog in blog_list:
blog_item = dict()
# 博主头像
blog_item['blogger_photo'] = blog.xpath('descendant::img[@class="W_face_radius"]/@src')[0]
# 博主昵称:这个信息有很多地方都出现了,可以选择一个较好取值的,我选的是跟微博内容在一个地方的,用nick-name属性表示
blog_item['blogger_name'] = blog.xpath('descendant::div[contains(@class, "WB_text")]/@nick-name')[0]
# 博客时间
blog_item['blog_time'] = blog.xpath('descendant::a[@node-type="feed_list_item_date"]/@title')[0]
# 博客文字内容, 这里注意的是有个 \u200b 字符,,这是个0长度的比较特殊的字符,编码可能转不过来,所以做个简单替换处理
blog_item['blog_content'] = blog.xpath('descendant::div[contains(@class, "WB_text")]/text()')[0].strip().replace('\u200b', '')
# 博客图片内容
blog_item['blog_picture_list'] = blog.xpath('descendant::ul[@node-type="fl_pic_list"]//li/img/@src')
# 评论数
blog_item['blog_comment'] = blog.xpath('descendant::span[@node-type="comment_btn_text"]//em[last()]/text()')[0]
# 点赞数, 在这里有个处理:当没有点赞的时候会显示出一个 “赞” 字, 所以当是 “赞” 的时候点赞数是 0
blog_likestar = blog.xpath('descendant::span[@node-type="like_status"]//em[last()]/text()')[0]
blog_item['blog_likestar'] = '0' if blog_likestar == '赞' else blog_likestar
yield blog_item
贴一下主要实现代码:
def get_response(self, req_url, params_dict=None):
"""
请求
:param req_url:
:param params_dict:
:return:
"""
if params_dict:
response = requests.get(req_url, params=params_dict, headers=self._headers)
else:
response = requests.get(req_url, headers=self._headers)
if response.status_code == 200:
return response.content.decode('utf-8')
return None
def run(self, pagebar, rnd):
"""
主函数
:param pagebar:
:param rnd:
:return:
"""
params_dict = {
"ajwvr": 6,
"domain": 100505,
"refer_flag": "1087030701_2975_2023_0",
"is_hot": 1,
"pagebar": pagebar,
"pl_name": "Pl_Official_MyProfileFeed__20",
"id": 1005051558473534,
"script_uri": "/u/1558473534",
"feed_type": 0,
"page": 1,
"pre_page": 1,
"domain_op": 100505,
"__rnd": rnd,
}
start_url = "https://weibo.com/p/aj/v6/mblog/mbloglist"
# 1.发出请求,获取响应
response = self.get_response(start_url, params_dict)
# 2.数据解析
blog_content = self.get_blog_list(response)
# 3.输出采集到的内容, 想存储的可自选存储方式
for blog in blog_content:
print(blog)
def get_blog_list(self, response):
"""
获取博客列表
:param response:
:return:
"""
# 相应信息中获取加载的数据信息
data_dict = json.loads(response)
html_content = etree.HTML(data_dict['data']) # 转为Element对象
# 获取每篇博客的div块
blog_list = html_content.xpath('//div[@action-type="feed_list_item"]')
# 遍历解析每篇博客内容
blog_content = self.data_parse(blog_list)
return blog_content
def data_parse(self, blog_list):
"""
解析每篇博客内容
:param response:
:return:
"""
for blog in blog_list:
blog_item = dict()
# 博主头像
blog_item['blogger_photo'] = blog.xpath('descendant::img[@class="W_face_radius"]/@src')[0]
# 博主昵称:这个信息有很多地方都出现了,可以选择一个较好取值的,我选的是跟微博内容在一个地方的,用nick-name属性表示
blog_item['blogger_name'] = blog.xpath('descendant::div[contains(@class, "WB_text")]/@nick-name')[0]
# 博客时间
blog_item['blog_time'] = blog.xpath('descendant::a[@node-type="feed_list_item_date"]/@title')[0]
# 博客文字内容, 这里注意的是有个 \u200b 字符,,这是个0长度的比较特殊的字符,编码可能转不过来,所以做个简单替换处理
blog_item['blog_content'] = blog.xpath('descendant::div[contains(@class, "WB_text")]/text()')[0].strip().replace('\u200b', '')
# 博客图片内容
blog_item['blog_picture_list'] = blog.xpath('descendant::ul[@node-type="fl_pic_list"]//li/img/@src')
# 评论数
blog_item['blog_comment'] = blog.xpath('descendant::span[@node-type="comment_btn_text"]//em[last()]/text()')[0]
# 点赞数, 在这里有个处理:当没有点赞的时候会显示出一个 “赞” 字, 所以当是 “赞” 的时候点赞数是 0
blog_likestar = blog.xpath('descendant::span[@node-type="like_status"]//em[last()]/text()')[0]
blog_item['blog_likestar'] = '0' if blog_likestar == '赞' else blog_likestar
yield blog_item
再贴一下main:
if __name__ == '__main__':
wb_spider = WeiboSpider()
dtime = datetime.datetime.now()
un_time = time.mktime(dtime.timetuple())
rnd = int(f'{int(un_time)}{rd.randint(100, 999)}')
for i in range(2):
print(f'{"=" * 30}第 {i + 1} 次数据加载')
wb_spider.run(i, rnd)
time.sleep(10)
看一下最终打印结果,由于数据较多,这里贴个图:
4.结语
虽然看起来篇幅很长,其实也是挺简单基础的一个采集小实例,就是简单说了下Ajax异步加载数据获取的方式跟分析的简单步骤,如果大家有更好的方法可以留言一起交流。下一篇再用一个完整的实例来加深对Ajax异步加载数据采集的印象。