
1. 项目概述与核心价值最近在做一个数据分析项目需要一批特定领域的视频数据B站自然成了我的首选目标。直接手动下载几十上百个视频显然不现实而市面上那些现成的下载工具要么有水印要么功能受限要么就是收费。作为一个老程序员我的第一反应就是自己动手写一个。这次我选择的是PythonSelenium的组合目标很明确不仅要能稳定爬取B站的高清视频还要解决最烦人的登录验证问题实现Cookie的自动化管理。这个方案听起来简单但实操起来从绕过B站的反爬机制到处理动态加载的视频流再到让登录状态持久化每一步都有不少门道。如果你也受困于手动收集视频数据的低效或者想学习如何用自动化工具处理复杂的Web交互那这篇从实战中总结出来的经验应该能帮你省下不少摸索的时间。2. 技术选型与整体设计思路2.1 为什么是Selenium而不是Requests很多人在做爬虫时第一个想到的可能是requests库因为它轻量、快速。但对于B站这样的现代网站直接使用requests会遇到几个难以逾越的障碍。首先B站的大量页面内容包括视频信息、评论列表是通过JavaScript动态渲染的简单的HTTP GET请求拿不到完整的HTML。其次视频播放页有复杂的反爬措施比如对XHR请求的参数进行加密校验。最后也是最关键的登录状态维持和验证码处理用纯requests模拟会异常繁琐。Selenium的优势就在这里。它本质上是一个浏览器自动化工具可以驱动一个真实的浏览器如Chrome来访问网页并执行点击、输入等操作。这意味着网站“看到”的访问者是一个真实的用户在使用浏览器从而完美绕过了JS渲染和大多数基于请求特征的反爬。我们可以用Selenium来完成登录操作包括处理滑动验证码获取登录后的Cookie然后再用这个Cookie去发起后续的请求甚至结合requests来下载文件实现“自动化登录 高效抓取”的组合拳。2.2 核心工具链搭建工欲善其事必先利其器。这个项目的基础环境搭建有几个关键点Python环境建议使用Python 3.8及以上版本兼容性和库支持都更好。使用虚拟环境如venv或conda来管理依赖是个好习惯能避免包冲突。Selenium库通过pip安装即可pip install selenium。浏览器驱动这是Selenium能控制浏览器的桥梁。我选用Chrome所以需要下载对应版本的ChromeDriver。这里有个大坑ChromeDriver的版本必须与你电脑上安装的Chrome浏览器主版本号完全一致。你可以通过Chrome的“设置-关于Chrome”查看版本然后去淘宝镜像源等网站下载匹配的驱动。将下载的chromedriver.exe放在项目目录下或者将其路径添加到系统的环境变量中。辅助库requests: 用于在获取到视频真实地址后进行高效的文件下载。json: 用于解析B站接口返回的数据。time/datetime: 用于添加延时模拟真人操作避免请求过快被封。os: 用于创建本地目录来保存下载的视频。注意在正式编写爬虫前请务必阅读并遵守B站的robots.txt协议以及相关服务条款。本技术分享仅用于个人学习与研究请勿用于大规模、商业化的数据抓取以免对目标网站造成负担并引发法律风险。3. 核心环节一Cookie的自动化获取与持久化登录是爬取个人相关数据或高清视频的第一步。手动登录再复制Cookie太麻烦我们的目标是全自动。3.1 使用Selenium模拟登录流程B站的登录页面有多种方式为了通用性和稳定性我们选择扫描二维码登录。虽然看起来复杂但Selenium可以让我们自动化完成“打开登录页-等待扫码-登录成功”的全过程。from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time import json def auto_login(): # 初始化浏览器并添加一些选项以优化体验和避免检测 options webdriver.ChromeOptions() # 避免浏览器弹出“正受到自动测试软件控制”的提示某些版本可能需要 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 可选项无头模式不显示浏览器界面适合在服务器后台运行 # options.add_argument(--headless) driver webdriver.Chrome(optionsoptions) driver.get(https://passport.bilibili.com/login) try: # 等待页面加载并找到二维码图片元素 wait WebDriverWait(driver, 30) qr_img wait.until( EC.presence_of_element_located((By.CLASS_NAME, qrcode-img)) ) print(请使用B站APP扫描屏幕上的二维码进行登录...) # 循环检测登录是否成功通过判断页面是否跳转或者某个登录后才会出现的元素是否存在 # 这里以检查页面URL是否包含‘/h5/’或跳转到首页为例更可靠的是检查用户头像等元素 while True: time.sleep(3) # 每3秒检查一次 current_url driver.current_url # 如果URL不再是登录页或者页面中出现了“我的”等登录后元素则认为登录成功 if h5/ in current_url or driver.find_elements(By.CLASS_NAME, header-avatar): print(登录成功) break except Exception as e: print(f登录过程出现异常: {e}) driver.quit() return None # 登录成功后获取所有的Cookies cookies driver.get_cookies() # 将cookies转换为requests库可用的字典格式 cookie_dict {} for cookie in cookies: cookie_dict[cookie[name]] cookie[value] # 将cookies保存到本地文件供后续使用 with open(bilibili_cookies.json, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2) print(Cookies已保存至本地文件。) driver.quit() return cookie_dict if __name__ __main__: my_cookies auto_login()实操心得等待策略使用WebDriverWait配合expected_conditions是Selenium最佳实践之一比死板的time.sleep更智能、更高效。它会在指定时间内持续检查条件是否满足一旦满足就立即执行后续代码。登录成功判定判定登录成功不能只看URL因为B站的登录跳转逻辑可能变化。更稳健的方法是等待一个登录后必定存在的页面元素出现比如用户昵称、头像等。上述代码中的判断条件可能需要根据B站页面实际结构进行调整。Cookie保存获取到的Cookie列表是一个包含字典的列表每个字典有name,value,domain,path等字段。我们将其保存为JSON格式方便下次直接读取实现“一次登录长期使用”。3.2 Cookie的加载与复用我们不可能每次运行脚本都扫码登录一次。将Cookie保存到文件后下次就可以直接加载使用。import json import requests def load_cookies_from_file(filenamebilibili_cookies.json): 从文件加载cookies并构建requests可用的session try: with open(filename, r, encodingutf-8) as f: cookies_list json.load(f) # 创建一个requests Session对象用于保持会话 session requests.Session() for cookie in cookies_list: # 为session的cookies属性设置每一个cookie session.cookies.set(cookie[name], cookie[value]) print(Cookies加载成功。) return session except FileNotFoundError: print(Cookie文件未找到请先运行登录程序。) return None # 使用示例 session load_cookies_from_file() if session: # 使用这个带cookie的session去访问需要登录的页面如个人中心 response session.get(https://api.bilibili.com/x/web-interface/nav) user_info response.json() if user_info[code] 0: print(f当前登录用户{user_info[data][uname]}) else: print(Cookie可能已失效需要重新登录。)注意事项Cookie有效期B站的登录Cookie是有有效期的可能几天或几周。当脚本发现用旧Cookie访问API返回错误码如-101表示未登录时就需要重新触发auto_login函数来更新Cookie文件。Session对象使用requests.Session()可以自动管理Cookie在同一个Session内发起的请求都会携带之前获得的Cookie非常方便。4. 核心环节二高清视频地址的解析与爬取策略登录问题解决后就进入了核心环节如何找到并下载高清视频。B站的视频播放采用了分段传输M4S和动态密钥直接找.mp4或.flv链接的时代早已过去。4.1 解析视频播放页获取aid、bvid和cidB站的每个视频都有几个关键IDbvid现在视频链接中的主要标识符如BV1xx411c7mh。aid传统的数字ID与bvid有一一对应关系。cid分P视频中每一P的唯一标识符是获取视频流地址的关键。我们的第一步是从视频播放页的源代码中提取这些信息。这些信息通常藏在一个名为__INITIAL_STATE__的JavaScript变量中。import re import json def get_video_ids_by_selenium(url, driver): 使用Selenium打开页面并解析出视频ID driver.get(url) time.sleep(2) # 等待页面JS加载完成 # 获取页面源码 page_source driver.page_source # 使用正则表达式查找 __INITIAL_STATE__ 变量 pattern rwindow\.__INITIAL_STATE__\s*\s*({.*?}); match re.search(pattern, page_source, re.DOTALL) if match: init_state_str match.group(1) try: init_state json.loads(init_state_str) # 解析数据结构获取bvid, aid, cid bvid init_state.get(bvid) aid init_state.get(aid) # 对于多P视频需要获取当前播放的cid通常位于videoData或videoInfo中 # 这里是一个常见路径实际结构可能需要微调 cid init_state.get(videoData, {}).get(cid) if not cid: cid init_state.get(videoInfo, {}).get(cid) print(f解析成功: bvid{bvid}, aid{aid}, cid{cid}) return {bvid: bvid, aid: aid, cid: cid} except json.JSONDecodeError as e: print(f解析JSON失败: {e}) else: print(未在页面中找到 __INITIAL_STATE__ 信息。) return None4.2 调用B站API获取视频流信息拿到aid和cid后我们就可以模拟播放器调用B站内部获取视频流信息的API。这个API返回的数据包含了不同清晰度如1080P、720P对应的音视频分段地址、密钥等信息。def get_video_play_info(aid, cid, session): 通过API获取视频播放信息含视频流地址 api_url https://api.bilibili.com/x/player/playurl params { avid: aid, cid: cid, qn: 80, # 清晰度标识80代表1080P64代表720P32代表480P fnval: 80, # 视频流格式标识80代表获取dash格式音视频分离 fnver: 0, fourk: 1, # 是否允许4K } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.bilibili.com, # 必须携带Referer否则可能403 } response session.get(api_url, paramsparams, headersheaders) if response.status_code 200: play_info response.json() if play_info[code] 0: return play_info[data] else: print(fAPI返回错误: {play_info[message]}) else: print(f请求API失败状态码: {response.status_code}) return None关键参数解析qn(Quality Number): 决定视频清晰度。常用值120(4K),116(1080P60),112(1080P),80(1080P),64(720P),32(480P)。数值越大通常清晰度越高但并非所有视频都有所有档位。fnval: 决定返回的视频流格式。1是老的FLV格式16是DASH格式MPEG-480或4048是DASH格式并包含杜比视界等信息。我们选择80获取通用的DASH格式它通常能提供最好的清晰度和音质并且音视频是分开的。Referer: 这个请求头非常重要B站服务器会校验它必须设置为B站的主站域名否则会返回403 Forbidden错误。4.3 解析DASH流并下载音视频从API获取的play_info中dash字段包含了视频流(video)和音频流(audio)的数组。每个流都有baseUrl或backupUrl和size等信息。def download_dash_video(play_info, title, session): 下载DASH格式的音视频并合并简易示例 if not play_info or dash not in play_info: print(未获取到DASH流信息。) return dash play_info[dash] # 通常选择第一个视频流和第一个音频流可根据codecid、bandwidth等选择最高质量 video_url dash[video][0][baseUrl] audio_url dash[audio][0][baseUrl] video_size dash[video][0][size] audio_size dash[audio][0][size] print(f视频流大小: {video_size / (1024**2):.2f} MB) print(f音频流大小: {audio_size / (1024**2):.2f} MB) headers { User-Agent: Mozilla/5.0 ..., Referer: https://www.bilibili.com, # 对于视频流Range请求头很重要支持断点续传 Range: bytes0- } # 下载视频部分 print(f正在下载视频流...) video_resp session.get(video_url, headersheaders, streamTrue) video_filename f{title}_video.m4s with open(video_filename, wb) as f: for chunk in video_resp.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f视频流下载完成: {video_filename}) # 下载音频部分 print(f正在下载音频流...) audio_resp session.get(audio_url, headersheaders, streamTrue) audio_filename f{title}_audio.m4s with open(audio_filename, wb) as f: for chunk in audio_resp.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f音频流下载完成: {audio_filename}) # 合并音视频此处为示意实际合并推荐使用ffmpeg print(f请使用FFmpeg合并音视频文件命令示例) print(fffmpeg -i {video_filename} -i {audio_filename} -c:v copy -c:a copy {title}.mp4)重要提示流式下载使用streamTrue和iter_content来下载大文件可以避免内存占用过高。合并工具下载下来的.m4s文件是音视频分离的容器格式需要使用专业工具合并。FFmpeg是命令行下最强大、最通用的选择。上述代码最后打印了合并命令。你需要先在系统上安装FFmpeg并确保其在环境变量中。清晰度选择代码中固定了qn80。在实际应用中你可以先获取所有可用的清晰度列表API返回的accept_quality和accept_description然后让用户选择或根据规则自动选择最高可用清晰度。5. 实战流程整合与优化将上述模块组合起来就形成了一个完整的爬虫工作流。我们还需要考虑一些工程化优化。5.1 完整的单视频爬取函数def download_bilibili_video(video_url, save_dirdownloads, quality80): 主函数给定B站视频链接下载指定清晰度的视频。 # 0. 准备工作 if not os.path.exists(save_dir): os.makedirs(save_dir) # 1. 初始化Selenium WebDriver (用于解析页面如果需要) options webdriver.ChromeOptions() options.add_argument(--headless) # 无头模式不显示浏览器窗口 driver webdriver.Chrome(optionsoptions) # 2. 获取视频ID print(f正在解析页面: {video_url}) ids get_video_ids_by_selenium(video_url, driver) if not ids: driver.quit() return driver.quit() # 获取到ID后就可以关闭浏览器了 # 3. 加载Cookie会话 session load_cookies_from_file() if not session: print(无法加载Cookie尝试重新登录...) # 这里可以添加自动调用登录函数的逻辑 return # 4. 获取视频播放信息 print(f正在获取视频流信息 (清晰度: {quality})...) play_info get_video_play_info(ids[aid], ids[cid], session, quality) if not play_info: print(获取播放信息失败。) return # 5. 提取视频标题并清理非法文件名字符 title play_info.get(title, ids[bvid]) # 移除文件名中不允许的字符 import re title re.sub(r[:/\\|?*], _, title) # 6. 下载DASH流 print(f开始下载: {title}) download_dash_video(play_info, os.path.join(save_dir, title), session) print(f下载流程结束请检查 {save_dir} 目录下的文件并使用FFmpeg合并。)5.2 处理多P视频与合集B站很多视频是分P的或者属于一个合集。我们的爬虫需要能够处理这种情况。def get_video_cid_list(aid, session): 通过视频aid获取其所有分P的cid列表 api_url https://api.bilibili.com/x/player/pagelist params {aid: aid} response session.get(api_url, paramsparams) if response.status_code 200: data response.json() if data[code] 0: # 返回一个列表每个元素包含cid和page(分P序号)等信息 return data[data] return [] # 在主函数中可以先获取cid列表然后循环下载每一个cid对应的视频 cid_list get_video_cid_list(target_aid, session) for index, part in enumerate(cid_list): print(f开始下载第 {part[page]} P: {part[part]}) play_info get_video_play_info(target_aid, part[cid], session) # ... 后续下载逻辑文件名可以加上 part[page] 以示区分5.3 添加延时与异常处理为了避免请求过快被B站的风控系统识别为爬虫必须在请求之间添加随机延时。同时健壮的程序必须有完善的异常处理。import random import time def safe_request_with_delay(session, url, **kwargs): 包装请求函数添加随机延时和重试机制 delay random.uniform(1, 3) # 随机延时1-3秒 time.sleep(delay) max_retries 3 for attempt in range(max_retries): try: response session.get(url, timeout10, **kwargs) # 设置超时 response.raise_for_status() # 如果状态码不是200抛出HTTPError return response except requests.exceptions.RequestException as e: print(f请求失败 (尝试 {attempt1}/{max_retries}): {e}) if attempt max_retries - 1: retry_delay random.uniform(5, 10) print(f等待 {retry_delay:.1f} 秒后重试...) time.sleep(retry_delay) else: print(已达到最大重试次数放弃请求。) raise return None6. 常见问题排查与进阶技巧在实际操作中你肯定会遇到各种各样的问题。这里记录了几个我踩过的坑和解决方案。6.1 常见错误码与应对错误现象可能原因排查与解决思路API返回-101未登录或Cookie失效。检查Cookie文件是否存在、是否过期。重新运行登录函数获取新Cookie。API返回-400请求参数错误。检查aid、cid、qn、fnval等参数是否正确尤其是cid是否对应了正确的分P。API返回-404视频不存在或无权限。检查视频链接是否正确视频是否已被UP主删除或设置为私密。下载链接返回403请求头不完整特别是缺少Referer或User-Agent。确保在下载请求中携带了正确的RefererB站域名和模拟真实浏览器的User-Agent。下载速度极慢或中断网络问题或目标服务器限流。使用streamTrue分块下载。添加重试机制。考虑在请求头中添加Accept-Encoding但注意requests会自动解压。Selenium找不到元素页面加载未完成或元素定位方式改变。增加等待时间使用更稳健的WebDriverWait。检查元素的CSS选择器或XPath是否因网站改版而失效。6.2 关于清晰度qn的深入理解不是所有视频都支持所有清晰度。更稳妥的做法是先查询可用清晰度。在get_video_play_info的API返回数据中有一个accept_quality列表里面包含了该视频支持的所有qn值。你应该编写逻辑优先选择你期望的清晰度如80如果不在支持列表中则降级选择列表中可用的最高清晰度。6.3 使用FFmpeg合并的细节打印出FFmpeg命令让用户自己操作只是权宜之计。更好的做法是在Python脚本中直接调用FFmpeg进行合并。import subprocess def merge_audio_video(video_path, audio_path, output_path): 使用FFmpeg合并音视频 command [ ffmpeg, -i, video_path, -i, audio_path, -c:v, copy, # 视频流直接复制不重新编码速度快无损 -c:a, copy, # 音频流直接复制 -y, # 覆盖已存在的输出文件 output_path ] try: print(f正在合并文件: {output_path}) result subprocess.run(command, checkTrue, capture_outputTrue, textTrue) print(合并成功) # 可选合并成功后删除临时的.m4s文件 # os.remove(video_path) # os.remove(audio_path) except subprocess.CalledProcessError as e: print(fFFmpeg合并失败: {e}) print(f标准错误输出: {e.stderr}) except FileNotFoundError: print(未找到FFmpeg请确保已安装并将其添加到系统环境变量PATH中。)6.4 应对反爬升级User-Agent池与IP代理如果进行大规模爬取即使有Cookie和延时单一IP和User-Agent也可能被限制。这时需要考虑User-Agent池准备一个列表存放多种浏览器和设备的User-Agent字符串每次请求随机选取一个。IP代理使用代理IP池来轮换请求的出口IP。这涉及到付费代理服务或自建代理复杂度较高仅在必要时考虑。对于个人学习和少量数据抓取通常不需要走到这一步。整个项目从环境搭建到最终下载合并涉及了Web自动化、网络请求、数据解析、文件处理等多个环节。最关键的体会是理解网站的数据加载逻辑和接口参数含义远比盲目写代码更重要。多利用浏览器的开发者工具F12中的“网络(Network)”面板观察真实播放视频时浏览器发起了哪些请求这些请求携带了哪些参数和请求头这是逆向解析任何网站数据接口的通用法门。最后再次强调技术用于学习请合理控制爬取频率尊重网站规则和创作者版权。