IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> python爬取B站动态的评论 -> 正文阅读

[Python知识库]python爬取B站动态的评论

目录

前言

需求

方案分析

方案一

?方案二

接口分析

请求流程

抓包演示

?请求接口

接口说明

接口测试

?代码


前言

想看接口分析和代码的,可跳过前言。

需求

我参加了一些up主的动态抽奖,转赞评那种的,如图1。

图1 - 动态抽奖

开奖周期一般在两周到四周,如果粉丝数多的话,会被大量评论,而且他就没及时开过奖(指说几号开奖就几号开奖),实际都是比说的日期再晚几天才开奖。

开奖后我想删除我的转赞评。转发在自己动态里就能删,点赞只要在up主原动态再点一下点赞按钮就能取消,评论却只能翻到自己的那条评论,才能删除。

问了B站客服,他说只有以下方法能删除自己的评论:

  1. 手动翻到自己的那条评论
  2. 别人回复你的评论,你收到提醒,在提醒里点击对方回复的内容,可跳转到你的评论?

方案分析

按热度排序,我不确定我到底在哪,反正不在靠前的位置,应该是中间或后面。

按时间排序,B站是把距离当前时间最近的评论,放在第一个(置顶除外),把从时间上真正第一个发的评论,放在最后一个。我认为这是降序,可我参加时一般都是up主发动态的当天,降序的话几乎得翻完全部评论才能找到我的评论,也就是说升序可以很快地找到我的评论,但B站不支持,我反馈了,客服说会给研发提。

由于参与评论的人太多,手动翻是翻不到的;可是也没人回复我的评论,那怎么办?

方案一

首先我想到让程序自动翻页,我知道的有两类:

  • 按键精灵之类的软件,录制好键鼠动作,然后自动执行动作
  • 用代码操作浏览器,实现自动翻页

第一类

我用的不是按键精灵,是别人自己写的软件,功能类似。一开始翻页时,新数据加载得挺快;随着时间的推移,新数据加载得越来越慢。例如最开始时,只要页面滚动的位置到了,下一页的数据马上就能加载出来;一段时间后,虽然页面滚动的位置到了,也出现了正在加载的提示,但需要几秒甚至十几秒甚至几分钟才能显示出下一页的数据,这个加载时间越来越长。

时间长点没事,只要能加载出来,我就能点删除。按热度排序的话,我得隔一会儿就搜下有没有加载出我的评论,且翻页次数非常非常多后,页面搜索很慢(我试了),也没搜到我。所以后来我又选了按时间排序,花了好几个小时,当加载到我发评论的前一天时,页面崩溃了,只能刷新,刷新后之前已加载的那么多评论全没了,又试了次依然这样。

针对以上加载速度慢、页面崩溃,我能想到的原因有:

  • B站限制了数据返回的速率或频率
  • 我的网络原因(这个可能性最低)
  • 浏览器性能不够,我用的Edge
  • 电脑硬件性能瓶颈,如CPU和内存,我是I7-6700HQ,12G内存,三星860EVO

实际是什么原因就不知道了,总之这个方法不行。

第二类

一开始,我用selenium打开那条动态,让selenium执行js代码,使页面滚动,但实际滚动的效果和手动用鼠标滚动不一样,好像是滚动几次就不行了,就加载不出新数据了,我忘了,总之这个方法不行。

后来,我在浏览器Console里手动执行同样的js代码,效果和selenium一样,可能是代码原因,有知道的可以告诉我,或者其他写法也行。我试了以下几种,当然不是一起运行,一次运行一种:

//第一种
document.body.scrollTop=10000 // PhantomJS使用,headless不支持

//第二种
document.documentElement.scrollTop=10000 // 页面向下滚动指定的像素,数值很大时肯定能滚动到底部。这是页面滚动,不是模拟鼠标滚轮滚动

//第三种
var q=document.documentElement.scrollTop=10000 // 或这种写法 from https://www.cnblogs.com/landhu/p/5761794.html

//第四种
window.scrollTo(0,document.body.scrollHeight) // 滚动条回到底部。连续滚动时,有时上面两种js代码滚动一会儿就失效了,可以用这种 from https://www.cnblogs.com/xyztank/articles/14259330.html
// window.scrollTo(0,0) // 滚动条回到顶部

?方案二

方案一中的方法都不行,我不准备删了,突然有人回复我在某视频下的评论,如图2。

