前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >源代码和工具 | 2023 bilibili 视频弹幕爬虫,单条视频最多可爬取 10000 条弹幕

源代码和工具 | 2023 bilibili 视频弹幕爬虫,单条视频最多可爬取 10000 条弹幕

作者头像
月小水长
发布2023-08-17 13:58:59
1.5K1
发布2023-08-17 13:58:59
举报
文章被收录于专栏:月小水长

书接上回,b 站除了评论区出人才,弹幕也是 b 站文化富集之地,所以今天分享的是 b 站弹幕爬虫,文末同时附上源代码和 exe 工具链接。

测试了下这份代码/工具大概单个视频最多能爬到 10000 条左右的弹幕。

b 站没啥反爬的,带个 User-Agent 就能请求数据。

代码语言:javascript
复制
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36',
    'Referer': 'https://www.bilibili.com/'
}

和评论时间不同,弹幕时间戳是距离视频开始的秒数,所以需要额外写个解析。

代码语言:javascript
复制
def timeFormatter(param):
    minute = int(param) // 60
    second = float(param) - minute * 60
    return f'{str(minute).zfill(2)}:{str(second).zfill(5)}'

如果视频标题做文件名的话,也需要根据文件命名规则将视频标题处理之,也可以通过标题判断视频是否公开可见或者被删除。

代码语言:javascript
复制
def validateTitle(title):
    re_str = r"[\/\\\:\*\?\"\<\>\|]"  # '/ \ : * ? " < > |'
    new_title = re.sub(re_str, "_", title)  # 替换为下划线
    return new_title

请求弹幕数据主要注意下 F12 寻找弹幕的 url 地址,同时需要留意,弹幕请求的响应编码需要自适应编码。

代码语言:javascript
复制
def getHTML(url):
    try:
        response = requests.get(url=url, headers=headers,
                                timeout=timeout)
        # 自适应编码
        response.encoding = response.apparent_encoding
        return response.text
        # 下句作用等同于上两句
        # return response.text.encode(response.encoding).decode('utf-8')
    except:
        print(f"reqeuset url : {url} error...")
        print(traceback.format_exc())
        return None

用个 for 循环遍历要爬取的视频的 bv 号,实现一次爬取多个视频的弹幕的功能。

最后构造 dataframe,边爬取边保存。

以 b 站著名百大 up 【木鱼水心】的热门视频为例

标题:《水浒传》原著影视全解读!带你看懂奇书与神剧!(P1高俅发迹) 链接:https://www.bilibili.com/video/BV16F411B7Ek

抓取的结果字段包括时刻、弹幕文本两个字段,如下图所示。

一同抓取了木鱼水心关于四大名著最热的几个视频的弹幕,关于这些结果文件的获取可以查看今天的另外一篇推送。

代码语言:javascript
复制
# -*- coding: utf-8 -*-
# 作者:             inspurer(月小水长)
# 创建时间:          2020/10/30 23:16
# 运行环境           Python3.6+
# github            https://github.com/inspurer
# qq邮箱            2391527690@qq.com
# 微信公众号         月小水长(ID: inspurer)
# 文件备注信息       b 站弹幕爬虫

import requests
import re
from bs4 import BeautifulSoup
import operator
import traceback
import os
import pandas as pd
from lxml import etree
from time import sleep

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3970.5 Safari/537.36',
    'Referer': 'https://www.bilibili.com/'
}

timeout = 5


def getHTML(url):
    try:
        response = requests.get(url=url, headers=headers,
                                timeout=timeout)
        # 自适应编码
        response.encoding = response.apparent_encoding
        return response.text
        # 下句作用等同于上两句
        # return response.text.encode(response.encoding).decode('utf-8')
    except:
        print(f"reqeuset url : {url} error...")
        print(traceback.format_exc())
        return None


def parsePage(page):
    try:
        print("parsing...")
        html_ = etree.HTML(page)
        meta_title = html_.xpath('//meta[@name="title"]/@content')[0]
        if meta_title == '视频去哪了呢?_哔哩哔哩_bilibili':
            print(f'视频 404 not found')
            return [], '视频 404 not found'
        syntax = [':', '=']
        flag = 0
        keys = re.findall(r'"cid":[\d]*', page)
        if not keys:
            keys = re.findall(r'cid=[\d]*', page)
            flag = 1
        comments, title = {}, None
        keys = [keys[1]]
        for index, item in enumerate(keys):
            key = item.split(syntax[flag])[1]
            print(f'{index + 1}/{len(keys)}: {key}')
            comment_url = f'https://comment.bilibili.com/{key}.xml'  # 弹幕地址
            comment_text = getHTML(comment_url)
            bs4 = BeautifulSoup(comment_text, "html.parser")
            if not title:
                title = BeautifulSoup(page, "html.parser").find('h1').get_text().strip()
            for comment in bs4.find_all('d'):
                time = float(comment.attrs['p'].split(',')[0])
                time = timeFormatter(time)
                comments[time] = comment.string
        sorted_comments = sorted(comments.items(), key=operator.itemgetter(0))  # 排序
        comments = dict(sorted_comments)
        print("parse finish")
        return comments, title
    except:
        print("parse error")
        print(traceback.format_exc())


def validateTitle(title):
    re_str = r"[\/\\\:\*\?\"\<\>\|]"  # '/ \ : * ? " < > |'
    new_title = re.sub(re_str, "_", title)  # 替换为下划线
    return new_title


def timeFormatter(param):
    minute = int(param) // 60
    second = float(param) - minute * 60
    return f'{str(minute).zfill(2)}:{str(second).zfill(5)}'


def main():
    bvs = ['BV1mL411z7Kf', 'BV1CC4y1a7ee', 'BV1hx411e7KP', 'BV16F411B7Ek']
    for bv in bvs:
        url = f"https://www.bilibili.com/video/{bv}"
        save_folder = "BarRage"
        if not os.path.exists(save_folder):
            os.mkdir(save_folder)
        comments, title = parsePage(getHTML(url))
        if len(comments) == 0:
            continue
        title = validateTitle(title)
        df = pd.DataFrame({'时刻': list(comments.keys()), '弹幕文本': list(comments.values())})
        df.drop_duplicates(subset=['时刻', '弹幕文本'], keep='first', inplace=True)
        df.to_csv(f"{save_folder}/{title}.csv", index=False, encoding='utf-8-sig')
        print(f'已经保存 {df.shape[0]} 条弹幕到 {save_folder}/{title}.csv\n\n')

        sleep(10)


if __name__ == '__main__':
    main()

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 月小水长 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档