爬取B站视频弹幕的简易教程(上)

1.项目介绍

最近一个粉丝找我帮忙,弄一个关于B站视频弹幕的爬虫,我将记录完成过程中遇到的问题,和参考的网址,由于本人在爬虫方面就是个小趴菜,不会逆向解密,对CSS、html等知识掌握的不充分,所以如有更好的爬取方法,欢迎交流!

2.准备工作

1.准备一个浏览器,这里以Chrome浏览器为例,安装必要的库。
2.准备一个解密工具,如下图所示,如果仅仅只需要获取弹幕的话,确实可以不考虑解密,但是如果想要获取发弹幕的用户标识,发送弹幕的视频时间点等,就需要解密,这里我参考了CSDN上的这位大佬,链接在此
图一
3.获取seg.so文件,这一点非常希望能够有大佬一起优化这一步,seg.so文件是分段的,目前只知道长视频,分段的比较多,达到一个视频的时间戳会出现一个文件,如果一个一个保存的话,短视频还好,长视频的话,规模太大了。这里借助AI生成了一份使用Selenium工具模拟浏览器行为并捕获所有网络请求的代码,目前体验下来感觉还行,当然还有需要优化的地方,但是作为一个爬虫小白,看到有如此效果,已经觉得非常的牛了。

3.selenium配置

详细可以参考这一篇文章,以Chrome浏览器为例,首先查看浏览器版本,在浏览器的地址栏键入 Chrome://version 可以查看版本。
Image Name
选择对应版本号的驱动版本,下载地址:CNPM Binaries Mirror (npmmirror.com)

Image Name

我这里选择109.0.5414.74,这个版本和我的版本最接近,然后就是配置环境,由于我配置环境已经是好几年前的事情了,我这里放一个网址,大家自行去学一下,这里不多赘述。
然后就是安装selenium,使用pip下载
pip install selenium
这样就基本配置完成了,当然如果要学习selenium的基础操作,可以去其他博主那里学一下。

4.配置Protobuf 环境

还是参考这位博主的文章,他提到了这种加密格式是Protobuf,这个格式为二进制编码传输,如果要解密的话,可以下载Protobuf 的编译器,下载链接,我的是win64,所以选择win64的下载,如图二所示 。
图二
下载后,进行解压放在指定的目录,然后配置环境,借助该博主的图参考一下。

Image Name
在cmd中输入protoc来验证我们的配置是否成功,如果你的控制台输出结果如下,那么环境配置成功。

Image Name

5.生成编译文件

首先去GitHub上面去下载dm.proto
Image Name
下载完成后放入protoc-3.17.3-win64\bin目录 输入:

protoc --python_out=. dm.proto  

Image Name
这样就会生成一个dm_pb2.py文件。

Image Name
将上一步编译出来的dm_pb2.py文件放在脚本的同一级,然后就ke可以解析本地的so文件了。

6.获取so文件

我这里介绍一下我之前的思路,再介绍一下AI生成的代码。

6.1我最初的思路

这里以B站视频,苦练李兰香博主的视频为例:惊天大翻盘!IG 2-1 JDG 赛后数据雷达图+虎扑现状 LPL第二赛段

首先,打开浏览器开发者选项,F12,或者鼠标右键然后点击检查,然后点击Network,刷新一下。


然后在搜索框,输入seg.so。

然后点击获得的seg.so文件,然后点击Preview,如图所示:

这样可以看到获取了弹幕,当然需要使用之前提到的解密工具,先保存这个so文件,点击Headers,然后复制URL,如图:

把URL复制在浏览器的搜索栏中,就能下载了这个so文件了。

当然,我们可以在这一步往下拖,看看有多少条弹幕;

然后视频时长是3分钟,可能弹幕被分段了,所以需要找到下一个分段点,此视频为例,在2分钟左右的时候,又跳出一个新的so文件,如图所示:

这一条so文件有271条弹幕,和第一条的so文件合并的话,肯定会有重复的,所以在合并成xlsx文件的时候,我有一个去重的过程,当然,这是后话,现在先将获取so文件。
当然,这仅仅是一条3分钟的视频,如果是一部电影呢?分段的弹幕肯定就很多,如果人工一条条筛选,一个个节点去试的话,不仅耗费人力和时间,还可能会存在遗漏的情况,有没有一种简单又自动的方式?有的兄弟,有的。

第一种方式,通过调用历史信息:

详细可以参考这个博主的内容,链接地址,当然,这个问题就是会长时间多次访问B站,有被检测的风险,而且获取历史信息,必须要设置cookie,所以,我个人认为不一定安全,结合AI优化,发现提取不到用户的信息和发弹幕的时间戳,我也很疑惑,当然本人水平没那么高,所以这个方法,我就先否决了,当然感兴趣的读者,可以自行研究一下,这个方法绝对比手动寻找节点方便的多,而且不涉及解密(这一点,第二种方法我会提到)


第二种方法,JS逆向,当然,这个步骤,我是不会的,有感兴趣的大神,可以研究一下,我说一下我的发现:


可以看到,这两段弹幕的url是不一样的,其中的难点就是如何准确的获取加密后的签名w_rid,这一点,我实在破解不来,大家可以参考一下,这个B站视频:视频链接,所以这个方法我这里也是否决了,当然,我不知道为啥方法一,可以只调整date,不需要解密,如果有大牛看到,欢迎交流。


第三种方法,调用bilibili_api,可以参考这几位博主的,和鲸社区博主——凹凸数据CSDN博主——bBADAS,这个方法应该是比较简单的,站在前人的肩膀上完成的,由于我研究这个方法也不是非常透彻,感兴趣的可以去看看这两位博主的使用说明。


第四种方法,在需要获取视频的URL的b前面加i,以https://www.bilibili.com/video/BV1Up5EznE2d/?spm_id_from=333.337.search-card.all.click&vd_source=012f78c4d90354a5aa6d7d65da206014 为例,加i后变成:
https://www.ibilibili.com/video/BV1Up5EznE2d/?spm_id_from=333.337.search-card.all.click&vd_source=012f78c4d90354a5aa6d7d65da206014 如图所示:

复制弹幕地址打开后是这样的:

可以看到获取了421条弹幕(因为我是边写变弄的,而这场比赛距离我目前写到这里,已经过去半天了,可能热度比较大,弹幕数量有可能变多),这个方法本质上就是以前的旧方法,获取XML的URL,然后爬虫,感兴趣的可以自己试一下,这些都不是今天的大头。


方法是多种多样的,无论黑猫白猫,抓到耗子就是好猫,当然放在爬虫里,不管什么方法(别违反网站的规则,别给网站带来困扰的前提下),只要简单快速且全面的爬虫,就是好爬虫。

6.2AI提供的思路与代码

本代码由claude提供,使用Selenium或Playwright等工具模拟浏览器行为,让它自动播放视频并捕获所有网络请求。
核心思路:
1.模拟真实播放行为:使用Selenium驱动浏览器真实播放视频
2.监控网络请求:捕获浏览器发起的弹幕请求
3.加速播放过程:通过设置高倍速播放减少等待时间
4.强制请求所有段:计算视频总段数,强制请求所有弹幕段
5.从头开始播放:确保视频从头开始播放,不错过任何弹幕
6.智能检测视频结束:自动识别视频何时结束,避免继续播放下一视频