图2 - 别人回复我的评论

??以往回复我的内容,字数很少,直接在图2中的界面就能看完,我就没点过。直到这次的太长了显示不完,我就点击了他回复的内容,然后竟然直接跳转到我的评论了,如图3。

图3 - 点击图2后跳转到我的评论

?这正好符合客服说的第二种,并且向下翻,还看到了页码,如图4,不过这张图是后来测试时在某个动态的截图(因为我要删对动态的评论),但页码、跳转到指定页等内容都和当时视频那里一样,注意页面滚动条位于底部,平时B站无论动态还是视频,评论都是动态加载的,滚动条不会一直在底部,会触发加载下一页,除非所有评论真的记载完了,那样页面底部也有提示到底了。

图4 - 图3页面的底部出现页码

?既然有页码了,那不是可以直接跳转到靠后的页面(按时间排序,我评论得早,一般都是up主发动态的当天),然后快速找到我的评论?最多就是多翻几页。可我点击页码后,页面内容还是当前页的内容,不能通过点击页码跳转页面,后面那个跳转到指定页也不管用。

然后我看了当前URL,reply后应该是当前评论的ID,即我的评论的ID。

https://www.bilibili.com/video/BV1aQ4xxxxxx#reply5816xxxxxx

于是有了新思路,找到所有我参加过评论的动态,用爬虫爬取它们的所有评论数据,里面就有我的评论的ID,然后将ID拼接到动态的URL的末尾,访问就能跳转到我的评论,然后就能删除。如图5,当然那条不是我发的,我为了验证这个思路随意找了一条测试的。

图5 - 访问构造的URL跳转到我的评论

另外,因为是转赞评,有些抽奖动态我以前直接在我的动态里删除了转发 ,现在怎么找到它们从而删除评论?在up主的动态列表里,只要点赞按钮是蓝色,说明自己已点赞,已参加转赞评,如图6。但是转发和评论不会变蓝,无论自己有没有真的转发和评论。

图6 - 点赞变蓝是已点赞,说明已参加转赞评

点开这条动态,也能看到,如图7。

图7 - 点开图6的动态

接口分析

请求流程

  1. 打开一条动态,如https://t.bilibili.com/56764707xxxxxxxxxx,注意已手动去除末尾的?tab=2
  2. 获取动态本身的信息,如类型、oid等
  3. 根据动态的类型、oid获取评论数据,当然必需的参数还有页码、排序方式

抓包演示

图8 - 步骤2的请求的参数

图9 - 步骤2的请求返回的json数据,得到type

图10 - 步骤3的请求返回的json数据,包含评论

?请求接口

接口说明

接口1,步骤2请求的接口

https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id=

参数dynamic_id就是步骤1的URL末尾的一长串字符,我称为动态ID。

返回的json数据中有一个键是type,如图9,表示动态类型,它的值有以下几种可能:

  • 1:有转发内容
  • 2:动态有图片
  • 4:纯文字
  • 2048:可能是内容有站内分享(比如装扮推荐)
  • 其他情况有知道的可以补充

接口2,步骤3请求的接口

图10中携带了很多参数,但callback和plat和_都不是必需的,带上了反而可能出错(接口测试部分第三步会说),有人说callback和_是根据某种规则生成的,用于跟踪用户,后来我发现_的值很像当前时间的时间戳。

https://api.bilibili.com/x/v2/reply/main?jsonp=jsonp&next=&type=&oid=&mode=
接口2的请求参数的说明
参数名参数类型含义
next字符串0和1都表示第一页,2表示第二页,3表示第三页...
type字符串

若接口1返回的type为2,则接口2的参数type的值为11

若接口1返回的type为1、4、2048,则接口2的参数type的值为17
oid字符串

若接口1返回的type为2,则oid为图9或图12中返回值中的rid的值

若接口1返回的type为1、4、2048,则oid的值和动态ID一样,即dynamic_id
其他情况有知道的可以补充
mode字符串

3:按热度排序

2:按时间排序

接口测试

为了确定抓包得到的URL、参数、响应之中,哪些参数是必需的,就得进行验证,在真正用代码验证之前,可以先用浏览器或postman测试,我以浏览器为例。

根据请求流程中的三步,这里我省略第一步

