本文写于2020-4-27,当你阅读到本文的时候如果因为下列原因导致本文代码无法正常工作,本人概不负责。
www.bilibili.com/video/{BV}
,从页面的meta
标签中可以获得视频对应的AV号和评论条数(包括评论的回复)这两个信息。api.bilibili.com/x/v2/reply
获取视频的评论,得到的是JSON格式的数据。api.bilibili.com/x/v2/reply/reply
获取评论的回复,得到的是JSON格式的数据。通过页面www.bilibili.com/video/{BV}
可以获得一个原始页面,需要注意的是用urlopen
打开这个页面时需要添加好头部字段,否则会返回HTTP403。
第一个问题:最初我是用chrome内核的Edge打开B站页面时,发现浏览器添加的头部字段中有一部分只能在HTTP2.0中使用,如下图所示:
经过查看文档和一番搜索,我发现urlopen只支持HTTP1.1,而且似乎没有成熟的能够进行HTTP2.0请求的第三方库,然后我尝试使用火狐打开页面,发现火狐添加的头部字段中没有只能在HTTP2.0中使用的部分,所以我复制了一份火狐的头部字段在代码中使用。
代码如下:
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是api.bilibili.com/x/v2/reply&type=1&pn={第几页}&oid={AV号}&sort={0是按照时间排序,2是按照热度排序}
,这个API似乎没有什么反爬虫机制,直接urlopen
就可以了(在我这里是这样),其中type参数我还没有搞清楚有什么用。
由于API是为网页服务的,所以要一页一页的爬。
浏览器里F12可以看到对评论的请求,网页实际工作时用的参数比我这里用的多,不过我去掉的参数似乎不产生影响。
代码如下:
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的格式是怎样的,自己在编辑器里看一下就好了。
获取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'))