当然,这些思路是经过一个多小时才优化出来的,一开始没考虑倍速,没考虑需要登录等行为,但是不得不佩服AI现在的发展,只需要告诉它,它自己就能够完善了。

代码详解:
1. 初始化与基本配置

def __init__(self, video_url, save_dir=None, playback_speed=2.0, max_duration=None, force_segments=True):  
    # 视频URL与基本设置  
    self.video_url = video_url  
    self.playback_speed = min(max(0.5, playback_speed), 16.0)  # 限制在0.5-16.0之间  
    self.video_info = {"title": "未知视频", "bvid": "unknown", "duration": 0}  
    
    # 爬取状态追踪  
    self.danmaku_requests = []  
    self.downloaded_urls = set()  # 跟踪已下载的URL  
    self.downloaded_segments = set()  # 跟踪已下载的分段号  
    self.file_counter = 1  # 文件计数器  
    
    # 播放控制参数  
    self.max_duration = max_duration  # 最大监控时长  
    self.video_ended = False  # 视频是否已结束  
    self.force_segments = force_segments  # 是否强制请求所有分段  
    
    # 配置浏览器选项  
    self.chrome_options = Options()  
    self.chrome_options.add_argument('--disable-gpu')  
    self.chrome_options.add_argument('--no-sandbox')  
    self.chrome_options.add_argument('--disable-application-cache')  
    # 启用性能日志捕获  
    self.chrome_options.set_capability('goog:loggingPrefs', {'performance': 'ALL'})  

这部分代码设置了爬虫的基本配置,包括视频URL、播放速度、保存目录等。重要的是追踪下载状态的变量和浏览器配置,特别是启用性能日志捕获,这是获取弹幕请求的关键。


2. 确保视频从头开始播放

