Python爬虫实战:B站视频批量下载工具开发全流程解析
2026/9/2 13:54:19 网站建设 项目流程

简介:这是一份面向Python初学者与网络爬虫实践者的入门级练习资源,聚焦B站小视频的批量采集场景,涵盖动态请求构造、User-Agent随机化、视频文件大小预估及下载进度实时反馈等核心技能点,适用于课程实验、自学项目或技术备赛中的HTTP协议实战训练。压缩包共5个Python源文件,总大小仅12KB,结构清晰:主程序bilibili.py统筹调度,其余task_*.py分别封装获取动态接口数据、生成随机浏览器头部、计算目标视频体积、实时打印下载进度等模块化功能,便于分步调试与原理理解。已有272人学习下载,资源代码简洁规范,注释充分,无第三方依赖硬编码,可直接运行并快速迁移至其他类似视频平台爬取逻辑中,是掌握requests+time+os基础组合应用的优质实操范例。

1. 项目概述与核心价值

最近在整理硬盘,翻出来一个几年前写的Python脚本,名字就叫“网络爬虫-批量爬取B站视频-python练习源码.zip”。当时写这个纯粹是为了练手,想看看自己能不能搞定一个相对复杂的爬虫任务。没想到,这个项目后来成了我理解网络请求、数据解析、并发处理和反爬策略的绝佳案例。今天,我就把这个“练习源码”背后的完整思路、踩过的坑以及一些实用的技巧,掰开揉碎了跟大家聊聊。无论你是刚学完Python基础想找个项目练手,还是对爬虫技术感兴趣,想了解如何从零构建一个能稳定运行的批量下载工具,这篇文章应该都能给你一些直接的参考。

简单来说,这个项目就是用Python写一个脚本,能够自动、批量地获取B站上指定视频的下载链接,并保存到本地。它解决的痛点很直接:当你看到一个UP主的系列教程、一个精彩的合集,或者只是想备份自己收藏夹里的视频时,手动一个个点开下载效率太低。这个脚本就是为了把我们从重复劳动中解放出来。当然,我必须强调,任何爬虫行为都必须在法律和网站服务条款允许的范围内进行,尊重版权和网站服务器的负载,本分享仅用于技术学习和交流目的。

2. 项目整体设计与思路拆解

2.1 核心需求与技术选型

这个项目的核心目标很明确:输入一个或多个B站视频的链接(比如一个合集页面、一个UP主的主页、或一个视频列表),程序能自动解析出所有视频的真实播放地址,并下载到本地指定文件夹。

要实现这个目标,我们需要拆解出几个关键技术环节:

  1. 网页内容获取:如何模拟浏览器,向B站服务器发起请求并拿到返回的HTML或JSON数据。
  2. 数据解析与提取:如何从复杂的网页源码或接口返回的数据中,精准地找到我们需要的视频标题、BV号/AV号以及最重要的——视频流信息。
  3. 视频流地址解析:B站的视频是分P、分清晰度、并且地址是动态生成的,如何获取到可下载的m4sflv/mp4直链。
  4. 文件下载与存储:如何高效、稳定地下载可能很大的视频文件,并合理命名、组织文件夹。
  5. 反爬虫策略应对:如何应对常见的反爬手段,如请求头校验、频率限制等,确保脚本能稳定运行一段时间。

基于这些环节,我的技术选型如下:

  • 请求库:requests。这是Python中最简单易用的HTTP库,足以应对B站大部分接口和页面请求。对于更复杂的动态页面(早期可能需要),会考虑selenium,但为了效率和轻量,本项目优先尝试分析其API接口。
  • 解析库:json+re(正则表达式) +BeautifulSoup。B站很多数据是通过API返回的JSON格式,直接用json解析最方便。对于部分嵌入在HTML中的数据,用BeautifulSoup进行HTML解析。一些特定的参数或链接,用re进行正则匹配往往更快。
  • 下载工具:requests流式下载。对于大文件,使用requests.get(stream=True)进行分块下载,避免内存溢出,同时可以显示下载进度。
  • 并发处理(可选):concurrent.futuresasyncio+aiohttp。当需要批量下载几十上百个视频时,串行下载太慢。使用线程池或异步IO可以极大提升效率。本练习源码中,我使用了concurrent.futures.ThreadPoolExecutor,因为它对I/O密集型任务(如下载)效果显著且代码简单。

注意:技术选型的核心原则是“够用就好,易于维护”。requests+BeautifulSoup的组合在应对B站这类混合了API和静态页面的网站时非常灵活。盲目上scrapy等重型框架对于这个特定任务来说,可能增加了不必要的复杂度。

