【Python】使用Python根据BV号爬取对应B站视频下的所有评论(包括评论下的回复)

【Python】使用Python根据BV号爬取对应B站视频下的所有评论(包括评论下的回复)

本文写于2020-4-27,当你阅读到本文的时候如果因为下列原因导致本文代码无法正常工作,本人概不负责。

  • B站的页面和API接口的变动
  • B站为页面和API加入了反爬虫机制,或者对请求首部有了新的要求
  • Python版本的变动和标准库的调整
  • BeautifulSoup4的变动

使用到的库

  • 【第三方库】:BeautifulSoup4
  • 【标准库】: urlpib.request中的Request对象和urlopen函数
  • 【标准库】: json和gzip,用于解码从网络获得的数据

爬取思路

  1. 根据BV号,获取页面www.bilibili.com/video/{BV},从页面的meta标签中可以获得视频对应的AV号和评论条数(包括评论的回复)这两个信息。
  2. 使用B站的API获取视频的评论,即从api.bilibili.com/x/v2/reply获取视频的评论,得到的是JSON格式的数据。
  3. 使用B站的API获取评论的回复,即从api.bilibili.com/x/v2/reply/reply获取评论的回复,得到的是JSON格式的数据。

(一) 通过BV号获取AV号和评论数

通过页面www.bilibili.com/video/{BV}可以获得一个原始页面,需要注意的是用urlopen打开这个页面时需要添加好头部字段,否则会返回HTTP403。

第一个问题:最初我是用chrome内核的Edge打开B站页面时,发现浏览器添加的头部字段中有一部分只能在HTTP2.0中使用,如下图所示:
在这里插入图片描述

经过查看文档和一番搜索,我发现urlopen只支持HTTP1.1,而且似乎没有成熟的能够进行HTTP2.0请求的第三方库,然后我尝试使用火狐打开页面,发现火狐添加的头部字段中没有只能在HTTP2.0中使用的部分,所以我复制了一份火狐的头部字段在代码中使用。
【Python】使用Python根据BV号爬取对应B站视频下的所有评论(包括评论下的回复)_第1张图片

代码如下:

video_url = 'https://www.bilibili.com/video/' + bv
    headers = {
        'Host': 'www.bilibili.com',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
        'Accept-Encoding': 'gzip, deflate, br',
        'Connection': 'keep-alive',
        'Cookie': '',
        'Upgrade-Insecure-Requests': '1',
        'Cache-Control': 'max-age=0',
        'TE': 'Trailers',
    }
    rep = Request(url=video_url, headers=headers)  # 建立一个Request对象
    html_response = urlopen(rep)  # type: HTTPResponse

第二个问题www.bilibili.com/video/{BV}返回的页面是通过gzip编码的,需要使用Python内置的gzip模块进行解码,解码后得到的是byte字符串,需要调用其decode方法转换成utf-8编码的字符串。
代码如下:

html_content = gzip.decompress(html_response.read()).decode(encoding='utf-8')

至此,我们可以按照正常的方式初始化BeautifulSoup对象,查看获得的页面可以发现,视频的AV号和评论数记录在以下两个meta标签中:

<meta data-vue-meta="true" property="og:url" content="https://www.bilibili.com/video/av710394386/">
<meta data-vue-meta="true" itemprop="commentCount" content="644">

然后我们就可以使用BeautifulSoup获得我们需要的信息(AV号和评论数):

bs = BeautifulSoup(markup=html_content, features='html.parser')
comment_meta = bs.find(name='meta', attrs={'itemprop': 'commentCount'})
av_meta = bs.find(name='meta', attrs={'property': 'og:url'})

comment_count = int(comment_meta.attrs['content'])
av_number = av_meta.attrs['content'].split('av')[-1][:-1]

print(f'视频 {bv} 的AV号是 {av_number} ,元数据中显示本视频共有 {comment_count} 条评论(包括评论的评论)。')

(二) 通过B站API获取视频的直接评论

获取B站视频直接评论的API是api.bilibili.com/x/v2/reply&type=1&pn={第几页}&oid={AV号}&sort={0是按照时间排序,2是按照热度排序},这个API似乎没有什么反爬虫机制,直接urlopen就可以了(在我这里是这样),其中type参数我还没有搞清楚有什么用。
由于API是为网页服务的,所以要一页一页的爬。
浏览器里F12可以看到对评论的请求,网页实际工作时用的参数比我这里用的多,不过我去掉的参数似乎不产生影响。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzuMXyJm-1587930491282)(images/2020-04-27-03-25-34.png)]

代码如下:

comment_url = f'https://api.bilibili.com/x/v2/reply?pn={page_num}&type=1&oid={av_number}' + \
              f'&sort={0 if time_order else 2}'
comment_response = urlopen(comment_url)  # type: HTTPResponse
comments = json.loads(comment_response.read().decode('utf-8'))  # type: dict
comments = comments.get('data').get('replies')  # type: list

这里可以根据自己的需要来处理JSON数据,至于获得的JSON的格式是怎样的,自己在编辑器里看一下就好了。
【Python】使用Python根据BV号爬取对应B站视频下的所有评论(包括评论下的回复)_第2张图片

