爬取哔哩哔哩评论区(包含对评论的回复)并保存在xlsx中
第一次写爬虫,作业要求写报告,那就修改一下顺便发到这里啦。最后成型的代码大量参考了这里
代码地址在这里
要干什么
通过python爬虫抓取哔哩哔哩弹幕视频网任一视频下的评论内容并保存为表格(.xlsx)
主要的问题
获取请求URL
- 一开始没有查看api文档、直接尝试获取URL时已知出现问题,后来才知道要删除中间的jQuery段
存储
- 因为爬取的是评论区,常常有大段的文字,常用'/n'换行,常用的csv存储可能会因此结构混乱,所以想通过xlsx存储
- 但是常用的几种操作xlsx的库对于“添加”这一操作都非常困难,看了一会儿总觉得头大
- 寻找了很久发现pandas的数据表操作非常实用,格式规范、合并简单
- 但是json格式跟pandas常用的写入方式还是不太一样,不过还好python转换键值对比较方便,具体实现可以看下方
子评论
- 对评论的回复(下称“子评论”)常常被无视,它的请求URL和参数值都和父评论本身不一样
- 遍历他们并提取数据花费了一定的时间
爬取间隔
- 不是异步代码。没有header池的话一定要有时间间隔啊!!!!!!!!!!!!
网页分析
随意打开一个视频的评论区,按下f12
打开控制台搜索相关内容,找到评论数据的保存格式,并在标头中找到对应的请求URL
查阅github上总结的api文档了解到各必要参数意义:
- next:页码
- type:默认为1
- oid:av号
- mode:查询模式(楼层、时间、热度)
- plat:默认为1
需要注意的是,中间的jQuery段需要删除才能获取请求
找到了目标地址和格式就可以开始写代码了
代码实现
没有太多特别的地方,函数互相调用导致拆开了分析不太方便,一次性附上了
""" 主要参考:https://blog.csdn.net/mlyde/article/details/118936871 query说明:https://www.bilibili.com/read/cv8325021/ 通过向API发送请求获得json文件 请求地址: https://api.bilibili.com/x/v2/reply?pn={1}&type={2}&oid={3}&sort={4} """ import requests import os import time import json import pandas as pd from bilibili_api import video, sync # https://bili.moyu.moe/#/ # 全局变量 cookie = "最好修改成自己的,保留bili_jct、buvid3和SESSDATA,在终端查询或者直接点击浏览器地址栏的小锁找找都能找到" file_dir = "./comment_data/" bv = "BV号,链接也成" comment_mode = 3 # mode是需要传入的api,规定了排序模式: 1:评论(楼层);2:最新评论(时间);3:热门评论(热度),不过1已经失效了 def b2a(bv_num): """ 调用现成的bilibili库将用户输入的哔哩哔哩地址转为真正用于识别视频的oid(av号) :param bv_num: 用户设定的哔哩哔哩视频BV号,每个bv参数名都不一样是因为pycharm一直提示我要从外部隐藏该名称…… :return: oid(av号) """ v = video.Video(bvid=bv_num) info = sync(v.get_info()) return info.get('aid', "None") def response_f(bv_id, next=0, mode=3): """ request网页提取出的返回父评论的函数,以json格式传输 :param bv_id: bv号 :param next: json中用来标注页码 :param mode: 所用的额排序模式 :return: 返回提取后的json """ api_url = 'https://api.bilibili.com/x/v2/reply/main' url = 'https://www.bilibili.com/video/' + bv_id av = b2a(bv_id) # 先转av号 # 复制的headers,user-agent和cookie是我自己的 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/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36', } # 定义所拆解内容 data = { 'jsonp': 'jsonp', 'next': next, # 页码 'type': '1', 'oid': av, # av号 'mode': mode, # 1:楼层大前小后, 2:时间晚前早后, 3:热门评论 'plat': '1', '_': str(time.time() * 1000)[:13], # 时间戳 } response = requests.get(api_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 response_r(bv, rpid, pn=1): """ 返回子评论json,和父评论方式基本相同但是参数不同,重写了一个,这里其实复用程度不是很够,可以写个循环+判断省略的,因为是作业就偷懒了 :param bv: bv号 :param rpid: 父评论的id :param pn: 子评论的页码是通过pn判断的 :return: json格式的子评论 """ r_api_url = 'https://api.bilibili.com/x/v2/reply/reply' url = 'https://www.bilibili.com/video/' + bv av = b2a(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 = { 'jsonp': 'jsonp', 'pn': pn, # page number 'type': '1', 'oid': av, 'ps': '10', 'root': rpid, # 父评论的rpid '_': str(time.time() * 1000)[:13], # 时间戳 } response = requests.get(r_api_url, headers=headers, params=data) response.encoding = 'utf-8' # 加载得到的json if 'code' in response.text: r_cjson = json.loads(response.text) else: r_cjson = {'code': -1} if r_cjson['code'] != 0: print('error!') print(response.status_code) print(response.text) return 0 # 读取错误 return r_cjson def parse_comment_r(bv, rpid, df): """ 解析子评论json :param bv: bv号 :param rpid: 父评论的id :param df: pandas datagram,作为本程序的数据传递方式 :return: 返回修改后的df """ cr_json = response_r(bv, rpid)['data'] count = cr_json['page']['count'] for pn in range(1, count // 10 + 2): time.sleep(0.1) print('p%d %d ' % (pn, count), end='\r') cr_json = response_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 = [{ 'time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(cr_list[i]['ctime'])), # 时间 'like': cr_list[i]['like'], # 赞数 'uid': cr_list[i]['member']['mid'], # uid 'name': cr_list[i]['member']['uname'], # 用户名 'sex': cr_list[i]['member']['sex'], # 性别 'content': '"' + cr_list[i]['content']['message'] + '"', # 子评论 }] # 保留需要的内容 df2 = pd.DataFrame(comment_temp) df = pd.concat([df, df2], axis=0, ignore_index=True) return df def parse_comment_f(bv, df): """ 解析父评论json :param bv: bv号 :param df: pandas datagram,作为本程序的数据传递方式 :return: 返回修改后的df """ c_json = response_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', '0' # json错误 # 开始序号 count_next = 0 # 存放原始json all_json = '' for page in range(min((count_all // 20 + 1),150)): time.sleep(1) print('page:%d' % (page + 1)) c_json = response_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[i]['floor'], # 楼层 'time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(c_list[i]['ctime'])), # 时间 'like': c_list[i]['like'], # 赞数 'uid': c_list[i]['member']['mid'], # uid 'name': c_list[i]['member']['uname'], # 用户名 'sex': c_list[i]['member']['sex'], # 性别 'content': c_list[i]['content']['message'], # 评论内容 }] # 保留需要的内容 # 若有子评论,记录rpid,爬取子评论 replies = False replies = False if c_list[i]['rcount'] or ('replies' in c_list[i] and c_list[i]['replies']): replies = True rpid = c_list[i]['rpid'] df = parse_comment_r(bv, rpid, df) # 如果有回复评论,爬取子评论 df2 = pd.DataFrame(comment_temp) df = pd.concat([df, df2], axis=0, ignore_index=True) if c_json['data']['cursor']['is_end']: print('读取完毕,结束') # 为最后一个json,结束爬取 break else: print('评论为空,结束!') break time.sleep(0.2) return df, all_json def main(): global file_dir global bv if '/' in bv or '?' in bv: # 分解链接 bv = bv.split('/')[-1].split('?')[0] # 处理存储路径 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('已创建') data0 = [{'time': '', 'like': '', 'uid': '', 'name': '', 'sex': '', 'content': ''}] # 行首 df = pd.DataFrame(data0) df, all_json = parse_comment_f(bv, df) df = df.drop(index=0) # 这种方式会有空行,把它干掉 # 保存 while True: # 使用while以便占用时可以关掉文件后继续操作而非必须从头执行 try: df.to_excel("./%s/%s.xlsx" % (file_dir, bv)) break except PermissionError: input('文件被占用(关闭占用的程序后,回车重试)') if __name__ == "__main__": main() print('== over! ==')
本文作者:有氧
本文链接:https://www.cnblogs.com/youtmdyang/p/16126595.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步