2.2 环境准备与依赖安装

工欲善其事,必先利其器。在开始编码前,我们需要搭建好Python环境并安装必要的库。这里假设你已经安装了Python(3.6及以上版本)。

  1. 创建虚拟环境(推荐):这是一个好习惯,可以隔离项目依赖。

    # 在项目目录下 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate
  2. 安装核心依赖库:使用pip进行安装。

    pip install requests beautifulsoup4
    • requests: 用于发送HTTP请求。
    • beautifulsoup4: 用于解析HTML和XML文档。

    如果你的Python版本较新,jsonre是标准库,无需安装。对于并发下载,concurrent.futures也是Python 3.2+的标准库。

  3. 可选工具

    • IDE/编辑器:VSCode、PyCharm等,它们对代码提示、调试支持很好。在VSCode中配置Python环境也很简单,安装Python扩展即可。
    • 浏览器开发者工具:这是爬虫工程师的“眼睛”。F12打开,在Network(网络)选项卡中查看页面加载过程中的所有请求(XHR/Fetch类型尤其重要),是找到真实数据接口的关键。

3. 核心细节解析与实操要点

3.1 逆向分析:找到数据源头

这是爬虫项目中最关键、也最体现技术含量的部分。我们不能直接去下载网页上播放器里的mp4文件,因为那个地址通常是临时的、有鉴权的。我们需要找到B站提供视频信息的原始API。

操作过程实录:

  1. 打开一个B站视频页面,例如https://www.bilibili.com/video/BV1xx411c7mD
  2. 按F12打开开发者工具,切换到Network(网络)选项卡。
  3. 刷新页面,在纷繁复杂的请求列表中,过滤XHRFetch类型的请求。
  4. 仔细观察,你会发现一个包含web?aid=x/web-interface/view等字样的请求。点击它,在Preview(预览)或Response(响应)标签页里,就能看到结构清晰的JSON数据,里面包含了视频的标题、分P信息、cid(每一P的唯一标识)等。
  5. 继续寻找,有一个包含playurlx/player/wbi/playurl的请求,这个请求的响应里,就藏着不同清晰度(如1080p、720p)对应的视频和音频的m4s文件地址(或直接的flv/mp4链接)。这就是我们需要的真实下载地址

核心要点:

  • bvidcid:B站视频有两个重要ID。bvid(如BV1xx411c7mD)是视频的唯一标识,用于前端展示。cid是每一P视频的内部标识,用于后端接口获取流信息。通常需要先用bvid通过第一个API拿到该视频所有分P的cid列表。
  • 接口参数:这些API请求通常带有大量参数,如bvidcidqn(清晰度标识)、fnval(流格式标识)、fourk(是否4K)等。我们需要在代码中模拟这些参数。
  • 请求头(Headers):必须模拟浏览器的请求头,至少包含User-AgentReferer(通常设置为视频页面URL)。有时还需要Cookie(特别是登录后才能看的视频),但出于合规考虑,练习项目应避免处理需要个人登录Cookie的内容。

3.2 构建请求与处理反爬

拿到API地址和参数后,我们需要用Python的requests库来模拟这个请求。

import requests import json 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': 'https://www.bilibili.com', # 通常Referer是必须的 } def get_video_info(bvid): """根据bvid获取视频基本信息(标题,分P列表)""" info_url = f'https://api.bilibili.com/x/web-interface/view?bvid={bvid}' try: resp = requests.get(info_url, headers=headers, timeout=10) resp.raise_for_status() # 检查请求是否成功 data = resp.json() if data['code'] == 0: return data['data'] else: print(f"获取视频信息失败: {data['message']}") return None except requests.exceptions.RequestException as e: print(f"请求视频信息时发生错误: {e}") return None # 示例:获取BV1xx411c7mD的信息 video_data = get_video_info('BV1xx411c7mD') if video_data: title = video_data['title'] pages = video_data['pages'] # 分P信息列表 for page in pages: print(f"分P{page['page']}: {page['part']}, cid: {page['cid']}")

避坑技巧:

  • 超时设置:务必为requests.get设置timeout参数(如10秒),防止因网络问题导致程序长时间卡死。
  • 异常处理:使用try...except包裹网络请求,并检查返回的JSON中code字段(B站API通常0表示成功)。
  • 频率控制:在循环请求多个视频时,务必在请求间添加随机延时(如time.sleep(random.uniform(1, 3))),这是对目标网站最基本的尊重,也能有效避免因请求过快被暂时封禁IP。
  • User-Agent轮换:可以准备一个User-Agent列表,每次请求随机选择一个,增加一些随机性。