第二步,chrome直接访问构造好的带参数的URL(即接口1),成功返回json数据,如图11。另外,火狐可以直接格式化返回的json数据,如图12,这样就不用拿着chrome显示的json数据再去格式化网站上手动格式化,我之前用的格式化网站是JSON在线校验格式化工具(Be JSON),这是新版界面,旧版是在线JSON校验格式化工具(Be JSON),也可以在旧版界面点击新版跳转到新版,如图13。

图11 - chrome访问接口1返回的json数据
图12 - 火狐访问接口1返回的json数据
图13 -?在旧版界面点击新版跳转到新版,也可以直接访问新版URL

?第三步,火狐直接访问构造好的带参数的URL(即接口2),成功返回json数据,如图14。另外,接口说明部分接口2处说带上callback和plat和_参数可能出错,如图15。

图14 - 火狐访问接口2返回的json数据
图15 - 火狐访问带上非必需的参数的URL时无法得到json数据

?修改接口2中参数的值,如表示页码的next,同样可以得到json数据,这里就不截图了。

测试完成,说明接口URL及参数都是正确的,都能得到正确的json数据。

?代码

整体流程就是请求流程部分中的那三步,最终你想爬取什么数据,去接口2返回的json数据中提取就行。页码可以根据for循环生成,即for循环中先生成一个页码,再构造URL,再请求并获取响应数据,再从响应数据中提取需要的数据。

另外,看完代码你可能有疑问,接口2返回的json数据中,已有all_count、is_end字段,如图14,为什么不直接用all_count除以每页的数据量得到页码总数进而得到最后一页的页码,或根据is_end判断是否是最后一页?

针对第一种,我测试时按每页20条数据算的,得到的页码总数与图4中那个样子不一致,而且后来发现有的动态请求一页得到18条数据,总之这种方法不行,起码我测试时不行,也可能我哪里做错了。

针对第二种,我测试时发现即便is_end为true,但依然可以请求下一页,只不过下一页得到的评论数据和本页相同;不过写文章时发现是测试时看错地方了,应该看与is_end的上一级cursor同级的replies,但是我代码里判断本页有没有数据确实是判断的这里的replies是否为空(在has_comment_data()中),所以也可以改成直接判断is_end是否为true,不过代码里我没改,因为最终都能判断本页是否有数据,是否到了最后一页。

# -*- coding: utf-8 -*-
# 上一行设置编码的代码不能删,否则有一句注释会报编码错误
import requests
import json
import math
import time
import os
import csv

headers1={ # 获取oid时使用
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36 Edg/96.0.1054.34',
}
headers2={ # 获取评论内容时使用
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36 Edg/96.0.1054.34',
    'Referer':'https://t.bilibili.com/'
}

# url_for_get_oid='https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id=567647071398441903' # 打开一条具体的动态时fiddler抓的原始URL。需从返回的信息中获取oid,dynamic_id是浏览器打开一条具体的动态时,地址栏末尾的一长串字符
url_for_get_oid='https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id={}'

# url_for_get_comment='https://api.bilibili.com/x/v2/reply/main?callback=jQuery3310715507182874241_1637852200168&jsonp=jsonp&next=0&type=11&oid=164467640&mode=3&plat=1&_=1637852200169' # 在一条具体的动态页,页面滚动到底部第一次触发"正在加载..."时fiddler抓的原始URL。浏览器地址栏直接访问这个链接显示B站自定义的404页面,但去掉开头的callback参数和末尾的_参数后,浏览器地址栏可直接访问,返回json数据,另外测试发现plat参数也可去掉
# https://api.bilibili.com/x/v2/reply/main?callback=jQuery3310715507182874241_1637852200168&jsonp=jsonp&next=2&type=11&oid=164467640&mode=3&plat=1&_=1637852200171 在一条具体的动态页,页面滚动到底部第二次触发"正在加载..."时fiddler抓的原始URL。除了末尾的_参数,就只有next参数变了,说明next参数和页码有关,因_参数不是必需的
url_for_get_comment='https://api.bilibili.com/x/v2/reply/main?jsonp=jsonp&next={next}&type={type}&oid={oid}&mode={mode}'

comment_detail_list=[] # 所有评论的详情,列表中每一条评论是一个字典


