爬虫实战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"]')

看下结果:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容

  • 前端开发面试题 面试题目: 根据你的等级和职位的变化,入门级到专家级,广度和深度都会有所增加。 题目类型: 理论知...
    怡宝丶阅读 2,567评论 0 7
  • Yahoo!的Exceptional Performance团队为改善Web性能带来最佳实践。他们为此进行了一系列...
    拉风的老衲阅读 1,829评论 0 1
  • 点我查看本文集的说明及目录。 本项目相关内容( github传送 )包括: 实现过程: CH4 创建社交网站 CH...
    学以致用123阅读 1,386评论 1 2
  • HTTP基本原理 URI、URL、URN(Uninform Resource) URI(Identifier):统...
    GHope阅读 2,057评论 2 26
  • 老人回首来时路。有人悔不当初,有人难得糊涂,有人感慨万千。你我不过一捧黄土。 这告诉我们一个道理――努力奋斗是没有...
    借我扁舟一叶阅读 119评论 0 1