玩命加载中 . . .

哔哩哔哩课程评价指南开发经历——网络爬虫


背景

既然要从评论角度,对哔哩哔哩的课程进行优质与劣质的分类,那么其中一个很重要的环节就是爬取课程视频下方的评论。这些评论文本送入训练好的深度学习模型,进行情感分类,综合得到课程质量分类结果。本文主要记录个人在爬取哔哩哔哩视频评论的过程与心得。

目标

我们只需要爬取视频下方的一级评论。所有对评论的回复均不予关注。我们需要以表格形式将同一个视频的前200条评论展示出来。

需要爬取的部分

抓包爬取

点开查看网页源代码会发现,评论部分的源代码实际上是不存在的(换言之,展示出来的页面源代码不完整)。因此,需要进行抓包爬取。按下F12键,点击“网络”选项卡,查找含有reply关键字的响应资源,不难找到评论所在的响应资源。

找到评论区域文本资源

然后在标头中看一下它的请求链接:

请求链接

完整链接为https://api.bilibili.com/x/v2/reply/main?csrf=660751d5c3b843b85b100ece74c17f36&mode=3&oid=443609142&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=&type=1

另外其实可以发现,原哔哩哔哩视频的评论是分批获取的,因为用户在不断往下拉时,会相应地刷新出现新的评论。因此评论的资源请求也是同样地分批进行。这个道理可以类比计算机网络中的数据报分组。全部评论就如同完整的数据报,而哔哩哔哩中每一页存在20条评论,可以类比为每一个分组大小就是20,由此进行分组请求。事实上,请求信息的相应字段也证实了这一点。

评论分段获取

字段is_beginis_end分别指示了当前的分片是否为开头或者结尾。next字段指示的是下一个分片,可以理解为分片索引。理想状态是根据next的迭代自增爬取完全部的评论,但是上面的请求链接似乎压根不含这个字段!怎么办?

关键:获取oid

参考网络上众多爬取哔哩哔哩评论的教程,我们可以发现现在的评论请求链接有一些变化了(过去请求链接中显式地包含着next数组,显而易见地可以按照上面的理想情况爬取):

https://api.bilibili.com/x/v2/reply/main?jsonp=jsonp&next={num}&type=1&oid={oid}&mode=3&plat=1&_=1647577851745

不过不变的是,字段csrf在请求链接中确实没用,可以直接删去。于是请求链接变为:

https://api.bilibili.com/x/v2/reply/main?mode=3&oid=443609142&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=&type=1

但是原来的请求链接现在还能用吗?不妨试一试,显然此处的字段oid具有课程唯一性,不同的课程的全部评论可以通过该字段进行区分。那我们只需要拿到oid字段,填入原先的请求链接中的oid字段,看看能不能照样显示出网页文本。结果居然是可以的。那么我们仍然可以按照理想情况来进行爬取评论了:只需要填入课程的oid即可。

爬取到的内容实际上是json对象。所包含的有用评论信息在其中的子属性下。通过不断地迭代取值,便可以取出所需要的属性值。

爬取到的json对象

完整代码

考虑到后续需要封装到后端,我们实现一个爬取评论的函数即可。

def get_root_reply(oid):
    # 网页头
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36",
        "referer": "https://www.bilibili.com/"
    }
    num = 1
    # oid = 443609142
    replay_index = 1
    file = open('lanyin.txt', 'w', encoding='utf-8')
    replies = [] # 评论数组

    while True:
        # 只需要前10×20=200条评论
        if num == 11:
            break
        URL = f"https://api.bilibili.com/x/v2/reply/main?jsonp=jsonp&next={num}&type=1&oid={oid}&mode=3&plat=1&_=1647577851745"  # 获得网页源码
        respond = requests.get(URL, headers=headers)  # 获得源代码 抓包
        if (respond.status_code == 200):  # 如果响应为200就继续,否则退出
            respond.encoding = "UTF-8"
            html = respond.text
            json_html = json.loads(html)  # 把格式转化为json格式 一个是好让pprint打印,一个是好寻找关键代码

            if json_html['data']['replies'] is None or len(json_html['data']['replies']) == 0:
                break

            for reply_num in range(0, len(json_html['data']['replies'])):  # 一页只能读取20条评论
                reply = json_html['data']['replies'][reply_num]['content']['message']
                reply = post_filter(reply) # post_filter()暂时按下不表,他相当于是一个后处理过滤器
                print(f"[{reply_num}/{num}]: {reply}")
                replies.append(reply)
                file.write(str(replay_index) + '.' + reply + '\n')
                replay_index += 1
            num += 1
            time.sleep(0.5)
        else:
            file.close()
            return "Error"
    file.close()
    return replies

post_filter()函数负责去除评论中的换行符以及其中的emoji表情符转化后的[]文本:

def post_filter(text):
    """去除评论文本中的换行符,以及emoji经转化后的[]文本信息"""
    text = text.replace('\n', ',')
    return re.sub(u"\\{.*?}|\\[.*?]", "", text)

效果

做了一个简单的前端和后端,展示一下爬取到的评论效果:

展示爬取到的评论


文章作者: 鹿卿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 鹿卿 !
评论
  目录