def check_json_data(r): # 检查返回的数据是否是json格式
    try:
        json_data=r.json()
    except json.decoder.JSONDecodeError as e:
        raise Exception('发生了异常,可能因返回的数据不是json格式,或检查传入的url_for_get_oid及参数是否有误,具体异常为:'+'json.decoder.JSONDecodeError: '+str(e)) # raise json.decoder.JSONDecodeError()需三个参数,所以直接raise Exception();而Exception()中异常提示只能是字符串类型,这里的e是JSONDecodeError类型,所以强制转换;另外,这种写法输出异常提示时没有异常类型,所以以字符串形式手动加上
    except:
        raise Exception('发生了未捕获的异常')
    else:
        return json_data


def get_oid_and_type_and_total_page_number(url_for_get_oid, dynamic_id): # 获取url_for_get_comment中用的oid和type的值,和页码总数
    r=requests.get(url=url_for_get_oid,headers=headers1)
    json_data=check_json_data(r)

    try:
        type=json_data['data']['card']['desc']['type']
        total_comment_number=json_data['data']['card']['desc']['comment']
    except KeyError: # 返回的数据中没有指定的键,导致获取不到对应的值,说明当前URL中没有需要的json数据,即URL有误
        raise KeyError('无效的dynamic_id,请重新运行输入')
    except:
        raise Exception('发生了未捕获的异常')
    
    if type==2:
        oid=json_data['data']['card']['desc']['rid']
        type_in_url_for_get_comment=11
    elif type in [1,4,2048]:
        oid=dynamic_id
        type_in_url_for_get_comment=17
    else:
        print(f'当前type变量的值{type}未被if捕获,需重新分析URL中type参数的值的所有可能')
        exit()
    
    total_page_number=math.ceil(total_comment_number/20)+19 # math.ceil(x)返回大于等于x的最小整数。每页20条数据,手动加19页防止页码总数由于各种原因计算错误,导致获取的数据不完整,且多出的页码由于没有数据会直接终止for循环
    return oid, type_in_url_for_get_comment, total_page_number


def get_mode(): # 获取url_for_get_comment中用的mode
    while True:
        sort_by=input('请输入评论的排序方式,3表示按热度排序,2表示按时间排序,最好不按时间排序,原因在save_data()的except语句的注释中:')
        if (sort_by=='3') or (sort_by=='2'):
            return sort_by
        else:
            print('无效的排序方式,请重新输入')


def has_comment_data(json_data): # 判断本页是否有评论,没有就直接终止for循环
    try:
        comment=json_data['data']['replies']
    except:
        raise Exception('发生了未捕获的异常')
    else:
        if comment:
            return True
        else:
            print('本页无数据,爬取结束')
            return False


def get_comment_data(json_data): # 获取评论内容,不含对评论的回复
    try:
        replies_list=json_data['data']['replies'] # 当前页的所有评论
    except:
        raise Exception('发生了未捕获的异常')
    else:
        for replies in replies_list:
            comment_detail={} # 当前评论的详情
            try:
                comment_detail['rpid']=replies['rpid'] # 当前评论的id,可能是replyid的缩写。可在一条具体的动态的URL末尾(即dynamic_id后,且删除默认自带的?tab=2)加#及这条动态中某个评论的rpid,访问后会在对应的评论下出现回复文本框,这样就能快速在所有评论中找到需要的评论(如动态下某条自己的想删但又没被点赞或回复的评论,若当前动态有大量评论,则想删的这条评论不一定能靠滚动浏览器加载评论找到)
                comment_detail['ctime']=time.strftime('%Y-%m-%d %H:%M',time.localtime(replies['ctime'])) # 评论时间
                comment_detail['mid']=replies['member']['mid'] # 用户ID
                comment_detail['uname']=replies['member']['uname'] # 用户名
                comment_detail['current_level']=replies['member']['level_info']['current_level'] # 用户等级
                comment_detail['message']=replies['content']['message'] # 评论内容
            except:
                raise Exception('发生了未捕获的异常')
            else:
                if comment_detail not in comment_detail_list: # 由于网络或其他原因(如反爬等),获取的数据可能有重复,需要去重
                    comment_detail_list.append(comment_detail)


