爬虫实战1.4.1 Ajax数据采集-微博博客采集

不知道大家有没有遇到这种情况:当我们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选项时是没有内容的,然后鼠标滚轮往下滚,直到出现第一条请求为止,见下图:
Ajax请求

接下来我们→_→,看一下他的一些选项,首先Headers
Headers

这里包含了请求的地址,请求的方式是get请求,请求的code是200表示成功,下面是请求头,返回头,还有请求的参数,可以说这里包含了一个请求所有的内容了,先不看具体字段的意思。
这时候一个请求可能看不出什么,可以继续往下滚动滚轮,直到最下面,这里出现了分页, 就先不管了:
底部

我们再看下请求,这里有多出现了一条,下面就根据这两条Ajax请求来分析一下:
Ajax请求

刚才已经了解了Headers了,下面看下Preview跟Response,两者都是响应的信息,只是Preview是标准的Json格式的,好看一点:
Preview响应

这里就比较清楚了,返回了三个参数:code, data, msg,genuine意思我们就可以猜测:data中就是我们
需要的数据了,把鼠标移到Show more(400KB)上,发现是一个HTML的代码块,看起来不是很清晰。
可以通过右边的copy,把这段复制出来,然后在编辑器中新建一个html,粘贴到这里面,ctrl+alt+L整洁下代码,呈现一下,看进度条还是挺多内容的:
获取到的data

点击右上角的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这个属性。

加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块

通过左边的 + - 符号可以很清晰的展现出每一篇博客的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"]')

看下结果:

[, , , , , , , , , , , , , , ]

然后再遍历解析每篇博客,获取我们所需要的数据:

        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异步加载数据采集的印象。

你可能感兴趣的:(爬虫实战1.4.1 Ajax数据采集-微博博客采集)