【Python】大数据挖掘课程作业1——使用爬虫爬取B站评论、弹幕与UP主的投稿视频列表
数据挖掘部分的基本目标是:对于指定的UP主,能够获取其投稿视频列表;对于指定的视频,能够获取其视频标签、评论(包括评论下的回复)、弹幕。
文章默认读者对网络爬虫有一定的基础知识;
文章写作时(2020-06),B站正处于AV号像BV号过度的时期,部分API可能会在今后发生重大变化,请今后的读者注意。
获取指定UP主的投稿视频列表
首先,我们知道每一个B站帐号都有一个对应的数字UID,然后,通过在浏览器中访问用户的个人主页并查看后台请求,可以发现,用户的投稿视频列表信息是从api.bilibili.com/x/space/arc/search
获取的,响应为JSON格式,具体的查询参数附加在URL后的查询字符串中,基本的查询参数如下:
参数 | 含义 |
---|---|
mid | 用户的UID |
ps | 返回的结果中需要包含多少个视频的信息 |
tid | 视频的分类 |
pn | 需要获取第几页投稿视频 |
keyword | 搜索关键字 |
order | 返回结果的排序方式 |
其中,tid视频的分类指的是我们再B站主页上看到的分类信息:
API的响应会告诉我们不同的分类用哪个数字代表,也会告诉我们这个UP主在不同的分类下各有几个投稿视频:
需要注意的是,如果一个UP主在某一个分类下没有投稿视频,那么API的响应不会包含这个分类的信息;
order是搜索结果的排序方式,有三种排序方式:按更新时间、按播放数量、按点赞数量,这三种排序方式分别对应order=pubdate
、order=click
、order=stow
;
pn代表你想要第几页的投稿视频,ps代表一页要放下几个投稿视频,想象一下你在浏览网页,这两个参数的作用就不难理解了;
API返回的响应为JSON格式,结构如下:
其中,tlist包含了视频分类的信息,与API的tid参数有关,如上上张图所示;vlist则包含了我们需要的投稿视频信息,vlist的长度由ps参数决定,vlist中每一个对象的结构如下:
其中,我能确定意义的属性是:
属性 | 意义 |
---|---|
comment | 评论数量 |
paly | 播放数量 |
pic | 封面图片地址 |
subtitle | 小标题 |
description | 视频下方简介 |
title | 视频标题 |
author | UP主昵称 |
mid | UP主UID |
created | 视频上传日期的UNIX时间戳 |
length | 视频时长 |
aid | av号 |
bvid | bv号 |
到这里,API:api.bilibili.com/x/space/arc/search
的用法就介绍完成了,最后还需要注意两件事情:首先,为了防止爬虫被403,我们需要复制浏览器的请求header,并添加到爬虫中。其次,响应数据使用gzip压缩的,使用前需要解压缩,Python内置有gzip模块。
从视频播放页面中提取视频标签和其他信息
截至本文写作时,可以通过bilibili.com/video/BVxxxxxxx
或者bilibili.com/video/AVxxxxxxx
获取视频的播放页面,如果我们直接使用urlopen获取播放页面(这时候的页面是没有经过JS动态加载),得到的播放页面的结构如下:
meta标签中,有两个值得我们注意一下,第一个是拥有属性property="og:url"
的meta标签,这个标签的content的属性包含了这个视频使用AV号表示的播放页面(如果在只有BV号的情况下,想获取AV号,可以使用这个方法),第二个值得注意的meta标签拥有属性itemprop="commentCount"
,而这个标签的content属性记录了视频的评论数(如果需要最新的评论总数,可以使用这个方法)。
接下来,我们需要注意head中的最后两个script(第三个和第四个),第三个script中的内容如下:
可以看到,这个script标签中包含了大段的JSON数据,JSON数据中有很多URL,而这些URL中都包含了同一个ID:18xxxxx45,不难发现这个ID应该是指向视频的实际文件,但同时,这个ID也指向了视频的弹幕文件,所以在这里我们需要想办法提取出这个ID备用。
head中的第四个script标签中同样也是包含了大段JSON数据的JS代码,其中有一部分数据如下所示:
可以看到,这就是当前视频对应的标签,在我的课程设计中,我需要通过视频的标签对视频进行过滤,所以这里需要从页面中提取出标签信息。
我使用BeautifulSoup解析页面,并从meta标签和script标签中分离出我需要的信息,需要注意的是,在请求播放页面时仍需要完整的请求header以防止403,响应数据仍就经过gzip压缩,在进行分析前需要进行解压缩。
需要注意的是,这个页面结构只针对一般的视频,如果是电影、番剧、纪录片,页面结构会不一样,请注意。
获取某一视频的弹幕
获取B站弹幕的API是api.bilibili.com/x/v1/dm/list.so
,用这个API获取弹幕只需要在查询字符串中添加一个参数oid,而oid就是上面一节那个需要在播放页面的script标签中提取的id。
与其他API不同,获取弹幕的API的响应使用了deflate算法进行压缩而不是gzip压缩,具体到如何使用Python解压缩deflate,->https://www.baidu.com
。
解压缩后,我们发现弹幕数据是XML格式的,如下所示:
可以看到,弹幕内容记录在了d标签中,而弹幕的属性记录在了d标签的p属性中,弹幕的属性是几个用逗号分隔的数字组成的字符串,我只能认出这当中第一个数字是弹幕出现在视频中的时间,第五个数字是弹幕被发送的时刻的UNIX时间戳,其余属性的含义我就无能为力了。
使用Python解析XML有很多种方法,我才用了xml.etree
中的ElementTree
类进行XML解析。
在浏览器请求弹幕的请求头中,有一个字段为Refer,内容为https://www.bilibili.com./video/BVxxxxxxx
,所以在请求弹幕数据时,需要一并知道这个视频的BV号。
如果你的浏览器的请求头中还包含了Last-Modified这个字段,在复制请求头时请忽略这个字段(知道Last-Modified是干什么用的就能理解为什么要去掉它了)。
获取某一视频下的评论与评论下的回复
获取视频评论的API是https://api.bilibili.com/x/v2/reply
,使用的查询参数如下:
参数 | 含义 |
---|---|
type | 我不知道有什么用,设置成1就行了 |
pn | 获取第几页评论 |
oid | 对应视频的AV号 |
sort | 按热度排序的话,设置成0,按时间排序的话,设置成2 |
返回数据为JSON格式,如下所示:
其中,replies包含了我们需要的数据,每一个reply的格式如下:
其中,rpid表示这个评论的id,ctime代表评论时间的UNIX时间戳,comment下的message则是评论的具体内容,replies则代表了这个评论下的回复(不是所有,只有默认显示出来的几条)。
如果需要获取评论下的回复,则使用API:https://api.bilibili.com/x/v2/reply/reply
,查询字符串中的参数如下:
参数 | 含义
type | 不知道有什么用,设置成1就行了
oid | 对应视频的AV号
pn | 需要第几页回复
ps | 一页回复中需要几条回复
root | 回复所属于的评论的rpid
返回的数据也是JSON格式,结构与评论数据的结构一样,只是replies下不再会有replies;
注意事项:
- 返回的数据都是gzip压缩的,需要解压缩后使用;
- 如果某一视频被关闭评论,则API返回未经压缩的提示信息,需要为此做好异常处理;
- 浏览器的请求头中包含
Refer: https://bilibili.com/video/BVxxxxxxx
,严谨起见,在请求评论时一并提供视频的BV号;
相关代码
./liteTool.py
包装一下urlopen函数,如果发生错误会进行再次尝试,最多尝试3次,请求成功后会等待0.3s,防止请求过于频繁。
from urllib.request import urlopen, Request
from http.client import HTTPResponse
import time
firefox_cookie = '请从自己的浏览器获取’
def my_urlopen(url: Request) -> HTTPResponse:
err = Exception()
for _ in range(3):
try:
resp = urlopen(url=url) # type: HTTPResponse
except Exception as e:
err = e
print(e)
else:
time.sleep(0.3)
return resp
with open(file='./errors.data', mode='a', encoding='utf-8') as f:
f.write(url.get_full_url() + '\n')
f.write(str(err) + '\n')
f.write('\n')
raise Exception('HTTP请求失败!')
./GetBilibiliUploaderInfo.py
包含一个函数get_video_list_from_uploader_id(uid: str, start_time: datetime.datetime, end_time: datetime.datetime) -> list
,根据用户的UID获取一定时间段内所有的投稿视频信息。
from urllib.request import Request
from http.client import HTTPResponse
from .liteTool import firefox_cookie, my_urlopen
import json
import gzip