def save_data(dynamic_id): # 保存所有评论的详情
    dir_path=os.path.abspath(os.path.dirname(__file__))
    file_name=f'B站动态ID{dynamic_id}的评论.csv'
    file_path=dir_path+'/'+file_name
    try:
        header=comment_detail_list[0].keys()
    except IndexError: # 按时间排序时由于部分页码(目前我发现的是页码为1和页码为2)返回的评论为空导致直接终止for循环,然后执行save_data(),若刚好这些页码是开始爬取的页码,则comment_detail_list中无数据,执行save_data()中try语句触发IndexError异常
        raise IndexError('comment_detail_list中无数据,数据保存失败')
    except:
        raise Exception('发生了未捕获的异常')
    else:
        # 不同保存方式的注释在bs4爬取招聘.py中
        with open(file_path,'w',newline='',encoding='utf-8') as f:
            writer=csv.DictWriter(f,fieldnames=header)
            writer.writeheader()
            writer.writerows(comment_detail_list)
        print('数据已保存到本地')

        '''
        string=json.dumps(comment_detail_list,ensure_ascii=False) # 将数据以json格式保存。ensure_ascii=False使json中的中文正常显示
        with open(dir_path+'/'+f'B站动态ID{dynamic_id}的评论.txt','w',encoding='utf-8') as fp:
            fp.write(string)
        print('数据已保存到本地')
        '''
        '''
        with open(dir_path+'/'+f'B站动态ID{dynamic_id}的评论.txt','w',encoding='utf-8') as f:
            f.write(str(comment_detail_list)) # 将列表直接转为字符串写入文件
        print('数据已保存到本地')
        '''


if __name__=='__main__':
    dynamic_id=input('请输入dynamic_id的值,它是浏览器打开一条具体的动态时,地址栏末尾的一长串字符:')
    url_for_get_oid=url_for_get_oid.format(dynamic_id)
    oid, type, total_page_number=get_oid_and_type_and_total_page_number(url_for_get_oid, dynamic_id)
    mode=get_mode()

    print(f'共有{total_page_number}页,这个数字已加入冗余,防止由于各种原因计算错误,导致获取的数据不完整,且多出的页码由于没有数据会直接终止for循环')
    start_page_number=int(input('请输入开始爬取的页码:')) # for循环中需是int类型
    end_page_number=int(input('请输入结束爬取的页码:'))
    if start_page_number==0: # next为0或1都返回第一页的数据
        start_page_number=1
    loop_count=0 # 记录循环执行次数
    for page_number in range(start_page_number,end_page_number+1):
        print(f'第{page_number}页开始爬取')
        comment_url=url_for_get_comment.format(next=page_number,type=type,oid=oid,mode=mode) # 注意等号左面不能写url_for_get_comment,会覆盖全局变量,导致next的值永远为第一轮循环中page_number的值,即start_page_number的值
        r=requests.get(url=comment_url,headers=headers2)
        json_data=check_json_data(r)
        if not has_comment_data(json_data):
            break
        get_comment_data(json_data)
        loop_count=loop_count+1
        print(f'第{page_number}页结束爬取')
        print() # 打印空行
        time.sleep(3)
    save_data(dynamic_id)
    print(f'获取数据共执行了{loop_count}次循环,每页20条数据(有时18条),假设最后一页不足20条,则去重后的数据量应大于({loop_count}-1)*20={(loop_count-1)*20}条,请打开文件核对数据量是否正确。若数据量不足,根据需求决定是否重新爬取,如针对有些需求,比完整数据稍微少一些也不影响。')
图16 - 结果

参考链接:

手把手教你爬取B站动态下的评论(看了必会,2个字简单) - 哔哩哔哩

爬虫实战 - 如何爬取B站视频评论? - phyger - 博客园

【python爬虫】爬取bilibili动态下面的评论 - 哔哩哔哩

bilibili动态评论的爬取: 通过b站开放的APL接口进行一个评论的爬取,只爬取的评论本身内容,发布评论的网友信息没有进行一个爬取

第四个与第三个内容一样,但可读性更好

我在get_comment_data()末尾,只是简单判断新爬到的数据是否在已有数据中已存在,关于数据去重,可看以下参考链接:

Python数据去重_夜空下的凝视-CSDN博客_python 去重

Python判断列表里是否有重复元素的三种方法_宁宁Fingerstyle的博客-CSDN博客_python重复元素判定
python常用的去重方式 - 星牧 - 博客园

左手用R右手Python系列8——数据去重与缺失值处理 - 云+社区 - 腾讯云

python数据去重(pandas)_Oliver、He的博客-CSDN博客_python数据去重

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2021-12-04 13:24:12  更:2021-12-04 13:26:48 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 2:35:28-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码