在浏览社交媒体时,我们所看的内容仿佛是无穷无尽的。

我们常常滑动到页面底端,以为没有内容了,却发现新的内容又一下子刷新出来。内容越滑越多,这种数据被称作列表流数据。

有趣的是,当页面不断为我们提供新的内容时,网页却还是原来的网页——URL 并没有改变。这是怎么回事?

列表流数据示例:微博热门内容

1 Ajax

在同一个页面中,网页是如何源源不断的展现新内容的呢?

如果打开浏览器的开发者模式,当我们滑动到页面底端时,我们可以在 “网络” 选项卡中观测到一些新生成的 xhr 类型条目。这类条目中包含的就是 Ajax 请求。根据崔庆才老师的介绍:

Ajax,全称为 Asynchronous JavaScript and XML,即异步的 JavaScript 和 XML。它不是一门编程语言,而是利用 JavaScript 在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。

对于传统的网页,如果想更新其内容,那么必须要刷新整个页面,但有了 Ajax,便可以在页面不被全部刷新的情况下更新其内容。在这个过程中,页面实际上是在后台与服务器进行了数据交互,获取到数据之后,再利用 JavaScript 改变网页,这样网页内容就会更新了。

开发者模式下的 xhr 条目

简单来说,当我们向下滑动到页面底端,JavaScript 会向网页后台的服务器发送一个请求,告诉服务器我们想要更多的内容。服务器返回相应的响应内容,JavaScript 又对响应内容(不论是 HTML 格式,还是 JSON 格式)进行了解析,并渲染成为我们看到的新内容。

2 列表流数据的爬取:以微博为例

了解了列表流数据是如何源源不断产生的后,我们就有了获取这种数据的思路:模拟 JavaScript 向网页服务器发送 Ajax 请求,并解析获取到的响应数据。

下面来演示爬取过程——以爬取微博个性化推荐内容为例。

2.1 观察 Ajax 请求

我们找到上文提到的 xhr 文件,点击它,我们可以看到文件包含的标头信息。其中相关的信息包括:

  • 请求 URL:如果你观察多条 xhr 条目的话,你会发现它们的 URL 几乎完全一样,唯一不同之处在于 max_id 的值,从 1 开始,每从底部刷新一次,新的 xhr 条目的 max_id 的值就会增加 1。
  • 请求方法
  • 状态代码
  • 请求标头中的 cookie
  • 请求标头中的 user-agent
  • 请求标头中的 x-requested-with,它的值为 XMLHttpRequest,这标记了该条请求是 Ajax 请求

xhr 标头

除了标头以外,我们还可以观察到该条 Ajax 请求的预览,这是一个 JSON 文件。

xhr 预览

可以看到,该条请求中包含 10 条内容,如果深入观察一条内容内部,我们可以找到这条内容对应的微博信息,包括微博 id、发布用户、微博文字内容、微博图片内容、发布该条微博的 IP 地址信息等。

xhr 预览-内部

2.2 爬取 Ajax 请求获取的 JSON 数据

现在我们可以尝试爬取上述数据了。首先导入我们所需的包,并定义一些后续用得到的变量:

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

base_url = 'https://weibo.com/ajax/feed/hottimeline?'  # 本项目要爬取的 url 都是以此为开头的
headers = {
    'User-Agent': '你的 User-Agent',  # 点击你要爬取的 xhr 文件,在标头中可以找到相关信息。
    'X-Requested-With': 'XMLHttpRequest',
    'Cookie': '你的 Cookie'
}

我们来构建一个函数,用于返回我们要发送模拟请求的 URL:

def get_hottimeline_url(max_id):
    '''
    返回一个 xhr 类型 "hottimeline (热榜时间流)" 的 url。
    :param max_id: 每个 hottimeline url 中特殊的 max_id (1, 2, 3...)
    '''
    params = {
        'refresh': '2',
        'group_id': '你 URL 中的 group_id',
        'containerid': '你 URL 中的 group_id containerid',
        'extparam': 'discover%7Cnew_feed',
        'max_id': max_id,  # URL 中只有该值是变化的
        'count': '10'
    }
    url = base_url + urlencode(params, safe='%')  # 此处指明 safe 参数,使 "%" 不被转义为 "%25",不然会拼接成错误的 URL
    return url