def ensure_video_starts_from_beginning(self):  
    """确保视频从头开始播放"""  
    try:  
        # 获取当前播放时间  
        current_time = self.driver.execute_script("""  
            var video = document.querySelector('video');  
            return video ? video.currentTime : 0;  
        """)  
        
        # 如果不是从头开始,需要重置  
        if current_time > 1:  
            print(f"检测到视频从 {current_time} 秒处开始播放,尝试重置到开始位置")  
            
            # 直接使用JavaScript重置视频时间  
            self.driver.execute_script("""  
                var video = document.querySelector('video');  
                if (video) {  
                    video.currentTime = 0;  
                }  
            """)  
            
            # 验证重置结果  
            new_time = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                return video ? video.currentTime : 0;  
            """)  
            
            if new_time < 1:  
                print(f"已成功将视频重置到起始位置")  
            else:  
                # 如果JavaScript重置失败,尝试刷新页面  
                refresh_url = f"{self.video_url}?t={int(time.time())}"  
                self.driver.get(refresh_url)  
        else:  
            print(f"视频已在起始位置: {current_time} 秒")  
            
        return True  
    except Exception as e:  
        print(f"确保视频从头开始播放时出错: {str(e)}")  
        return False  

这个函数确保视频从头开始播放,避免因为B站的播放记忆功能导致错过前面部分的弹幕。它先检测当前播放位置,如果不是起始位置,则使用JavaScript直接重置video元素的currentTime属性,必要时会刷新页面。


3. 监控网络请求捕获弹幕

def process_performance_logs(self):  
    """处理性能日志并提取弹幕请求"""  
    logs = self.driver.get_log('performance')  
    
    for log in logs:  
        try:  
            log_data = json.loads(log['message'])['message']  
            
            # 检查是否是网络请求  
            if 'Network.requestWillBeSent' not in log_data['method']:  
                continue  
            
            request_url = log_data['params']['request']['url']  
            
            # 过滤弹幕请求  
            if 'api.bilibili.com/x/v2/dm/wbi/web/seg.so' in request_url:  
                # 检查是否已存在相同的请求  
                if request_url not in [req['url'] for req in self.danmaku_requests]:  
                    self.danmaku_requests.append({  
                        'url': request_url,  
                        'timestamp': time.time(),  
                        'method': 'natural'  
                    })  
                    print(f"发现新的弹幕请求: {request_url}")  
                    
                    # 从URL中提取segment_index以记录  
                    segment_match = re.search(r'segment_index=(\d+)', request_url)  
                    if segment_match:  
                        segment_index = segment_match.group(1)  
                        self.downloaded_segments.add(segment_index)  
                    
                    # 立即下载弹幕数据  
                    self.download_danmaku(request_url)  
                    
        except Exception as e:  
            continue  

这个函数是爬虫的核心,它通过Chrome浏览器的性能日志API监控所有网络请求,过滤出弹幕请求(包含seg.so的URL),并将其保存下来。当检测到新的弹幕请求时,会立即下载对应的弹幕数据。


4. 强制请求所有弹幕段

def force_request_all_segments(self):  
    """强制请求所有弹幕段落"""  
    try:  
        print("开始尝试强制请求所有弹幕段落...")  
        
        # 获取视频时长  
        duration = self.driver.execute_script("""  
            var video = document.querySelector('video');  
            return video ? video.duration : 0;  
        """)  
        
        # 计算需要请求的段数 (B站弹幕段每段6分钟/360秒)  
        segment_duration = 360  
        total_segments = int(duration / segment_duration) + 1  
        
        print(f"视频时长: {int(duration)}秒,预计有{total_segments}个弹幕段")  
        
        # 获取弹幕API模板  
        danmaku_api_template = self.extract_danmaku_api_template()  
        if not danmaku_api_template:  
            return False  
        
        # 请求所有未下载的段  
        for segment_index in range(1, total_segments + 1):  
            # 检查该段是否已下载  
            if str(segment_index) in self.downloaded_segments:  
                print(f"段 {segment_index}/{total_segments} 已下载,跳过")  
                continue  
                
            # 构造完整URL  
            segment_url = danmaku_api_template + str(segment_index)  
            
            # 发送请求并下载  
            self.download_segment(segment_url, segment_index, total_segments)  
            
            # 防止请求过快  
            time.sleep(0.5)  
        
        print(f"强制请求完成,共请求 {total_segments} 个段,成功下载 {len(self.downloaded_segments)} 个段")  
        return True  
            
    except Exception as e:  
        print(f"强制请求弹幕段落时出错: {e}")  
        return False  

这个函数是本爬虫的一个关键创新:它不仅依赖浏览器自然播放时发出的请求,而是主动计算视频有多少弹幕段,并强制请求所有段的弹幕。这确保了我们能获取到完整的弹幕数据,包括用户可能跳过的部分。


5. 智能检测视频结束

def check_if_video_ended(self):  
    """检查视频是否已经结束"""  
    try:  
        # 方法1: 直接检查视频是否已结束  
        ended = self.driver.execute_script("""  
            var video = document.querySelector('video');  
            if (video) {  
                // 检查是否结束或即将结束  
                return video.ended || (video.currentTime >= video.duration - 0.5);  
            }  
            return false;  
        """)  
        
        if ended:  
            print("检测到视频已播放完毕")  
            return True  
            
        # 方法2: 检查是否出现了"重新播放"按钮  
        try:  
            replay_button = self.driver.find_element(By.CSS_SELECTOR, ".bilibili-player-video-btn-start.video-state-pause")  
            if "replay" in replay_button.get_attribute("class") or "重新" in replay_button.text:  
                print("检测到重新播放按钮")  
                return True  
        except:  
            pass  
            
        # 方法3: 检查是否出现了推荐的下一个视频  
        try:  
            next_video = self.driver.find_element(By.CSS_SELECTOR, ".bilibili-player-electric-panel")  
            if next_video.is_displayed():  
                print("检测到下一个视频推荐面板")  
                return True  
        except:  
            pass  
            
        # 方法4: 检查播放进度是否长时间不变且接近视频末尾  
        current_time = self.driver.execute_script("""  
            var video = document.querySelector('video');  
            return video ? video.currentTime : 0;  
        """)  
        duration = self.driver.execute_script("""  
            var video = document.querySelector('video');  
            return video ? video.duration : 0;  
        """)  
        
        if abs(current_time - self.last_progress) < 0.1:  
            self.progress_unchanged_count += 1  
            if self.progress_unchanged_count > 5 and (duration - current_time) < 5:  
                print(f"检测到视频进度长时间未变且接近末尾")  
                return True  
        else:  
            self.progress_unchanged_count = 0  
            self.last_progress = current_time  
        
        return False  
    except Exception as e:  
        print(f"检查视频是否结束时出错: {e}")  
        return False  

这个函数用多种方法智能检测视频是否已结束,解决了B站视频结束后可能自动播放下一个视频或暂停的问题。它不仅检查视频元素的ended状态,还监测UI变化、推荐面板出现,以及播放进度是否停滞在接近结束的位置。


6. 主流程控制

def start(self):  
    """启动爬虫并监控到视频结束"""  
    # 初始化和登录处理  
    # ...  
    
    try:  
        # 加载视频并准备播放  
        self.get_video_info()  
        self.ensure_video_starts_from_beginning()  
        self.disable_autoplay_next()  
        self.ensure_video_is_playing()  
        self.set_playback_speed()  
        
        print(f"开始监控视频播放,以 {self.playback_speed}x 倍速播放")  
        
        # 主监控循环  
        start_time = time.time()  
        while True:  
            # 检查是否超过最大监控时长  
            elapsed = int(time.time() - start_time)  
            if self.max_duration and elapsed > self.max_duration:  
                print(f"已达到最大监控时长 {self.max_duration} 秒,停止监控")  
                break  
            
            # 检查视频是否已经结束  
            if self.check_if_video_ended():  
                self.video_ended = True  
                print("视频已播放完毕,停止监控")  
                break  
            
            # 确保视频持续播放  
            if not self.check_if_video_is_playing() and not self.video_ended:  
                self.ensure_video_is_playing()  
                self.set_playback_speed()  # 重新检查倍速设置  
            
            # 处理网络请求获取弹幕  
            self.process_performance_logs()  
            
            # 定期打印状态  
            self.print_status_if_needed(elapsed)  
            
            time.sleep(3)  # 检查间隔  
        
        # 视频结束后,强制请求所有段  
        if self.force_segments:  
            print("\n视频监控结束,开始强制请求所有弹幕段...")  
            self.force_request_all_segments()  
            
    except Exception as e:  
        print(f"发生错误: {e}")  
    finally:  
        # 保存结果  
        self.save_results()  
        self.driver.quit()  

主流程控制是整个爬虫的骨架,它先确保视频从头开始播放并设置倍速,然后持续监控视频播放状态和网络请求,捕获弹幕数据。当视频结束或达到最大监控时长后,会执行强制请求所有弹幕段的操作,以确保获取完整数据。

完整代码:

import time  
import json  
import os  
import re  
import requests  
from selenium import webdriver  
from selenium.webdriver.chrome.service import Service  
from selenium.webdriver.chrome.options import Options  
from selenium.webdriver.common.by import By  
from selenium.webdriver.support.ui import WebDriverWait  
from selenium.webdriver.support import expected_conditions as EC  
from webdriver_manager.chrome import ChromeDriverManager  
from urllib.parse import urlparse, parse_qs  
from selenium.webdriver.common.action_chains import ActionChains  


class BilibiliDanmakuCrawler:  
    def __init__(self, video_url, save_dir=None, playback_speed=2.0, max_duration=None, force_segments=True):  
        """  
        初始化B站弹幕爬虫  

        参数:  
        video_url: B站视频URL  
        save_dir: 保存目录,可以自定义,默认使用视频BV号  
        playback_speed: 播放速度(0.5-16.0之间)  
        max_duration: 最大监控时长(秒),防止视频检测失败时无限等待,默认为None(根据视频长度自动计算)  
        force_segments: 是否强制请求所有分段弹幕,即使没有自然播放到  
        """  
        self.video_url = video_url  
        self.playback_speed = min(max(0.5, playback_speed), 16.0)  # 限制在0.5-16.0之间  
        self.video_info = {"title": "未知视频", "bvid": "unknown", "duration": 0}  
        self.danmaku_requests = []  
        self.downloaded_urls = set()  # 跟踪已下载的URL  
        self.downloaded_segments = set()  # 跟踪已下载的分段号  
        self.file_counter = 1  # 文件计数器,用于命名  
        self.max_duration = max_duration  # 最大监控时长  
        self.video_ended = False  # 视频是否已经结束  
        self.last_progress = 0  # 上次记录的视频进度  
        self.progress_unchanged_count = 0  # 进度不变的计数  
        self.force_segments = force_segments  # 是否强制请求所有分段  

        # 提取视频ID  
        self.extract_video_id(video_url)  

        # 设置保存目录  
        if save_dir is None:  
            # 默认使用BV号作为目录名  
            self.save_dir = self.video_info['bvid']  
        else:  
            self.save_dir = save_dir  

        # 创建保存目录  
        if not os.path.exists(self.save_dir):  
            os.makedirs(self.save_dir)  
        print(f"弹幕将保存到目录: {self.save_dir}")  

        # 设置Chrome选项  
        self.chrome_options = Options()  
        # self.chrome_options.add_argument('--headless')  # 无头模式,取消注释以启用  
        self.chrome_options.add_argument('--disable-gpu')  
        self.chrome_options.add_argument('--no-sandbox')  
        self.chrome_options.add_argument('--disable-dev-shm-usage')  
        self.chrome_options.add_argument('--autoplay-policy=no-user-gesture-required')  # 允许自动播放  
        # 禁用自动播放下一个视频  
        self.chrome_options.add_argument('--autoplay-policy=user-required-for-cross-origin-autoplay')  
        # 禁用缓存,防止加载上次播放进度  
        self.chrome_options.add_argument('--disable-application-cache')  
        self.chrome_options.add_argument('--disk-cache-size=0')  
        self.chrome_options.add_experimental_option('excludeSwitches', ['enable-automation'])  
        self.chrome_options.add_experimental_option('useAutomationExtension', False)  

        # 启用Chrome的性能日志,用于捕获网络请求  
        self.chrome_options.set_capability('goog:loggingPrefs', {'performance': 'ALL'})  

        # 加载cookies(如果有)  
        self.cookies_path = 'bilibili_cookies.json'  

        # 初始化WebDriver  
        self.driver = webdriver.Chrome(  
            service=Service(ChromeDriverManager().install()),  
            options=self.chrome_options  
        )  

    def extract_video_id(self, url):  
        """从URL中提取视频ID和类型"""  
        try:  
            # 尝试提取BV号  
            bv_match = re.search(r'BV\w+', url)  
            if bv_match:  
                self.video_info["bvid"] = bv_match.group(0)  
            else:  
                # 尝试从URL参数中提取  
                parsed_url = urlparse(url)  
                query_params = parse_qs(parsed_url.query)  

                if 'bvid' in query_params:  
                    self.video_info["bvid"] = query_params['bvid'][0]  
                elif 'aid' in query_params:  
                    self.video_info["aid"] = query_params['aid'][0]  
                    self.video_info["bvid"] = f"av{query_params['aid'][0]}"  

            print(f"已提取视频ID: {self.video_info['bvid']}")  
        except Exception as e:  
            print(f"提取视频ID时出错: {e}")  

    def get_video_info(self):  
        """获取视频标题和时长等信息"""  
        try:  
            # 等待标题元素加载  
            title_element = WebDriverWait(self.driver, 10).until(  
                EC.presence_of_element_located((By.CSS_SELECTOR, "h1.video-title"))  
            )  
            self.video_info["title"] = title_element.text.strip()  
            print(f"视频标题: {self.video_info['title']}")  

            # 获取视频总时长(秒)  
            try:  
                time.sleep(2)  # 等待视频加载完成  
                duration = self.driver.execute_script("""  
                    var video = document.querySelector('video');  
                    return video ? video.duration : 0;  
                """)  

                if duration and duration > 0:  
                    self.video_info["duration"] = duration  
                    minutes = int(duration // 60)  
                    seconds = int(duration % 60)  
                    print(f"视频时长: {minutes}分{seconds}秒 ({int(duration)}秒)")  

                    # 尝试获取总弹幕数  
                    try:  
                        # 查找弹幕数量标签  
                        danmaku_count_elem = self.driver.find_element(By.CSS_SELECTOR, ".dm")  
                        if danmaku_count_elem:  
                            danmaku_text = danmaku_count_elem.text  
                            count_match = re.search(r'(\d+(?:\.\d+)?[万]?)', danmaku_text)  
                            if count_match:  
                                count_str = count_match.group(1)  
                                if '万' in count_str:  
                                    count = float(count_str.replace('万', '')) * 10000  
                                else:  
                                    count = float(count_str)  
                                self.video_info["danmaku_count"] = int(count)  
                                print(f"B站显示弹幕数量: 约{self.video_info['danmaku_count']}条")  
                    except Exception as e:  
                        print(f"获取弹幕数量时出错: {e}")  

                    # 如果没有设置最大监控时长,则基于视频时长计算(额外增加30秒缓冲)  
                    if self.max_duration is None:  
                        # 根据倍速调整监控时长  
                        self.max_duration = (duration / self.playback_speed) + 30  
                        print(f"根据视频时长自动设置最大监控时间: {int(self.max_duration)}秒")  
                else:  
                    print("无法获取视频时长,将使用默认最大监控时间")  
                    if self.max_duration is None:  
                        self.max_duration = 1800  # 默认30分钟  
            except Exception as e:  
                print(f"获取视频时长时出错: {e}")  
                if self.max_duration is None:  
                    self.max_duration = 1800  # 默认30分钟  

            # 保存视频信息到文件  
            with open(os.path.join(self.save_dir, 'video_info.txt'), 'w', encoding='utf-8') as f:  
                f.write(f"视频ID: {self.video_info['bvid']}\n")  
                f.write(f"视频标题: {self.video_info['title']}\n")  
                f.write(f"视频时长: {int(self.video_info.get('duration', 0))}秒\n")  
                if 'danmaku_count' in self.video_info:  
                    f.write(f"B站显示弹幕数量: 约{self.video_info['danmaku_count']}条\n")  
                f.write(f"视频URL: {self.video_url}\n")  

        except Exception as e:  
            print(f"获取视频信息时出错: {e}")  
            if self.max_duration is None:  
                self.max_duration = 1800  # 默认30分钟  

    def load_cookies(self):  
        """加载已保存的cookies"""  
        if os.path.exists(self.cookies_path):  
            with open(self.cookies_path, 'r') as f:  
                cookies = json.load(f)  
                for cookie in cookies:  
                    self.driver.add_cookie(cookie)  
            print("已加载cookies")  
            return True  
        return False  

    def save_cookies(self):  
        """保存当前cookies"""  
        cookies = self.driver.get_cookies()  
        with open(self.cookies_path, 'w') as f:  
            json.dump(cookies, f)  
        print("已保存cookies")  

    def login_check(self):  
        """检查是否需要登录"""  
        try:  
            # 尝试检测登录状态  
            self.driver.get("https://www.bilibili.com")  
            time.sleep(3)  

            # 检查是否存在用户头像元素,这通常表示已登录  
            try:  
                avatar = self.driver.find_element(By.CLASS_NAME, "bili-avatar-face")  
                print("已检测到登录状态")  
                return True  
            except:  
                print("未检测到登录状态,需要手动登录")  
                return False  
        except Exception as e:  
            print(f"登录检查出错: {e}")  
            return False  

    def manual_login(self, timeout=120):  
        """引导用户手动登录"""  
        print("请在浏览器中完成登录操作...")  
        print(f"您有 {timeout} 秒的时间来完成登录")  

        start_time = time.time()  
        while time.time() - start_time < timeout:  
            try:  
                avatar = self.driver.find_element(By.CLASS_NAME, "bili-avatar-face")  
                print("登录成功!")  
                self.save_cookies()  
                return True  
            except:  
                time.sleep(3)  

        print("登录超时,请重试")  
        return False  

    def ensure_video_starts_from_beginning(self):  
        """确保视频从头开始播放"""  
        try:  
            print("尝试让视频从头开始播放...")  

            # 获取当前播放时间  
            current_time = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                return video ? video.currentTime : 0;  
            """)  

            # 如果不是从头开始,需要重置  
            if current_time > 1:  
                print(f"检测到视频从 {current_time} 秒处开始播放,尝试重置到开始位置")  

                # 方法1: 直接使用JavaScript重置视频时间 - 最可靠的方法  
                try:  
                    self.driver.execute_script("""  
                        var video = document.querySelector('video');  
                        if (video) {  
                            video.currentTime = 0;  
                            console.log('已通过JavaScript将播放时间重置为0');  
                        }  
                    """)  
                    time.sleep(1.5)  
                    print("已通过JavaScript重置视频进度")  
                except Exception as e:  
                    print(f"JavaScript重置播放时间失败: {e}")  

                # 方法2: 尝试使用快捷键重置 (按Home键)  
                try:  
                    from selenium.webdriver.common.keys import Keys  
                    webdriver.ActionChains(self.driver).send_keys(Keys.HOME).perform()  
                    print("已发送Home键尝试重置视频")  
                    time.sleep(1)  
                except Exception as e:  
                    print(f"发送Home键失败: {e}")  

                # 检查是否已重置到起始位置  
                new_time = self.driver.execute_script("""  
                    var video = document.querySelector('video');  
                    return video ? video.currentTime : 0;  
                """)  

                if new_time < 1:  
                    print(f"已成功将视频重置到起始位置: {new_time} 秒")  
                else:  
                    print(f"无法重置视频到起始位置,当前位置: {new_time} 秒")  

                    # 最后尝试: 刷新页面并重新加载  
                    print("尝试刷新页面以重置视频进度...")  
                    # 添加t参数强制刷新缓存  
                    refresh_url = f"{self.video_url}{'&' if '?' in self.video_url else '?'}t={int(time.time())}"  
                    self.driver.get(refresh_url)  
                    time.sleep(3)  

                    # 等待视频重新加载  
                    WebDriverWait(self.driver, 10).until(  
                        EC.presence_of_element_located((By.TAG_NAME, "video"))  
                    )  

                    # 再次检查播放位置  
                    final_time = self.driver.execute_script("""  
                        var video = document.querySelector('video');  
                        return video ? video.currentTime : 0;  
                    """)  
                    print(f"页面刷新后的视频位置: {final_time} 秒")  
            else:  
                print(f"视频已在起始位置: {current_time} 秒")  

            return True  
        except Exception as e:  
            print(f"确保视频从头开始播放时出错: {str(e)}")  
            # 出错后尝试刷新页面作为备选方案  
            try:  
                refresh_url = f"{self.video_url}{'&' if '?' in self.video_url else '?'}t={int(time.time())}"  
                self.driver.get(refresh_url)  
                time.sleep(3)  
                print("已刷新页面以尝试从头开始")  
            except:  
                pass  
            return False  

    def set_playback_speed(self):  
        """设置视频播放速度"""  
        try:  
            print(f"尝试将播放速度设置为 {self.playback_speed}x")  

            # 直接使用JavaScript设置速度 - 最可靠的方法  
            result = self.driver.execute_script(f"""  
                try {{  
                    var video = document.querySelector('video');  
                    if(video) {{  
                        video.playbackRate = {self.playback_speed};  
                        console.log("已设置播放速度为 " + {self.playback_speed});  
                        return true;  
                    }}  
                    return false;  
                }} catch(e) {{  
                    console.error("设置播放速度失败:", e);  
                    return false;  
                }}  
            """)  

            if result:  
                print(f"已通过JavaScript设置速度为 {self.playback_speed}x")  
            else:  
                print("无法找到视频元素设置速度")  

            # 验证播放速度  
            time.sleep(0.5)  # 给一点时间让设置生效  
            actual_speed = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                return video ? video.playbackRate : 0;  
            """)  

            print(f"当前播放速度: {actual_speed}x")  

            # 如果通过JavaScript设置失败,尝试找备选方案  
            if abs(actual_speed - self.playback_speed) > 0.1:  
                print("JavaScript设置速度不准确,尝试其他方法")  

                # 尝试使用键盘快捷键  
                try:  
                    # 通过点击视频区域聚焦  
                    video = self.driver.find_element(By.TAG_NAME, "video")  
                    video.click()  
                    time.sleep(0.5)  

                    # 使用Shift+> 提速或 Shift+< 减速  
                    from selenium.webdriver.common.keys import Keys  
                    from selenium.webdriver.common.action_chains import ActionChains  

                    # 重置为1倍速  
                    self.driver.execute_script("""  
                        var video = document.querySelector('video');  
                        if(video) { video.playbackRate = 1.0; }  
                    """)  
                    time.sleep(0.5)  

                    # 根据目标速度决定加速还是减速  
                    if self.playback_speed > 1:  
                        # 每次加速通常是变为2倍、4倍、8倍等  
                        times_to_press = 0  
                        if self.playback_speed <= 2:  
                            times_to_press = 1  
                        elif self.playback_speed <= 4:  
                            times_to_press = 2  
                        elif self.playback_speed <= 8:  
                            times_to_press = 3  
                        else:  
                            times_to_press = 4  

                        for _ in range(times_to_press):  
                            ActionChains(self.driver).key_down(Keys.SHIFT).send_keys(">").key_up(Keys.SHIFT).perform()  
                            time.sleep(0.3)  
                    else:  
                        # 减速通常是变为0.5倍、0.25倍等  
                        times_to_press = 0  
                        if self.playback_speed >= 0.5:  
                            times_to_press = 1  
                        elif self.playback_speed >= 0.25:  
                            times_to_press = 2  
                        else:  
                            times_to_press = 3  

                        for _ in range(times_to_press):  
                            ActionChains(self.driver).key_down(Keys.SHIFT).send_keys("<").key_up(Keys.SHIFT).perform()  
                            time.sleep(0.3)  

                    print("已尝试使用键盘快捷键调整速度")  
                except Exception as e:  
                    print(f"使用键盘快捷键调整速度失败: {e}")  

            # 最终检查速度  
            final_speed = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                return video ? video.playbackRate : 0;  
            """)  

            print(f"最终播放速度: {final_speed}x")  
            return True  

        except Exception as e:  
            print(f"设置播放速度时出错: {str(e)}")  

            # 回退方案:直接尝试JavaScript设置  
            try:  
                self.driver.execute_script(f"""  
                    var video = document.querySelector('video');  
                    if(video) video.playbackRate = {self.playback_speed};  
                """)  
                print("已在错误处理中尝试设置播放速度")  
            except:  
                pass  

            return False  

    def check_if_video_is_playing(self):  
        """检查视频是否正在播放"""  
        try:  
            # 尝试使用JavaScript检查视频播放状态  
            is_playing = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                return video && !video.paused && !video.ended && video.readyState > 2;  
            """)  
            return is_playing  
        except:  
            return False  

    def check_if_video_ended(self):  
        """检查视频是否已经结束"""  
        try:  
            # 方法1: 直接检查视频是否已结束  
            ended = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                if (video) {  
                    // 检查是否结束或即将结束  
                    return video.ended || (video.currentTime >= video.duration - 0.5);  
                }  
                return false;  
            """)  

            if ended:  
                print("检测到视频已播放完毕")  
                return True  

            # 方法2: 检查是否出现了"重新播放"按钮  
            try:  
                replay_button = self.driver.find_element(By.CSS_SELECTOR,  
                                                         ".bilibili-player-video-btn-start.video-state-pause")  
                if "replay" in replay_button.get_attribute("class") or "重新" in replay_button.text:  
                    print("检测到重新播放按钮")  
                    return True  
            except:  
                pass  

            # 方法3: 检查是否出现了推荐的下一个视频  
            try:  
                next_video = self.driver.find_element(By.CSS_SELECTOR, ".bilibili-player-electric-panel")  
                if next_video.is_displayed():  
                    print("检测到下一个视频推荐面板")  
                    return True  
            except:  
                pass  

            # 方法4: 检查播放进度是否长时间不变且接近视频末尾  
            current_time = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                return video ? video.currentTime : 0;  
            """)  
            duration = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                return video ? video.duration : 0;  
            """)  

            # 如果进度与上次相同且接近视频末尾(最后5秒)  
            if abs(current_time - self.last_progress) < 0.1:  
                self.progress_unchanged_count += 1  
                if self.progress_unchanged_count > 5 and (duration - current_time) < 5:  
                    print(f"检测到视频进度长时间未变且接近末尾: {current_time:.1f}/{duration:.1f}")  
                    return True  
            else:  
                self.progress_unchanged_count = 0  
                self.last_progress = current_time  

            return False  

        except Exception as e:  
            print(f"检查视频是否结束时出错: {e}")  
            return False  

    def ensure_video_is_playing(self):  
        """确保视频正在播放"""  
        # 如果视频已经结束,不再尝试播放  
        if self.video_ended or self.check_if_video_ended():  
            self.video_ended = True  
            print("视频已经播放完毕,不再尝试播放")  
            return False  

        # 先检查视频是否已经在播放  
        if self.check_if_video_is_playing():  
            return True  

        # 如果视频没有播放,尝试点击播放按钮  
        try:  
            print("尝试播放视频...")  

            # 方法1: 尝试点击播放按钮  
            try:  
                play_button = WebDriverWait(self.driver, 5).until(  
                    EC.element_to_be_clickable((By.CLASS_NAME, "bilibili-player-video-btn-start"))  
                )  
                # 检查按钮状态,如果显示的是暂停图标,说明视频已在播放  
                if "bilibili-player-video-btn-pause" in play_button.get_attribute("class"):  
                    print("视频已在播放中 (按钮状态显示)")  
                    return True  

                play_button.click()  
                print("已点击播放按钮")  
                time.sleep(2)  
            except:  
                print("找不到播放按钮,尝试其他方式")  

            # 检查是否已开始播放  
            if self.check_if_video_is_playing():  
                print("视频已开始播放")  
                return True  

            # 方法2: 直接点击视频元素  
            try:  
                video = self.driver.find_element(By.TAG_NAME, "video")  
                video.click()  
                print("已点击视频元素")  
                time.sleep(2)  
            except:  
                print("无法点击视频元素")  

            # 方法3: 使用JavaScript播放视频  
            try:  
                self.driver.execute_script("""  
                    var video = document.querySelector('video');  
                    if(video) video.play();  
                """)  
                print("已通过JavaScript尝试播放视频")  
                time.sleep(2)  
            except:  
                print("JavaScript播放失败")  

            # 最终检查  
            if self.check_if_video_is_playing():  
                print("成功播放视频")  
                return True  
            else:  
                # 再次检查视频是否已结束  
                if self.check_if_video_ended():  
                    self.video_ended = True  
                    print("视频已经播放完毕")  
                    return False  

                print("无法自动播放视频,可能需要手动介入")  
                return False  

        except Exception as e:  
            print(f"确保视频播放时出错: {e}")  
            return False  

    def disable_autoplay_next(self):  
        """禁用自动播放下一个视频的功能"""  
        try:  
            print("尝试禁用自动播放下一个视频功能...")  

            # 直接使用JavaScript禁用自动播放  
            result = self.driver.execute_script("""  
                try {  
                    // 禁用任何自动播放的事件监听器  
                    var video = document.querySelector('video');  
                    if (video) {  
                        // 阻止视频结束时的自动播放行为  
                        video.addEventListener('ended', function(e) {  
                            e.stopPropagation();  
                            e.preventDefault();  
                            return false;  
                        }, true);  

                        console.log('已设置视频ended事件阻止器');  
                    }  

                    // 尝试多种方式找到自动播放开关  
                    var autoplayDisabled = false;  

                    // 方法1: 通过设置按钮和开关  
                    try {  
                        // 点击设置按钮  
                        var settingBtn = document.querySelector('.bilibili-player-video-btn-setting');  
                        if (settingBtn) {  
                            settingBtn.click();  
                            console.log('已点击设置按钮');  

                            // 等待100ms使面板显示  
                            setTimeout(function() {  
                                // 查找自动播放开关  
                                var switches = document.querySelectorAll('.bui-switch-input, .switch-input');  
                                for(var i=0; i<switches.length; i++) {  
                                    var switchLabel = switches[i].closest('.bui-area, .setting-item');  
                                    if(switchLabel &&  
                                       (switchLabel.textContent.includes('自动播放') ||  
                                        switchLabel.textContent.includes('播放下一个'))) {  
                                        if(switches[i].checked) {  
                                            switches[i].click();  
                                            autoplayDisabled = true;  
                                            console.log('已通过UI关闭自动播放');  
                                        } else {  
                                            console.log('自动播放已经关闭');  
                                            autoplayDisabled = true;  
                                        }  
                                    }  
                                }  

                                // 关闭面板  
                                document.body.click();  
                            }, 100);  
                        }  
                    } catch(e) {  
                        console.log('通过UI关闭自动播放失败:', e);  
                    }  

                    // 方法2: 直接修改localStorage中的设置  
                    try {  
                        var playerSettings = localStorage.getItem('bilibili_player_settings');  
                        if (playerSettings) {  
                            var settings = JSON.parse(playerSettings);  
                            if (settings && settings.setting_config) {  
                                settings.setting_config.autopart = 0;  // 禁用自动播放下一个  
                                settings.setting_config.autoplay = 0;  // 禁用自动播放  
                                localStorage.setItem('bilibili_player_settings', JSON.stringify(settings));  
                                console.log('已通过修改localStorage禁用自动播放');  
                                autoplayDisabled = true;  
                            }  
                        }  
                    } catch(e) {  
                        console.log('修改localStorage设置失败:', e);  
                    }  

                    return autoplayDisabled;  
                } catch(e) {  
                    console.error('JavaScript禁用自动播放失败:', e);  
                    return false;  
                }  
            """)  

            if result:  
                print("已成功禁用自动播放功能")  
            else:  
                print("通过JavaScript禁用自动播放可能未成功,但已添加了视频结束事件阻止器")  

            return True  
        except Exception as e:  
            print(f"禁用自动播放功能时出错: {str(e)}")  
            return False  

    def clear_browser_data(self):  
        """清除浏览器数据,确保不加载之前的播放进度"""  
        try:  
            self.driver.execute_script("""  
                try {  
                    // 清除localStorage  
                    localStorage.clear();  

                    // 清除sessionStorage  
                    sessionStorage.clear();  

                    // 尝试清除B站特定的播放进度存储  
                    if (localStorage.removeItem) {  
                        // 尝试删除可能存储进度的项  
                        const keys = [  
                            'bilibili_player_settings',  
                            'video_progress',  
                            'history',  
                            'bilibili_player'  
                        ];  

                        for (const key of keys) {  
                            localStorage.removeItem(key);  
                        }  
                    }  

                    console.log('已清除浏览器存储数据');  
                } catch(e) {  
                    console.error('清除浏览器数据失败:', e);  
                }  
            """)  
            print("已尝试清除浏览器数据")  
            return True  
        except Exception as e:  
            print(f"清除浏览器数据时出错: {e}")  
            return False  

    def force_request_all_segments(self):  
        """强制请求所有弹幕段落"""  
        try:  
            print("开始尝试强制请求所有弹幕段落...")  

            # 获取视频时长  
            duration = self.driver.execute_script("""  
                var video = document.querySelector('video');  
                return video ? video.duration : 0;  
            """)  

            if duration <= 0:  
                print("无法获取视频时长,无法强制请求弹幕段落")  
                return False  

            # 计算需要请求的段数  
            # B站弹幕段每段6分钟(360秒)  
            segment_duration = 360  
            total_segments = int(duration / segment_duration) + 1  

            print(f"视频时长: {int(duration)}秒,预计有{total_segments}个弹幕段")  

            # 使用脚本拼接并发送请求  
            danmaku_api_template = None  

            # 先检查已经捕获的请求中是否有弹幕请求  
            if self.danmaku_requests:  
                for req in self.danmaku_requests:  
                    url = req['url']  
                    if 'api.bilibili.com/x/v2/dm/wbi/web/seg.so' in url:  
                        # 从URL中提取出基本部分  
                        try:  
                            base_url = re.sub(r'segment_index=\d+', 'segment_index=', url)  
                            danmaku_api_template = base_url  
                            print(f"从已有请求中提取出弹幕API模板: {danmaku_api_template}")  
                            break  
                        except:  
                            pass  

            # 如果未找到模板,尝试构造一个  
            if not danmaku_api_template:  
                try:  
                    # 尝试找到弹幕的cid  
                    cid = self.driver.execute_script("""  
                        try {  
                            // 从页面变量中提取cid  
                            if (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.epInfo) {  
                                return window.__INITIAL_STATE__.epInfo.cid;  
                            }  
                            if (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.videoData) {  
                                return window.__INITIAL_STATE__.videoData.cid;  
                            }  
                            // 从URL中提取  
                            var params = new URLSearchParams(window.location.search);  
                            if (params.has('cid')) {  
                                return params.get('cid');  
                            }  
                            // 从页面其他地方获取  
                            var scriptTags = document.querySelectorAll('script');  
                            for (var i = 0; i < scriptTags.length; i++) {  
                                var content = scriptTags[i].textContent;  
                                if (content.includes('"cid"')) {  
                                    var match = content.match(/"cid":(\d+)/);  
                                    if (match) return match[1];  
                                }  
                            }  
                            return null;  
                        } catch(e) {  
                            console.error('获取cid出错:', e);  
                            return null;  
                        }  
                    """)  

                    if cid:  
                        # 构造一个基本的弹幕API URL  
                        danmaku_api_template = f"https://api.bilibili.com/x/v2/dm/wbi/web/seg.so?type=1&oid={cid}&segment_index="  
                        print(f"已构造弹幕API模板: {danmaku_api_template}")  
                    else:  
                        print("无法获取视频cid,无法构造弹幕请求")  
                        return False  
                except Exception as e:  
                    print(f"构造弹幕API模板时出错: {e}")  
                    return False  

            # 开始请求所有段  
            for segment_index in range(1, total_segments + 1):  
                # 检查该段是否已下载  
                if str(segment_index) in self.downloaded_segments:  
                    print(f"段 {segment_index}/{total_segments} 已下载,跳过")  
                    continue  

                # 构造完整URL  
                segment_url = danmaku_api_template + str(segment_index)  

                print(f"请求弹幕段 {segment_index}/{total_segments}: {segment_url}")  

                # 使用requests直接下载  
                try:  
                    # 设置请求头  
                    headers = {  
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',  
                        'Referer': self.video_url  
                    }  

                    # 发送请求  
                    response = requests.get(segment_url, headers=headers)  

                    if response.status_code == 200 and len(response.content) > 0:  
                        # 保存弹幕数据  
                        filename = f"seg_{self.file_counter}.so"  
                        filepath = os.path.join(self.save_dir, filename)  

                        with open(filepath, 'wb') as f:  
                            f.write(response.content)  

                        # 记录URL  
                        with open(os.path.join(self.save_dir, 'url_mappings.txt'), 'a', encoding='utf-8') as f:  
                            f.write(f"{filename}: {segment_url} (强制请求)\n")  

                        print(f"已保存段 {segment_index} 弹幕: {filepath}")  

                        # 更新记录  
                        self.downloaded_segments.add(str(segment_index))  
                        self.downloaded_urls.add(segment_url)  
                        self.file_counter += 1  

                        # 记录到请求列表  
                        self.danmaku_requests.append({  
                            'url': segment_url,  
                            'timestamp': time.time(),  
                            'method': 'forced'  
                        })  
                    else:  
                        print(f"段 {segment_index} 请求失败或无内容: {response.status_code},跳过")  

                except Exception as e:  
                    print(f"请求段 {segment_index} 弹幕出错: {e}")  

                # 短暂暂停,避免请求过快  
                time.sleep(0.5)  

            print(f"强制请求完成,共请求 {total_segments} 个段,成功下载 {len(self.downloaded_segments)} 个段")  
            return True  

        except Exception as e:  
            print(f"强制请求弹幕段落时出错: {e}")  
            return False  

    def start(self):  
        """  
        启动爬虫并监控到视频结束  
        """  
        # 先尝试加载cookies  
        self.driver.get("https://www.bilibili.com")  
        time.sleep(2)  

        # 清除浏览器数据,确保不加载之前的播放进度  
        self.clear_browser_data()  

        if os.path.exists(self.cookies_path):  
            self.load_cookies()  
            self.driver.refresh()  
            time.sleep(3)  

        # 检查登录状态  
        if not self.login_check():  
            print("需要登录才能获取弹幕")  
            if not self.manual_login():  
                print("登录失败,退出程序")  
                self.driver.quit()  
                return  

        print(f"正在打开视频: {self.video_url}")  

        # 在URL中添加t参数,强制刷新缓存  
        fresh_url = f"{self.video_url}{'&' if '?' in self.video_url else '?'}t={int(time.time())}"  
        print(f"使用带时间戳的URL: {fresh_url}")  
        self.driver.get(fresh_url)  

        try:  
            # 等待视频加载  
            WebDriverWait(self.driver, 30).until(  
                EC.presence_of_element_located((By.TAG_NAME, "video"))  
            )  
            print("视频已加载")  

            # 获取视频信息(标题等)  
            self.get_video_info()  

            # 确保视频从头开始播放  
            self.ensure_video_starts_from_beginning()  

            # 禁用自动播放下一个视频  
            self.disable_autoplay_next()  

            # 确保视频开始播放  
            time.sleep(3)  # 给一点时间让视频自动播放(如果会自动播放的话)  
            self.ensure_video_is_playing()  

            # 设置视频倍速  
            self.set_playback_speed()  

            # 关闭弹窗(如果有)  
            try:  
                WebDriverWait(self.driver, 5).until(  
                    EC.element_to_be_clickable((By.CLASS_NAME, "bili-mini-close"))  
                ).click()  
            except:  
                try:  
                    # 尝试关闭其他可能的弹窗  
                    close_buttons = self.driver.find_elements(By.XPATH,  
                                                              "//*[contains(@class, 'close') or contains(@class, 'Close')]")  
                    for button in close_buttons:  
                        if button.is_displayed():  
                            button.click()  
                            time.sleep(1)  
                except:  
                    pass  

            print(f"开始监控视频播放,以 {self.playback_speed}x 倍速播放")  

            # 监控直到视频结束或达到最大时长  
            start_time = time.time()  
            check_interval = 3  # 每3秒检查一次状态  

            while True:  
                # 检查是否超过最大监控时长  
                elapsed = int(time.time() - start_time)  
                if self.max_duration and elapsed > self.max_duration:  
                    print(f"已达到最大监控时长 {self.max_duration} 秒,停止监控")  
                    break  

                # 检查视频是否已经结束  
                if self.check_if_video_ended():  
                    self.video_ended = True  
                    print("视频已播放完毕,停止监控")  
                    # 再多监控5秒确保获取到所有弹幕  
                    time.sleep(5)  
                    break  

                # 检查视频是否还在播放  
                if not self.check_if_video_is_playing() and not self.video_ended:  
                    print("检测到视频已暂停,尝试恢复播放")  
                    self.ensure_video_is_playing()  
                    # 重新检查倍速设置  
                    self.set_playback_speed()  

                # 处理性能日志并提取弹幕请求  
                self.process_performance_logs()  

                # 每30秒打印一次状态  
                if elapsed % 30 == 0 and elapsed > 0:  
                    # 获取当前视频播放时间和总时长  
                    current_time = self.driver.execute_script("""  
                        var video = document.querySelector('video');  
                        return video ? video.currentTime : 0;  
                    """)  
                    duration = self.driver.execute_script("""  
                        var video = document.querySelector('video');  
                        return video ? video.duration : 0;  
                    """)  

                    # 计算进度百分比  
                    if duration > 0:  
                        progress_pct = (current_time / duration) * 100  
                        print(  
                            f"已监控 {elapsed} 秒,视频进度 {int(current_time)}/{int(duration)} 秒 ({progress_pct:.1f}%),共捕获 {len(self.danmaku_requests)} 个弹幕请求")  
                    else:  
                        print(  
                            f"已监控 {elapsed} 秒,视频进度 {int(current_time)} 秒,共捕获 {len(self.danmaku_requests)} 个弹幕请求")  

                # 暂停一段时间再检查  
                time.sleep(check_interval)  

            # 如果启用了强制请求所有分段,在视频结束后执行  
            if self.force_segments:  
                print("\n视频监控结束,开始强制请求所有弹幕段...")  
                self.force_request_all_segments()  

        except Exception as e:  
            print(f"发生错误: {e}")  
        finally:  
            # 保存视频信息到JSON文件  
            with open(os.path.join(self.save_dir, 'video_info.json'), 'w', encoding='utf-8') as f:  
                json.dump(self.video_info, f, ensure_ascii=False, indent=2)  

            self.save_requests()  
            self.driver.quit()  

    def process_performance_logs(self):  
        """处理性能日志并提取弹幕请求"""  
        logs = self.driver.get_log('performance')  

        for log in logs:  
            try:  
                log_data = json.loads(log['message'])['message']  

                # 检查是否是网络请求  
                if 'Network.requestWillBeSent' not in log_data['method']:  
                    continue  

                request_url = log_data['params']['request']['url']  

                # 过滤弹幕请求  
                if 'api.bilibili.com/x/v2/dm/wbi/web/seg.so' in request_url:  
                    # 检查是否已存在相同的请求  
                    if request_url not in [req['url'] for req in self.danmaku_requests]:  
                        self.danmaku_requests.append({  
                            'url': request_url,  
                            'timestamp': time.time(),  
                            'method': 'natural'  
                        })  
                        print(f"发现新的弹幕请求: {request_url}")  

                        # 从URL中提取segment_index以记录  
                        segment_match = re.search(r'segment_index=(\d+)', request_url)  
                        if segment_match:  
                            segment_index = segment_match.group(1)  
                            self.downloaded_segments.add(segment_index)  

                        # 立即下载弹幕数据  
                        self.download_danmaku(request_url)  

            except Exception as e:  
                continue  

    def download_danmaku(self, url):  
        """下载弹幕数据"""  
        try:  
            # 检查是否已下载过该URL  
            if url in self.downloaded_urls:  
                print(f"该URL已下载过,跳过: {url}")  
                return  

            # 设置请求头  
            headers = {  
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',  
                'Referer': self.video_url  
            }  

            # 发送请求获取弹幕数据  
            response = requests.get(url, headers=headers)  

            if response.status_code == 200:  
                # 使用简单的递增数字命名文件  
                filename = f"seg_{self.file_counter}.so"  
                filepath = os.path.join(self.save_dir, filename)  

                with open(filepath, 'wb') as f:  
                    f.write(response.content)  
                print(f"已保存弹幕数据: {filepath}")  

                # 记录URL和文件名的映射关系,便于后续查看  
                with open(os.path.join(self.save_dir, 'url_mappings.txt'), 'a', encoding='utf-8') as f:  
                    f.write(f"{filename}: {url}\n")  

                # 标记该URL已下载并递增计数器  
                self.downloaded_urls.add(url)  
                self.file_counter += 1  

            else:  
                print(f"下载弹幕失败,状态码: {response.status_code}")  

        except Exception as e:  
            print(f"下载弹幕时出错: {e}")  

    def save_requests(self):  
        """保存所有捕获的请求到文件"""  
        with open(os.path.join(self.save_dir, 'danmaku_requests.json'), 'w', encoding='utf-8') as f:  
            json.dump(self.danmaku_requests, f, ensure_ascii=False, indent=2)  
        print(f"已保存所有请求到 {os.path.join(self.save_dir, 'danmaku_requests.json')}")  
        print(f"共下载了 {len(self.downloaded_urls)} 个弹幕文件")  
        print(f"所有数据已保存到目录: {self.save_dir}")  


# 使用示例  
if __name__ == "__main__":  
    # 替换为您要爬取的B站视频URL  
    video_url = "https://www.bilibili.com/bangumi/play/ep1566136?from_spmid=666.7.banner.4"  

    # 可以自定义保存目录,例如使用中文目录名  
    # crawler = BilibiliDanmakuCrawler(video_url, save_dir="某视频的弹幕")  

    # 设置倍速和目录(都是可选的),force_segments=True表示在视频结束后强制请求所有段  
    crawler = BilibiliDanmakuCrawler(video_url, playback_speed=6.0, force_segments=True, save_dir="射雕英雄传_侠之大者")  

    # 自动监控到视频结束  
    crawler.start()  

运行结果:



这样就获取了所有的so文件,当然这里提一嘴,为啥有一个cookies的json文件,这里大家不用管,第一次使用的时候,涉及到一个登录,大家登录后,就自动会保存这个cookies了,切记这个文件不要给其他人,否则有被盗号的风险!! 准备工作已经弄完了,接下来,我将使用获得的so文件进行解密,提取想要的信息!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值