3.3 解析并获取下载链接

拿到cid后,下一步就是获取指定清晰度的播放地址。

def get_play_url(bvid, cid, quality=80): """ 根据bvid和cid获取视频播放地址 quality: 清晰度标识,80表示1080P,64表示720P,32表示480P """ play_url_api = 'https://api.bilibili.com/x/player/playurl' params = { 'bvid': bvid, 'cid': cid, 'qn': quality, # 清晰度 'fnval': 80, # 指定获取dash格式流(包含分开的视频和音频) 'fourk': 1, # 允许请求4K视频 } try: resp = requests.get(play_url_api, params=params, headers=headers, timeout=10) data = resp.json() if data['code'] == 0: dash = data['data']['dash'] # dash格式下,video和audio是分开的 video_url = dash['video'][0]['baseUrl'] # 通常取第一个,即最高画质 audio_url = dash['audio'][0]['baseUrl'] return video_url, audio_url else: print(f"获取播放地址失败: {data['message']}") return None, None except Exception as e: print(f"解析播放地址出错: {e}") return None, None

关键解析:

  • fnval参数:这是一个位掩码参数。fnval=80(十进制)或0x80(十六进制)表示请求DASH格式流。DASH格式将视频和音频分离,通常能获得更好的画质和更小的文件体积(因为可以只下载你需要的清晰度)。fnval=1则请求FLV格式(已逐渐淘汰)。fnval=16请求MP4格式。本项目选择80获取DASH流。
  • 返回的video_urlaudio_url就是.m4s文件的直链。我们需要分别下载它们,然后用工具(如ffmpeg)合并,或者寻找直接返回mp4格式的接口(可能清晰度受限)。

实操心得:直接请求fnval=16有时可以拿到直接的mp4链接,更方便,但清晰度选项可能不全。DASH流是未来的趋势,虽然多了一步合并操作,但更灵活、画质更好。在代码中,我们可以提供一个选项让用户选择。

4. 实操过程与核心环节实现

4.1 实现单视频下载函数

有了视频和音频的直链,我们就可以实现下载功能了。这里实现一个支持进度显示的流式下载函数。

def download_file(url, filepath, session=None): """ 下载文件并显示进度 session: 可传入一个requests.Session()对象以保持连接 """ if session is None: session = requests.Session() headers.update({'Referer': 'https://www.bilibili.com'}) # 下载时Referer也很重要 try: resp = session.get(url, headers=headers, stream=True, timeout=30) resp.raise_for_status() total_size = int(resp.headers.get('content-length', 0)) downloaded = 0 with open(filepath, 'wb') as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) if total_size > 0: percent = downloaded / total_size * 100 print(f"\r下载中: {filepath} - {percent:.2f}% ({downloaded}/{total_size} bytes)", end='') print(f"\n下载完成: {filepath}") return True except Exception as e: print(f"\n下载失败 {url}: {e}") return False

4.2 视频与音频的合并

下载完DASH流的.m4s文件后,我们需要将它们合并成一个完整的.mp4文件。最常用的工具是ffmpeg

方法一:使用subprocess调用系统ffmpeg命令(推荐)确保你的系统已安装ffmpeg并添加到环境变量PATH中。

import subprocess import os def merge_audio_video(video_path, audio_path, output_path): """使用ffmpeg合并音视频""" # 清理可能存在的旧文件 if os.path.exists(output_path): os.remove(output_path) cmd = [ 'ffmpeg', '-i', video_path, '-i', audio_path, '-c:v', 'copy', # 视频流直接复制,不重新编码,速度极快 '-c:a', 'aac', # 音频流编码为aac(或copy,但有时需要统一格式) '-strict', 'experimental', output_path ] try: subprocess.run(cmd, check=True, capture_output=True, text=True) print(f"合并成功: {output_path}") # 可选:删除临时的.m4s文件 os.remove(video_path) os.remove(audio_path) return True except subprocess.CalledProcessError as e: print(f"合并失败: {e.stderr}") return False

方法二:使用moviepy库(纯Python,但较重)如果不想依赖外部工具,可以安装moviepy库(pip install moviepy),但它处理大文件较慢,且功能可能不如ffmpeg全面。

4.3 实现批量爬取逻辑

现在,我们将上述所有环节串联起来,实现批量处理。核心逻辑是:输入一个包含多个B站视频ID(或一个合集URL)的列表,程序遍历列表,对每个视频执行“获取信息 -> 获取链接 -> 下载 -> 合并”的流程。

import time import random from concurrent.futures import ThreadPoolExecutor, as_completed def process_single_video(bvid, quality=80, save_dir='./downloads'): """处理单个视频的完整流程""" # 1. 创建保存目录 os.makedirs(save_dir, exist_ok=True) # 2. 获取视频基本信息 print(f"\n开始处理视频: {bvid}") video_info = get_video_info(bvid) if not video_info: return False title = video_info['title'].replace('/', '_').replace('\\', '_')[:100] # 清理非法文件名 pages = video_info['pages'] session = requests.Session() # 为这个视频创建一个会话 for page in pages: page_num = page['page'] part_title = page['part'] cid = page['cid'] # 生成文件名 if len(pages) > 1: file_base_name = f"{title}_P{page_num}_{part_title}" else: file_base_name = title print(f" 正在处理分P{page_num}: {part_title}") # 3. 获取播放地址 video_url, audio_url = get_play_url(bvid, cid, quality) if not video_url or not audio_url: print(f" 获取分P{page_num}播放地址失败,跳过。") continue # 4. 下载视频和音频 video_temp_path = os.path.join(save_dir, f"{file_base_name}_video.m4s") audio_temp_path = os.path.join(save_dir, f"{file_base_name}_audio.m4s") output_path = os.path.join(save_dir, f"{file_base_name}.mp4") if os.path.exists(output_path): print(f" 文件已存在,跳过: {output_path}") continue print(f" 开始下载视频流...") if not download_file(video_url, video_temp_path, session): continue time.sleep(random.uniform(0.5, 1.5)) # 请求间延时 print(f" 开始下载音频流...") if not download_file(audio_url, audio_temp_path, session): continue # 5. 合并 print(f" 合并音视频...") if merge_audio_video(video_temp_path, audio_temp_path, output_path): print(f" ✓ 分P{page_num}处理完成: {output_path}") else: print(f" ✗ 分P{page_num}合并失败。") return True def batch_download(bvid_list, quality=80, save_dir='./downloads', max_workers=3): """批量下载视频,支持并发""" with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_bvid = {executor.submit(process_single_video, bvid, quality, save_dir): bvid for bvid in bvid_list} for future in as_completed(future_to_bvid): bvid = future_to_bvid[future] try: success = future.result() if success: print(f"\n视频 {bvid} 所有分P处理完毕。") except Exception as e: print(f"\n处理视频 {bvid} 时发生未预期错误: {e}") # 每个视频任务完成后,添加一个较长延时,避免对服务器造成集中压力 time.sleep(random.uniform(3, 7)) if __name__ == '__main__': # 示例:下载两个视频 my_bvid_list = ['BV1xx411c7mD', 'BV1Bf4y1U7QP'] batch_download(my_bvid_list, quality=80, save_dir='./bilibili_videos', max_workers=2)

代码逻辑解读:

  1. process_single_video函数封装了处理一个视频(可能包含多P)的完整流水线。
  2. batch_download函数利用ThreadPoolExecutor实现并发下载。max_workers控制并发数,建议不要设置过高(如3-5),以免对目标服务器造成过大压力或触发反爬。
  3. 在主程序中,将要下载的视频bvid放入列表,调用batch_download即可。

5. 常见问题与排查技巧实录

在实际运行过程中,你肯定会遇到各种各样的问题。下面是我在开发和运行这个脚本时遇到的一些典型问题及解决方法。

5.1 问题:获取视频信息或播放地址时返回-403-404错误码

  • 可能原因1:请求头不完整或错误。

    • 排查:用浏览器开发者工具,对比你的Python脚本发送的请求头与浏览器发送的请求头。重点检查User-AgentReferer,有时还需要Origin
    • 解决:确保headers字典尽可能模拟浏览器。Referer通常必须设置为https://www.bilibili.com或具体的视频页面URL。
  • 可能原因2:接口参数已更新或需要额外的鉴权参数。

    • 排查:B站的API接口可能会更新。重新用开发者工具抓包,查看最新的playurl接口请求参数。特别注意是否有w_ridwts这类动态签名参数(这是B站一种常见的反爬机制)。
    • 解决:如果发现需要w_ridwts,说明接口已升级为WBI签名鉴权。你需要找到生成这两个参数的JavaScript代码,并用Python实现其算法,或者寻找其他无需签名的替代接口(有时旧版接口仍可用)。这是一个进阶挑战,需要一定的JS逆向能力。
  • 可能原因3:视频需要大会员或地区限制。

    • 排查:在浏览器中确认该视频是否需要登录或大会员才能观看你想要的清晰度。
    • 解决:对于需要登录的视频,必须在请求头中携带有效的Cookie但请注意,获取和使用他人Cookie涉及隐私和安全问题,且违反B站用户协议。本练习项目强烈建议仅处理公开、无限制的视频。可以尝试获取低清晰度(如360p)的流,这些通常无需鉴权。

5.2 问题:下载下来的.m4s文件用ffmpeg合并失败

  • 可能原因1:视频或音频文件本身下载不完整或损坏。

    • 排查:检查下载的.m4s文件大小是否异常小(比如只有几KB)。用文本编辑器打开看看,如果里面是JSON格式的错误信息(如{"code":-404, "message":"Not Found"}),说明下载链接本身就有问题。
    • 解决:回到上一步,确保获取到的video_urlaudio_url是有效的。可能是清晰度参数qn不支持,尝试换一个清晰度(如64代表720p)。
  • 可能原因2:ffmpeg命令参数问题或版本不兼容。

    • 排查:手动在命令行执行合并命令,看具体的错误信息。有时-c:a copy(音频流复制)会导致问题,因为源音频格式可能比较特殊。
    • 解决:将合并命令中的-c:a copy改为-c:a aac,强制转码为AAC格式。确保你安装的ffmpeg版本不是太旧。

5.3 问题:并发下载时,部分任务失败或程序卡死

  • 可能原因1:并发数过高,触发服务器的频率限制或连接被重置。
    • 解决:降低max_workers的值,比如从5降到2或3。在每个视频任务之间以及每个下载请求之间,增加随机的、更长的延时(time.sleep)。
  • 可能原因2:网络不稳定或单个任务超时。
    • 解决:在download_file函数中增加重试机制。例如,用循环包裹下载逻辑,失败后重试2-3次。同时,确保timeout参数设置合理(如30-60秒)。

5.4 问题:如何爬取一个UP主的所有视频或一个合集?

  • 思路:我们的核心函数process_single_video需要的是bvid。所以,首先要解决的是如何从一个UP主空间或合集页面,提取出所有视频的bvid列表。
  • 方法
    1. 分析页面结构:打开一个UP主主页(如https://space.bilibili.com/123456/video),F12查看其加载视频列表的API。通常是一个返回JSON的接口,里面包含了视频列表和对应的bvid
    2. 编写列表提取函数:写一个新的函数get_video_list_by_mid(mid),其中mid是UP主的ID。这个函数模仿浏览器去请求那个API,解析JSON,返回一个bvid的列表。
    3. 集成到主流程:先调用get_video_list_by_mid获取列表,再将这个列表传递给batch_download函数。
  • 注意:合集和频道的页面逻辑类似,都需要先找到其对应的数据接口。这又是一个逆向分析的过程,但原理和获取单个视频信息是相通的。

5.5 性能与优化建议

  1. 会话保持:在process_single_video函数中,我为每个视频创建了一个requests.Session()。Session可以复用TCP连接,在下载同一个视频的多P时,能稍微提升效率。
  2. 断点续传:当前的下载函数不支持断点续传。对于超大文件,可以考虑实现它。思路是检查本地已下载文件大小,在请求头中加入Range: bytes=start-end来请求剩余部分。但这需要服务器支持。
  3. 更优雅的进度条:可以使用第三方库tqdm来替代自己打印的进度信息,它会显示一个美观的进度条和预估剩余时间。
  4. 配置化:将清晰度、保存路径、并发数、请求头等参数提取到配置文件(如config.ini)或命令行参数中,使脚本更灵活。

最后,我想说的是,爬虫技术是一把双刃剑。这个“B站视频批量爬取”项目作为一个Python练习,极大地锻炼了我的网络编程、数据分析和问题解决能力。但在实际使用中,请务必牢记:

  • 遵守robots.txt:查看目标网站的robots.txt文件,尊重网站管理员的意愿。
  • 控制请求频率:这是最重要的道德和技术准则。过快的请求等同于攻击。
  • 尊重版权:下载的内容仅限个人学习、研究使用,切勿用于商业传播或任何侵犯创作者权益的行为。
  • 关注接口变更:网站前端和后端随时在变,今天能用的代码明天可能就失效了。保持学习,理解原理(HTTP请求、数据解析)比记住某个具体的API地址更重要。

这个项目的源码虽然叫“练习源码”,但其中涉及的思路和技巧是通用的。希望这份超详细的拆解,能帮你不仅完成这个练习,更能理解爬虫项目从设计到实现的完整脉络。如果在复现过程中遇到新问题,多利用开发者工具观察、多思考、多搜索,解决问题的过程本身就是最好的学习。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询