接下来,我们像获取静态页面的数据一样,通过 request 发送请求,获取 JSON 数据:

def get_response_json(url):  
    '''
    返回一个 url 的 json 文件。
    '''
    response = requests.get(url, headers=headers)
    json = response.json()
    return json

2.3 解析 JSON 数据

获取到 JSON 数据后,我们需要对数据进行解析,获取我们所需的信息。一个 URL 能返回 10 条微博内容,我们希望能循环得到每一条微博内容的数据,存为一个字典。再将这个字典,存入微博内容组成的列表中。

在获取一条微博的文本内容时,我们需要注意,当这条微博的文本内容过长时,文本段会被折叠。如果我们想看到完整的内容,需要在浏览器界面点击“展开”按钮,这使得我们无法在现有的 JSON 数据中获得完整的文本数据。但是,当我们点击展开时,可以看到开发者模式的网络选项卡中,又多出了名为 “longtext?id=xxxxxxxxxx” 的xhr条目,我们可以通过该条目的 URL 获取到完整的长文本数据。

def parse_hottimeline(list_recommendation, json):
    '''
    解析一条 hottimeline 的 json 文件,并将包含的 10 条热榜推荐内容追加到内容列表中。
    '''
    for item in json.get('statuses'):
        weibo = {}  # 创建一个临时的用于保存一条微博信息的字典

        # user information
        user_info = item.get('user')
        weibo['user_id'] = user_info.get('id')  # 发布者 id
        weibo['user_name'] = user_info.get('screen_name')  # 发布者昵称

        # weibo information
        weibo['id'] = item.get('id')  # 微博 id
        weibo['isLongText'] = item.get('isLongText')  # 该变量为 True 时,这个微博的文本为长文本(文本段会被折叠)
        weibo['mblogid'] = item.get('mblogid')  # 可以通过该变量,索引到存有长文本的 JSON 文件的 URL
        if weibo['isLongText'] is True:
            url = "https://weibo.com/ajax/statuses/longtext?id=" + weibo['mblogid']
            json_longtext = get_response_json(url)
            weibo['text'] = json_longtext.get('data').get('longTextContent')
        else:
            weibo['text'] = pq(item.get('text')).text()
        weibo['pic_num'] = item.get('pic_num')  # 该条微博包含的图片数
        weibo['pic'] = []  # 用于保存该条微博图片的 url
        if weibo['pic_num'] > 0:
            pic_dict = item.get('pic_infos')
            for pic in pic_dict:
                pic_url = pic_dict[pic]['original']['url']  
                weibo['pic'].append(pic_url)
        else:
            pass
        weibo['attitudes'] = item.get('attitudes_count')  # 点赞数
        weibo['comments'] = item.get('comments_count')  # 评论数
        weibo['reposts'] = item.get('reposts_count')  # 转发数
        region = item.get('region_name')  # 发布时的 IP 地址
        if region is None:
            weibo['region'] = region
        else: weibo['region'] = region.strip('发布于 ')
        print(weibo)
        list_recommendation.append(weibo)  # 将解析出的一条微博数据,加入一个列表中

2.4 存储数据

我们已经实现了页面底部刷新数据的 URL 获取、模拟请求、解析数据的功能。最后,我们将新建一个列表,将每条微博信息存储进去。主函数的代码如下:

if __name__ == '__main__':
    list_recommendation = []
    for max_id in range(1, 11):  # 模拟爬取 10 次刷新结果,最终能获取到 100 条个性化推荐热门微博数据
        hottimeline_url = get_hottimeline_url(max_id)
        print('hottimeline_url = ', hottimeline_url)
        response_json = get_response_json(hottimeline_url)
        parse_hottimeline(list_recommendation, response_json)

参考

注:转载请注明出处。

去码头整点薯条
1 声望1 粉丝

Internal Server Error