[爬虫] 获取B站视频评论
本帖最后由 mlyde 于 2021-10-3 18:12 编辑# 说明
前段时间,B站的网页版改版了,评论不以页码方式浏览,按时间排序只能从晚到早排序,对看刚发布时的评论带来极大的不便,于是写了这个python的脚本,爬取视频下方的所有评论,保存为`.csv`格式,便于查看。脚本仅用于学习交流。
目前只能一次性爬取视频的全部评论。
**修改:**
2021.9: 发现 json 中不再包含楼层,代码中楼层改为 0,原代码行注释。
顺便修复了父评论子程序中,遇到错误返回,但脚本依然异常退出的错误。
修复了爬取无评论视频时异常退出的错误。
# 用法
代码开头部分为设置:
>`cookie` 如果爬取有问题可以换一下
>`file_dir` 保存文件夹路径,默认留空,为运行路径
>`comment_mode` 可为 `1 或 2` 为按楼层或时间排序(逆序)`3` 按热度排序(默认)
直接运行脚本即可,输入av/BV号,或者直接粘贴视频链接,回车即可,爬取完成后将评论保存在指定位置,并自动退出。
# 完整代码
import requests
import os
import time
import json
# 设置 #
# cookie 与 数据保存路径(默认留空为'./')
cookie = "buvid3=63B1C902-3DD5-CD46-85D8-9A69679BC65665004infoc; CURRENT_FNVAL=80; blackside_state=1; sid=6aaqymp9; rpdid=|(u)mJ~Rlll~0J'uYkR||uuYm; fingerprint=33bf6967b63128e997c2ee0e3659a990; buvid_fp=63B1C902-3DD5-CD46-85D8-9A69679BC65665004infoc; buvid_fp_plain=63B1C902-3DD5-CD46-85D8-9A69679BC65665004infoc"
file_dir = ""
# 1:评论(楼层);2:最新评论(时间);3:热门评论(热度)
comment_mode = 3
# 设置 #
def visit(bv):
''' 访问av/BV对应的网页,查看是否存在 '''
if bv[:2] == 'BV' or bv[:2] == 'av':
url = 'https://www.bilibili.com/video/' + bv
headers = {
# 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-encoding': 'gzip, deflate',
'accept-language': 'zh-CN,zh;q=0.9',
'referer': 'https://www.bilibili.com/',
'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36',
}
response = requests.get(url, headers = headers)
else:
print('视频不存在!')
return 0
if response.status_code == 404 or """<div class="error-text">啊叻?视频不见了?</div>""" in response.text:
print('视频不存在!')
return 0
else:
return 1
def Bta(bv):
''' 将BV号转化为av号,如果已经是av号,直接返回数字部分(文本类型),方法参考cv9646821 '''
if bv[:2] == 'av':
return bv
bv = list(bv)
keys = {'1': 13, '2': 12, '3': 46, '4': 31, '5': 43, '6': 18, '7': 40, '8': 28, '9': 5,
'A': 54, 'B': 20, 'C': 15, 'D': 8, 'E': 39, 'F': 57, 'G': 45, 'H': 36, 'J': 38, 'K': 51, 'L': 42, 'M': 49, 'N': 52, 'P': 53, 'Q': 7, 'R': 4, 'S': 9, 'T': 50, 'U': 10, 'V': 44, 'W': 34, 'X': 6, 'Y': 25, 'Z': 1,
'a': 26, 'b': 29, 'c': 56, 'd': 3, 'e': 24, 'f': 0, 'g': 47, 'h': 27, 'i': 22, 'j': 41, 'k': 16, 'm': 11, 'n': 37, 'o': 2, 'p': 35, 'q': 21, 'r': 17, 's': 33, 't': 30, 'u': 48, 'v': 23, 'w': 55, 'x': 32, 'y': 14, 'z': 19}
for i in range(len(bv)):
bv = keys]
bv *= (58 ** 6)
bv *= (58 ** 2)
bv *= (58 ** 4)
bv *= (58 ** 8)
bv *= (58 ** 5)
bv *= (58 ** 9)
bv *= (58 ** 3)
bv *= (58 ** 7)
bv *= 58
return str((sum(bv) - 100618342136696320) ^ 177451812)
def send_f(bv, nexts=0, mode=3):
''' 返回父评论json\n bv: 全bv号\n nests: json页码\n mode: 1楼层,2时间,3热门 '''
r_url = 'https://api.bilibili.com/x/v2/reply/main'
url = 'https://www.bilibili.com/video/' + bv
av = Bta(bv)
headers = {
'accept': '*/*',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'no-cache',
'cookie': cookie,
'pragma': 'no-cache',
'referer': url,
'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
'sec-ch-ua-mobile': '?0',
'sec-fetch-dest': 'script',
'sec-fetch-mode': 'no-cors',
'sec-fetch-site': 'same-site',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36',
}
data = {
# 'callback': 'jQuery172030289933285891424_' + str(time.time()*1000)[:13],
'jsonp': 'jsonp',
'next': nexts, # 页码
'type': '1',
'oid': av, # av号
'mode': mode, # 1:楼层大前小后, 2:时间晚前早后, 3:热门评论
'plat': '1',
'_': str(time.time()*1000)[:13], # 时间戳
}
response = requests.get(r_url, headers = headers, params = data)
response.encoding = 'utf-8'
# 将得到的json文本转化为可读json
if 'code' in response.text:
c_json = json.loads(response.text)
else:
c_json = {'code': -1}
if c_json['code'] != 0:
print('json error!')
print(response.status_code)
print(response.text)
return 0 # 读取错误
return c_json
def send_r(bv, rpid, pn=1):
''' 返回子评论json\n bv: 全bv号\n rpid: 父评论的id\n pn: 子评论的页码 '''
r_url = 'https://api.bilibili.com/x/v2/reply/reply'
url = 'https://www.bilibili.com/video/' + bv
av = Bta(bv)
headers = {
'accept': '*/*',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'no-cache',
'cookie': cookie,
'pragma': 'no-cache',
'referer': url,
'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
'sec-ch-ua-mobile': '?0',
'sec-fetch-dest': 'script',
'sec-fetch-mode': 'no-cors',
'sec-fetch-site': 'same-site',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36',
}
data = {
# 'callback': 'jQuery172040348849791483166_' + str(time.time()*1000)[:13],
'jsonp': 'jsonp',
'pn': pn, # pagenumber
'type': '1',
'oid': av,
'ps': '10',
'root': rpid, # 父评论的rpid
'_': str(time.time()*1000)[:13], # 时间戳
}
response = requests.get(r_url, headers = headers, params=data)
response.encoding = 'utf-8'
# 将得到的json文本转化为可读json
if 'code' in response.text:
cr_json = json.loads(response.text)
else:
cr_json = {'code': -1}
if cr_json['code'] != 0:
print('error!')
print(response.status_code)
print(response.text)
return 0 # 读取错误
return cr_json
def parse_comment_r(bv, rpid):
''' 解析子评论json\n bv: 全bv号\n rpid: reply_id '''
cr_json = send_r(bv, rpid)['data']
count = cr_json['page']['count']
csv_temp = ''
for pn in range(1,count//10+2):
print('p%d %d' % (pn,count), end='\r')
cr_json = send_r(bv, rpid, pn=pn)['data']
cr_list = cr_json['replies']
if cr_list: # 有时'replies'为'None'
for i in range(len(cr_list)):
comment_temp = {
'floor': '',
'time': time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(cr_list['ctime'])),# 时间
'like': cr_list['like'], # 赞数
'uid': cr_list['member']['mid'], # uid
'name': cr_list['member']['uname'], # 用户名
'sex': cr_list['member']['sex'], # 性别
'content': '"' + cr_list['content']['message'] + '"', # 子评论
}# 保留需要的内容
for i in comment_temp:
csv_temp += str(comment_temp) + ','
csv_temp += '\n'
time.sleep(0.1)
return csv_temp
def parse_comment_f(bv):
''' 解析父评论json '''
c_json = send_f(bv, mode=comment_mode)
if c_json:
# 总评论数
try:
count_all = c_json['data']['cursor']['all_count']
print('comments:%d' % count_all)
except KeyError:
print('KeyError, 该视频可能没有评论!')
return '0', '2' # 找不到键值
else:
print('json错误')
return 1 # json错误
# 置顶评论
if c_json['data']['top']['upper']:
comment_top = c_json['data']['top']['upper']
# csv = '%s,%s,%s,%s,%s,%s,%s\n' % (comment_top['floor'], # 楼层
csv = '%s,%s,%s,%s,%s,%s,%s\n' % ('0', # 楼层
time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(comment_top['ctime'])),# 时间
comment_top['like'], # 赞数
comment_top['member']['mid'], # uid
comment_top['member']['uname'], # 用户名
comment_top['member']['sex'], # 性别
'"' + comment_top['content']['message'] + '"')# 评论内容
if comment_top['rcount'] or ('replies' in comment_top and comment_top['replies']):
rpid_f = comment_top['rpid'] # 父评论的rpid
csv += parse_comment_r(bv, rpid_f)
else:
csv = ''
# 开始序号
count_next = 0
# 存放原始json
all_json = ''
for page in range(count_all //20 +1):
print('page:%d' % (page+1))
c_json = send_f(bv, count_next, mode=comment_mode)
all_json += str(c_json) + '\n'
if not c_json:
return 1 # json错误
count_next = c_json['data']['cursor']['next'] # 下一个的序号
# 评论列表
c_list = c_json['data']['replies']
# 有评论,就进入下面的循环保存
if c_list:
for i in range(len(c_list)):
comment_temp = {
# 'floor': c_list['floor'], # 楼层
'floor': '0', # 楼层
'time':time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(c_list['ctime'])),# 时间
'like': c_list['like'], # 赞数
'uid': c_list['member']['mid'], # uid
'name': c_list['member']['uname'], # 用户名
'sex': c_list['member']['sex'], # 性别
'content': '"' + c_list['content']['message'] + '"', # 评论内容
}# 保留需要的内容
# 若有子评论,记录rpid,爬取子评论
replies = False
if c_list['rcount'] or ('replies' in c_list and c_list['replies']):
replies = True
rpid = c_list['rpid']
for i in comment_temp:
csv += str(comment_temp) + ','
csv += '\n'
# 如果有回复评论,爬取子评论
if replies:
csv += parse_comment_r(bv, rpid)
if c_json['data']['cursor']['is_end']:
print('读取完毕,结束')
# 为最后一个json,结束爬取
break
else:
print('评论为空,结束!')
break
time.sleep(0.2)
return csv, all_json
def main():
global file_dir
bv = input('input av/BV/url:')
if '/' in bv or '?' in bv:
# 分解链接
bv = bv.split('/')[-1].split('?')
if not visit(bv):
return
# 处理存储路径
if file_dir == '':
file_dir = './'
elif file_dir[-1] != '/' or file_dir[-1] != '\\':
file_dir += '/'
if not os.path.exists(file_dir):
print('存储路径不存在...', end='')
os.mkdir(file_dir)
print('已自动创建!')
dir_csv = file_dir + 'V_' + bv + '.csv'
dir_txt = file_dir + 'V_' + bv + '.txt'
# 如果是第一次写入文件,创建并写入标题
if not os.path.exists(dir_csv):
with open(dir_csv, 'w', encoding='utf-8-sig') as fp:
fp.write('楼层,时间,点赞数,uid,用户名,性别,评论内容\n')
csv, all_json = parse_comment_f(bv)
# 将返回的json保存为txt,原始内容存档
# with open(dir_txt, 'a', encoding='utf-8') as fp:
# fp.write(all_json + '\n\n')
# 保存评论csv
while True:
try:
with open(dir_csv, 'a', encoding='utf-8') as fp:
fp.write(csv)
break
except PermissionError:
input('文件被占用!!! (关闭占用的程序后,回车重试)')
if __name__ == "__main__":
main()
print('=== over! ===')
{"code":-412,"message":"请求被拦截","ttl":1,"data":null}
error!
412
{"code":-412,"message":"请求被拦截","ttl":1,"data":null}
TypeError Traceback (most recent call last)
<ipython-input-9-651070926d27> in <module>
307 input('文件被占用!!! (关闭占用的程序后,回车重试)')
308 if __name__ == "__main__":
--> 309 main()
310 print('=== over! ===')
<ipython-input-9-651070926d27> in main()
292 fp.write('楼层,时间,点赞数,uid,用户名,性别,评论内容\n')
293
--> 294 csv, all_json = parse_comment_f(bv)
295
296 # 将返回的json保存为txt,原始内容存档
<ipython-input-9-651070926d27> in parse_comment_f(bv)
206 if comment_top['rcount'] or ('replies' in comment_top and comment_top['replies']):
207 rpid_f = comment_top['rpid'] # 父评论的rpid
--> 208 csv += parse_comment_r(bv, rpid_f)
209 else:
210 csv = ''
<ipython-input-9-651070926d27> in parse_comment_r(bv, rpid)
158 for pn in range(1,count//10+2):
159 print('p%d %d' % (pn,count), end='\r')
--> 160 cr_json = send_r(bv, rpid, pn=pn)['data']
161 cr_list = cr_json['replies']
162 if cr_list: # 有时'replies'为'None'
TypeError: 'int' object is not subscriptable
求教楼主这是怎么回事? SherryYoung 发表于 2021-10-21 00:31
哥为啥我会报错呀,您能看看么,谢谢了!
json错误
我刚试了下,并没有出现这个错误。这个错误像是在第一次查询评论时获取的json出错。
请尝试在帖子对应的193-194行之间添加 `print(c_json)` ,查看出错json中的内容(不过我为了方便代码中有改json内容的行为,可能使错误看不出);或在第94行添加 `print(response.text)` ,查看请求返回的内容。
若无法解决,手动打开对应视频链接查看是否有评论,或告诉我使脚本出错的av/BV号。
谢谢楼主,学习交流了! 看lz的代码,如果一个视频名为《视频不见了》,会导致无法下载弹幕{:301_997:} 刚用火车头 熟练 这个python还不行 试试这个,谢谢楼主分享! 三滑稽甲苯 发表于 2021-7-20 17:18
看lz的代码,如果一个视频名为《视频不见了》,会导致无法下载弹幕
发个视频试下,我不信:lol 支持楼主! 老哥你的cookie在这摆着真的好吗{:301_971:} 谢谢楼主,学习交流了! 本帖最后由 三滑稽甲苯 于 2021-7-20 18:44 编辑
zheshiweihe 发表于 2021-7-20 17:41
发个视频试下,我不信
https://www.bilibili.com/video/BV19t411D7xd{:301_997:}