(三) 通过B站API获取视频评论下的回复

获取B站视频评论的回复的API是api.bilibili.com/x/v2/reply/reply?type=1&oid={AV号}&pn={第几页回复}&ps={你想要一页有几个回复}&root={从评论的JSON数据中取得属性rpid然后放在这里}
这个API似乎也没有什么反爬虫机制,我这里直接urlopen就行了,网页端实际工作时还会添加其他参数,不过我这里把他们去掉之后似乎也没有什么影响。
代码如下:

reply_url = f'https://api.bilibili.com/x/v2/reply/reply?' + \
            f'type=1&pn={rp_page}&oid={av_number}&ps={rp_num}&root={rp_id}'
reply_response = urlopen(reply_url)  # type: HTTPResponse
reply_reply = json.loads(reply_response.read().decode('utf-8'))  # type: dict
reply_reply = reply_reply.get('data').get('replies')  # type: dict

返回的也是JSON数据,JSON的结构自己爬下来看一下就好,然后根据自己的需要处理。

(四) 其他事项和完整代码

由于通过API获取评论和回复时只能一页一页获取,所以在实际代码中要自行判断何时停止请求下一页评论,我这里一开始从页面元数据中取得总评论数也是由于这个原因,用于判定何时停止请求下一页。

完整代码:

# _*_ coding: utf-8 _*_
from urllib.request import urlopen, Request
from http.client import HTTPResponse

from bs4 import BeautifulSoup

import gzip
import json


def get_all_comments_by_bv(bv: str, time_order=False) -> tuple:
    """
    根据哔哩哔哩的BV号,返回对应视频的评论列表(包括评论下面的回复)
    :param bv: 视频的BV号
    :param time_order: 是否需要以时间顺序返回评论,默认按照热度返回
    :return: 包含三个成员的元组,第一个是所有评论的列表(评论的评论按原始的方式组合其中,字典类型)
             第二个是视频的AV号(字符串类型),第三个是统计到的实际评论数(包括评论的评论)
    """
    video_url = 'https://www.bilibili.com/video/' + bv
    headers = {
        'Host': 'www.bilibili.com',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
        'Accept-Encoding': 'gzip, deflate, br',
        'Connection': 'keep-alive',
        'Cookie': '',
        'Upgrade-Insecure-Requests': '1',
        'Cache-Control': 'max-age=0',
        'TE': 'Trailers',
    }
    rep = Request(url=video_url, headers=headers)  # 获取页面
    html_response = urlopen(rep)  # type: HTTPResponse
    html_content = gzip.decompress(html_response.read()).decode(encoding='utf-8')
    bs = BeautifulSoup(markup=html_content, features='html.parser')
    comment_meta = bs.find(name='meta', attrs={'itemprop': 'commentCount'})
    av_meta = bs.find(name='meta', attrs={'property': 'og:url'})

    comment_count = int(comment_meta.attrs['content'])  # 评论总数
    av_number = av_meta.attrs['content'].split('av')[-1][:-1]  # AV号

    print(f'视频 {bv} 的AV号是 {av_number} ,元数据中显示本视频共有 {comment_count} 条评论(包括评论的评论)。')

    page_num = 1
    replies_count = 0
    res = []

    while True:
        # 按时间排序:type=1&sort=0
        # 按热度排序:type=1&sort=2
        comment_url = f'https://api.bilibili.com/x/v2/reply?pn={page_num}&type=1&oid={av_number}' + \
                      f'&sort={0 if time_order else 2}'
        comment_response = urlopen(comment_url)  # type: HTTPResponse
        comments = json.loads(comment_response.read().decode('utf-8'))  # type: dict
        comments = comments.get('data').get('replies')  # type: list
        if comments is None:
            break

        replies_count += len(comments)
        for c in comments:  # type: dict
            if c.get('replies'):
                rp_id = c.get('rpid')
                rp_num = 10
                rp_page = 1
                while True:  # 获取评论下的回复
                    reply_url = f'https://api.bilibili.com/x/v2/reply/reply?' + \
                                f'type=1&pn={rp_page}&oid={av_number}&ps={rp_num}&root={rp_id}'
                    reply_response = urlopen(reply_url)  # type: HTTPResponse
                    reply_reply = json.loads(reply_response.read().decode('utf-8'))  # type: dict
                    reply_reply = reply_reply.get('data').get('replies')  # type: dict

                    if reply_reply is None:
                        break

                    replies_count += len(reply_reply)
                    for r in reply_reply:  # type: dict
                        res.append(r)

                    if len(reply_reply) < rp_num:
                        break
                    rp_page += 1
                c.pop('replies')
                res.append(c)

        if replies_count >= comment_count:
            break
        page_num += 1

    print(f'实际获取视频 {bv} 的评论总共 {replies_count} 条。')
    return res, av_number, replies_count


if __name__ == '__main__':
    cts, av, cnt = get_all_comments_by_bv('BV1op4y1X7N2')
    for i in cts:
        print(i.get('content').get('message'))

你可能感兴趣的:(一入Python深似海,python,数据分析,http,json)