简介:面向Python爬虫初学者的实战入门示例,演示如何批量下载哔哩哔哩小视频,并在控制台实时显示每个文件的下载进度。核心逻辑围绕分页请求展开:脚本循环遍历10页排行榜JSON数据,从中提取视频标题与直链地址,再调用requests模块以流式方式写入本地video目录;标题中的非法字符会被自动替换为空,避免文件保存失败。同时,每次下载完成后随机等待3-6秒再发起下一次请求,体现基础的反爬虫访问节奏控制。压缩包内仅包含1个py源文件,大小约2KB,代码量虽然不大,但完整覆盖了网络请求、JSON解析、文件写入、字符串清洗与时间调度等常见知识点,适合入门者逐行阅读并改造。目前已有495人学习查看,对希望快速掌握网页数据抓取与文件下载流程的爬虫学习者来说,是一份可直接运行、便于扩展的迷你参考脚本。 干过视频下载的朋友都知道,B站网页端看视频是一码事,想把高清资源稳稳当当存到本地是另一码事。之前我也试过各种在线解析网站和浏览器插件,不是要会员就是限速,遇到分P视频更是麻烦。后来索性自己用Python写了一个带实时进度显示的B站视频下载爬虫,把从视频页提取数据到音画合并的完整流程跑通了一遍,顺便把进度条也做进去了。这篇文章就把这套方案的思路、关键细节和踩坑记录完整拆出来,给有同样需求的朋友做个参考。
1. 整体设计与方案选择
1.1 为什么选接口解析而不是网页HTML解析
B站视频页面是典型的动态渲染前端,视频的真实地址藏在JavaScript异步请求里,直接抓HTML源码只能拿到一堆脚本标签和初始化数据,拿不到真正的视频流地址。早期很多爬虫靠正则从window.__playinfo__里抠数据,但B站页面改版频繁,这种硬解析方式说废就废。
我这边选择直接模拟B站客户端调用官方接口,通过视频页的HTML拿到aid(稿件ID)和cid(分P ID),再请求播放信息接口获取视频流地址。接口虽然是公开的,但必须带上正常的请求头伪装成用户浏览器访问,否则容易被拦截或者直接返回风控错误。
提示:目前B站的播放信息接口返回的是DASH格式的音画分离流,视频轨和音频轨分开存放,需要分别下载后用工具合并。这是平台为了节省带宽和防盗链做的设计,下载器必须适配这种格式。
1.2 技术选型:requests加标准库就够
对比过scrapy和aiohttp等方案后,我最终选了requests加标准库的组合。B站下载本质是“拿接口数据,再依次下载几个文件”,并发要求不高,requests的同步流式下载配合tqdm进度条已经足够好用,不必引入过于复杂的异步框架。
此外,音画合并环节用了ffmpeg,通过子进程调用命令行完成。requests负责网络请求,ffmpeg负责本地文件处理,各司其职,代码结构非常清晰,也方便日后维护。
依赖清单如下:
requests:处理所有HTTP请求和流式下载tqdm:实现下载进度条的可视化re、json:数据提取与解析subprocess:调用ffmpeg合并音视频os:文件路径与命名处理
安装命令就一条:
pip install requests tqdm另外提前下载好ffmpeg并配置到系统环境变量里,后面合并音视频的时候要用。
2. 核心机制拆解:B站视频流是怎么组织的
2.1 从视频页到cid的获取过程
每个B站视频都有唯一的bvid(形如BV1xx411c7mD),通过bvid可以构造出视频页地址。用requests请求这个页面后,在HTML源码里能找到一个包含视频信息的全局对象。有两种提取方式:
第一种是正则匹配"aid":\d+这种方式去抠,优点是代码简单,缺点是页面一旦调整字段格式就容易失效。第二种是用json解析嵌入的数据结构,相对稳健一些。
我实际用的方法是匹配"cid":\d+和"aid":\d+"两个字段。需要注意的是,如果视频是多P合集,HTML初始数据里通常包含所有分P的cid列表,但直接解析会比较繁琐,更稳妥的做法是额外请求分P列表接口拿到每个分P对应的cid,再逐一下载。
2.2 DASH流与音画分离机制
B站的高清视频使用的是DASH自适应流媒体技术,会把视频画面和音频轨拆成独立的文件流。播放的时候由播放器同时拉取两条流然后同步播放,但下载下来就是两个文件:一个纯画面(多半是.m4s格式),一个纯音频(同样.m4s格式)。拿到这两个文件之后,必须使用ffmpeg把它们合成为一个完整的视频文件。
这就是为什么很多初次接触的人会疑惑:明明一个视频,怎么解析出来一堆链接?其实视频轨和音频轨各对应一个链接,缺一不可。单纯下载视频轨得到的文件没有声音,只下载音频轨又看不到画面。
好在接口返回的JSON数据里已经把视频轨和音频轨分好了类,通常视频轨道在dash.video列表,音頻轨道在dash.audio列表。我们只需要分别取最高画质和最高音质的URL进行下载即可。
2.3 请求头伪装和Referer校验
B站对视频下载请求的校验有两道硬门槛:
第一道是User-Agent。必须伪装成主流浏览器的UA,直接裸用Python默认UA会被识别为爬虫,大概率返回-412状态码(请求被拦截)。
第二道是Referer。下载视频流时,请求头里必须带Referer: https://www.bilibili.com/,否则服务器会拒绝返回完整的视频流数据。这一点是B站防盗链的核心逻辑,很多新人在这一步翻车。
我实际使用的请求头模板如下:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://www.bilibili.com/", }3. 实操过程:完整实现一个带进度条的下载器
3.1 获取video与audio流的地址
以BV1xx411c7mD为例,第一步是请求视频页面获取aid和cid。我写了这样一个函数:
import re import requests def get_page_info(bvid): url = f"https://www.bilibili.com/video/{bvid}" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } resp = requests.get(url, headers=headers) resp.encoding = "utf-8" html = resp.text aid = re.search(r'"aid":(\d+)', html).group(1) cid = re.search(r'"cid":(\d+)', html).group(1) return aid, cid这里有个细节:正则匹配的时候,页面源码里可能出现多个匹配项,但第一个aid和第一个cid通常就是当前页对应的主视频,实际测试下来基本稳定。
拿到这两个关键参数后,接着请求播放信息接口。接口地址是:
api_url = f"https://api.bilibili.com/x/player/playurl?bvid={bvid}&cid={cid}&fnval=16"fnval=16这个参数很关键,它告诉服务器我要返回DASH格式的数据,也就是音视频分离的URL列表。如果不带这个参数,可能只能拿到flv格式的低清流,或者拿不到完整的音视频解析结果。
接口返回的JSON里包含dash.video和dash.audio两个数组。视频数组里每个元素都有id字段表示清晰度,比如64代表720P,80代表1080P,112代表1080P高码率。音频数组里通常有不同码率的音軌,优先选bandwidth最大的那个即可。
选择最高画质的逻辑非常简单:
video_list = data["dash"]["video"] video_info = video_list[0] # 默认第一个就是最高画质 for item in video_list: if item["id"] > video_info["id"]: video_info = item audio_list = data["dash"]["audio"] audio_info = max(audio_list, key=lambda x: x["bandwidth"])3.2 流式下载与进度显示
拿到URL之后,进入实际下载环节。这里比较关键的一点是必须用stream=True参数让requests以流式方式接收数据,否则一次性加载整个文件到内存里,遇到几个GB大视频时内存直接爆掉。
进度条我用tqdm实现,先通过响应头里的Content-Length字段拿到文件总大小,再按块迭代写入本地文件。一个可复用的下载函数长这样:
from tqdm import tqdm def download_file(url, filepath, headers): resp = requests.get(url, headers=headers, stream=True) total_size = int(resp.headers.get("Content-Length", 0)) chunk_size = 1024 * 1024 # 1MB with open(filepath, "wb") as f, tqdm( total=total_size, unit="B", unit_scale=True, desc=filepath.split("/")[-1], ncols=100 ) as bar: for chunk in resp.iter_content(chunk_size=chunk_size): f.write(chunk) bar.update(len(chunk))iter_content是流式下载的核心方法,每次拉取1MB数据写入文件,tqdm根据写入字节数实时更新进度条。整个过程非常直观,终端里能看到文件名、下载速度、剩余时间,体验很接近专业的下载工具。
下载完成后目录下会产生两个临时文件,命名我习惯用video_temp.m4s和audio_temp.m4s,方便合并脚本统一识别。
3.3 音画合并与文件清理
最后一步是用ffmpeg把两条流合成一个完整视频。命令行参数不复杂:
ffmpeg -i video_temp.m4s -i audio_temp.m4s -c copy output.mp4-c copy表示直接复制流,不重新编码,速度非常快,一个30分钟的视频通常几秒到十几秒就能完成,画质也零损失。由于我是在Python里调用,写成子进程执行:
import subprocess def merge_video_audio(video_path, audio_path, output_path): cmd = [ "ffmpeg", "-y", "-i", video_path, "-i", audio_path, "-c", "copy", output_path ] subprocess.run(cmd, check=True) os.remove(video_path) os.remove(audio_path)-y参数是让ffmpeg覆盖同名文件时不弹确认提示,方便集成到脚本里自动执行。合并完成后再把两个临时文件删掉,整个流程就闭环了。
4. 常见问题与排查技巧实录
4.1 请求被拦截,返回-412状态码
这是最常见的问题。排查思路很明确:先看看请求头里的UA和Referer是不是带齐了。如果带了还是被拦截,有一个很有效的补救措施——给requests会话添加一个Cookie,从浏览器里复制任意一个B站登录态的SESSDATA字段(buvid3也可以)。即便不是VIP账号,带上Cookie后风控概率会大幅下降。
4.2 up主设置了禁止下载
部分up主在投稿时会开启“禁止下载”选项,这种情况下接口返回的视频流URL是带水印的版本,即使下载下来画面上也会有明显的B站水印和up主ID。另外还有一种情况是充电专属视频,非充电用户无法获取播放地址。
这不是爬虫代码能绕过的限制,属于平台规则层面。我的处理方式是在代码里增加一个检测步骤:请求播放接口后判断返回的URL里是否包含mcdn.bilivideo.cn或者upg-bc字样,如果带这些特征就提示用户该视频可能受下载限制,而不是闷头下载一个带水印的版本。
4.3 下载到一半网络中断
视频文件大的时候网络波动难免。requests的流式下载如果中途断掉,已写入的本地文件并不完整,重新下载只能从头再来。我在实际使用中做了两个优化:
第一个是给真实下载函数增加超时重试机制。第二个是结合tqdm进度条显示的已下载大小,对特别大的文件可以手动中断后只下载缺失区段。这个比较复杂,日常使用中做重试就够。
重试的简化写法如下:
for attempt in range(3): try: download_file(url, filepath, headers) break except Exception as e: print(f"第{attempt+1}次下载失败: {e}") time.sleep(3)4.4 ffmpeg合并时报错“Unknown encoder”
这个多半是ffmpeg安装版本太老或者没有正确配置。建议去官网下载完整版而不是精简版,Windows用户下载后把bin目录路径添加到系统环境变量Path里。验证安装是否成功的方法很简单,命令行执行ffmpeg -version,能正常输出版本信息就说明配置OK。
5. 使用效果与实际体验
整套脚本跑下来,当一个普通1080P视频(约30分钟,大小在500MB到1GB之间)下载时,视频轨和音频轨是并行串行下载的,两段加起来大概1分钟到2分钟左右,快速取决于网速。之后ffmpeg合并阶段由于是直接复制流,通常不到5秒就能出片。
片头片尾、弹幕这些都不需要特殊处理,下载的就是纯正的正片内容。如果想连弹幕一起保存,B站有独立的弹幕接口,按cid就能取到XML格式的弹幕,这个以后再单独写一篇展开。
我个人用这套脚本最大的感受是:B站的下载限制比想象中少,只要用正规接口并带上合规的请求头,大部分公开视频都能顺利下载。关键在于理解它的接口设计逻辑,而不是跟页面结构硬刚。学会通过接口拿数据的思路之后,其他视频平台的下载器也差不多是同一个套路,只是参数和格式细节不同。希望这篇内容能给想自己动手做下载工具的朋友提供一些真实可参考的经验。
本文还有配套的精品资源,点击获取