Python批量爬取B站小视频:从Web API到wbi签名全解析
简介面向Python爬虫初学者和需要批量获取B站小视频的开发者这份资源用单个脚本演示了从网络请求到数据解析的完整过程。脚本综合使用requests发送请求、借助HTML解析库解析页面并实现了模拟登录、会话管理、分页遍历与异常重试机制能够应对B站对非官方请求的常见限制。针对视频ID、标题、作者等字段代码提供了清晰的选择器示例方便改造成关键词筛选、数据统计或CSV存储等实用功能。脚本对每一步关键代码均做了注释包括请求头构造、会话传递、翻页链接拼接等细节即使遇到反爬限制也可参考内置的重试与延时策略调整请求频率。压缩包仅含一个py文件体积约2KB结构紧凑、依赖少解压后即可直接阅读运行。已有937人浏览学习适合作为Python爬虫入门练习或课程设计的参考脚本也可为后续学习Scrapy等框架打下基础。1. python批量爬取b站小视频它到底帮你省掉什么做短视频运营的人经常要盯竞品每天发了什么做内容库的朋友天天在找可复用的素材训练多模态模型的人则需要攒一批带标题、时长、UP主信息的视频样本。手动一个页面一个页面存一天存三百条就到头了而用python批量爬取b站小视频同样的量大概只需要十几分钟还顺手把标题、时长、分区、UP主这些元数据一并落盘。我的建议是别找现成脚本先搭一套自己能改的采集基座B站接口的签名方式换过好几次别人给的脚本今天能用明天就翻车。这篇笔记会把方案选型、最小实现、参数配置和踩坑点一次讲完。2. 先定方案再动手为什么优先选公开Web API而不是直接解析页面2.1 三种主流方案怎么选搜索接口、页面解析与开放平台动手之前先过一遍方案不走弯路。社区里做b站爬虫主流路线有三条直接用B站网页版背后的公开Web API用requests加解析库去解析HTML页面以及接入B站开放平台。三者的差别非常大我用一张表把它说清楚。方案实现成本翻车频率是否要登录适合谁公开Web APIsearch/type、view、playurl中要处理wbi签名低接口稳定但偶尔改签名低清晰度可不登录高清要cookie大多数个人采集需求解析HTML页面低正则或BeautifulSoup即可高页面结构一改就断不需要只爬少量页面不在乎后续维护开放平台高要申请资质低必须企业级、有合规授权的项目我一般直接选公开Web API。理由有三个第一页面解析看上去门槛低但它依赖DOM结构B站前端迭代很勤class名、标签层级说改就改今天能跑明天报错每次都要重新抓包定位元素第二页面里能拿到的字段远少于APIAPI直接返回JSON标题、时长、播放量、UP主、分区一次性给全第三API接口本身就是B站网页版在用的通道只要请求特征像正常浏览器存活时间远长于写死选择器的方案。开放平台虽然合规性最好但个人开发者去申请接口权限周期长、门槛高做个小批量采集没必要。综合下来公开Web API是投入产出比最高的路线。2.2 必懂的两个接口参数BV/cid 与 fnval选定了Web API之后有几个基础概念必须搞明白否则后面代码看不懂、报错也摸不着头。第一个概念是BV号和cid。BV号是B站稿件的公开身份标识类似文章的ID比如“BV1GJ411x7h7”。但它只定位到“一个视频”定位不到“视频里的哪一P”。B站很多视频是多P合集P1、P2各有自己的cid下载的时候必须同时拿到bvid和cid才能拼出准确的流媒体地址。view接口的作用是输入bvid返回这个稿件的标题、分区、时长以及默认分P的cid如果要处理多P接口的pages数组里也会给出每P的cid逐项取即可。第二个概念是fnval。它控制流媒体接口返回什么格式常见取值是0和16。fnval0时返回带durl的MP4或FLV格式一个视频就是一个完整文件拿回来就能直接播放fnval16时返回DASH格式视频轨和音频轨分开成两个M4S文件画质上限更高但需要额外用ffmpeg合并处理成本明显上涨。我的建议是批量下载小视频用fnval0省掉合并步骤单文件既方便人工抽查也方便脚本做后续处理。qn参数则控制清晰度64对应720P80对应1080P320则对应杜比视界等特殊规格。对素材采集来说720P通常够用流量成本也低。2.3 动手前的合规红线什么能爬、什么不能碰技术能实现不代表什么都该爬。我在给团队搭采集工具时会先用几句话把边界定死避免以后惹麻烦。第一只爬公开内容。公开发布、无需登录就能看到视频信息的内容属于平台公开数据但充电专属视频、付费课程、私密收藏夹这类内容无论技术上能不能绕过限制都不应该碰。B站对这些内容有明确的访问控制绕过它属于典型的违规行为。第二控制请求频率。公开API的免费属性是对正常用户而言的把接口当下载机一天刷几十万次既影响平台运营也可能让自己的IP被风控标记。常见的做法是每两个请求之间至少间隔0.5到1秒并发线程控制在4到8以内。后面第5章会给出具体参数。第三数据只用于个人学习、内容研究、非商业性素材整理等场景不要做二次转售更不要把它包装成所谓的“资源站”。2.4 wbi签名拦住大多数爬虫的第一道门禁B站网页版的大部分接口现在都要求wbi签名。所谓wbi签名是B站在前端维护的一套轻量级请求签名机制本质上是往请求参数里追加两个字段wts和w_rid。wts是当前Unix时间戳w_rid是对排序后的参数拼接一个密钥串做MD5得到的结果。密钥串不是固定的而是从nav接口的返回里动态取两张图片的URL文件名拼出来的img_key加sub_key。这意味着写爬虫时不能把签名写死必须先请求一次nav接口拿key再对本次请求参数做签名。这个机制的实际效果是不带w_rid的请求会被直接拒绝带错签名的请求会被风控系统盯上。早期很多爬虫脚本失效就是因为B站把之前静态密钥换成了动态获取。好在它并不复杂拿到img_key和sub_key后排序、拼串、MD5这三步就能完成。下一章我会直接给出完整的工具函数照抄就能跑通。但有一点要提醒接口签名算法存在被B站再次调整的可能如果你的脚本在某个时间点突然全部返回风控码优先去抓包看看nav接口返回结构有没有变化。3. python批量爬取b站小视频的最小可运行实现3.1 第一步用搜索接口拿候选小视频列表批量采集的第一步是把候选视频找出来我用B站的搜索接口来拉列表按发布时间排序再把时长小于等于5分钟的结果筛出来这就是我们说的“小视频”。# -*- coding: utf-8 -*- b站小视频采集搜索候选列表 wbi签名工具 import hashlib import re import time import requests from urllib.parse import urlencode UA Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \ (KHTML, like Gecko) Chrome/120.0 Safari/537.36 SESSION requests.Session() SESSION.headers.update({ User-Agent: UA, Referer: https://www.bilibili.com, }) # 从nav接口拿wbi密钥 def get_wbi_keys(): url https://api.bilibili.com/x/web-interface/nav resp SESSION.get(url, timeout(3, 10)) data resp.json().get(data, {}) img_url data[wbi_img][img_url] sub_url data[wbi_img][sub_url] img_key img_url.rsplit(/, 1)[1].split(.)[0] sub_key sub_url.rsplit(/, 1)[1].split(.)[0] return img_key, sub_key # 给请求参数加上wbi签名 def enc_wbi(params, img_key, sub_key): mixin_key img_key sub_key params {k: str(v) for k, v in params.items() if k not in (w_rid, wts)} params[wts] int(time.time()) params dict(sorted(params.items())) params {k: re.sub(r[!()*], , v) for k, v in params.items()} query urlencode(params) w_rid hashlib.md5((query mixin_key).encode()).hexdigest() params[w_rid] w_rid return params # 把接口返回的时长字符串转成秒数 def duration_to_seconds(duration_text): parts [int(x) for x in duration_text.split(:)] if len(parts) 2: return parts[0] * 60 parts[1] if len(parts) 3: return parts[0] * 3600 parts[1] * 60 parts[2] return 0 def search_videos(keyword, page1, max_seconds300): params enc_wbi({ search_type: video, keyword: keyword, page: page, page_size: 20, order: pubdate, }, *get_wbi_keys()) url https://api.bilibili.com/x/web-interface/wbi/search/type resp SESSION.get(url, paramsparams, timeout(3, 10)) result resp.json().get(data, {}).get(result) if not result: return [] videos [] for item in result: seconds duration_to_seconds(item.get(duration, 0:00)) if 0 seconds max_seconds: videos.append({ bvid: item[bvid], title: re.sub(r[^], , item[title]), duration: item.get(duration), seconds: seconds, author: item.get(author), }) return videos这段代码里有三个关键点。第一get_wbi_keys每次都实时从nav接口取密钥不写死避免密钥滚动后脚本失效。第二enc_wbi做了三件重要的事过滤掉非法字符、排序、拼key算MD5顺序颠倒都会导致签名被拒。第三orderpubdate表示按最新发布排序适合做增量采集如果你想要热度高的内容把order换成click即可。max_seconds300是我对“小视频”的定义5分钟以内的短片既符合标题诉求也方便后续处理。搜索接口一次最多返回20条要更多内容就循环翻页但翻页频率要压低下一页的请求延迟建议放到1秒以上。3.2 第二步view接口批量解析详情与下载直链拿到候选bvid列表之后需要逐个解析元数据和下载直链。元数据用view接口下载地址用playurl接口两个接口配合使用。# 解析单个视频的元数据并取其MP4直链 def fetch_meta(bvid): url https://api.bilibili.com/x/web-interface/view resp SESSION.get(url, params{bvid: bvid}, timeout(3, 10)) data resp.json() if data.get(code) ! 0: return None info data[data] return { bvid: bvid, title: info[title], cid: info[cid], duration: info[duration], owner: info[owner][name], } def get_play_url(bvid, cid): url https://api.bilibili.com/x/player/playurl params { bvid: bvid, cid: cid, qn: 64, # 720P够用且流量小 fnval: 0, # 返回MP4/FLV单文件免音视频合并 } resp SESSION.get(url, paramsparams, timeout(3, 10)) data resp.json().get(data) if not data or durl not in data: return None return data[durl][0][url]这里的逻辑是先通过view接口拿cid再用bvid和cid拼playurl请求。fnval0让接口返回单文件流地址省去DASH格式的合并步骤。qn64意味着请求的是720P如果视频本身没有720PB站会自动降级。对于只需要素材的采集场景这个配置最省事。要注意的是view接口返回的cid是对应视频的默认分P处理多P视频时需要翻阅data内的pages字段逐P取cid再调playurl。另外如果爬虫脚本部署在服务端我建议在fetch_meta失败时打印响应原文很多问题通过“message”字段就能一眼定位不需要反复猜。3.3 第三步并发下载与断点续传落地解析出直链之后最后一步是把视频文件下载到本地。这一步不能太粗暴直接requests.get一个URL然后写文件的做法在批量场景里非常容易翻车我加入了断点续传和并发控制。import os from concurrent.futures import ThreadPoolExecutor, as_completed def download_video(url, filepath): 下载单个直链支持断点续传失败返回状态和原因 headers { User-Agent: UA, Referer: https://www.bilibili.com/, } first_byte os.path.getsize(filepath) if os.path.exists(filepath) else 0 if first_byte 0: headers[Range] fbytes{first_byte}- try: resp SESSION.get(url, headersheaders, streamTrue, timeout(3, 20)) except requests.RequestException as e: return False, f请求异常: {e} if resp.status_code 416: return True, 已完整无需续传 if resp.status_code not in (200, 206): return False, fHTTP {resp.status_code} with open(filepath, ab) as f: for chunk in resp.iter_content(chunk_size256 * 1024): f.write(chunk) return True, ok def crawl_one(video, save_dir): 采集单个视频先取直链再下载最后落元数据 meta fetch_meta(video[bvid]) if not meta: return video[bvid], False, view接口解析失败 play_url get_play_url(meta[bvid], meta[cid]) if not play_url: return video[bvid], False, playurl解析失败 filepath os.path.join(save_dir, meta[bvid] .mp4) ok, msg download_video(play_url, filepath) if ok: with open(filepath .json, w, encodingutf-8) as f: json.dump(meta, f, ensure_asciiFalse, indent2) return video[bvid], ok, msg def batch_crawl(keyword, pages3, max_workers4, save_dirvideos): os.makedirs(save_dir, exist_okTrue) candidates [] for page in range(1, pages 1): candidates.extend(search_videos(keyword, pagepage)) time.sleep(1.5) # 翻页间隔降低风控概率 with ThreadPoolExecutor(max_workersmax_workers) as pool: futures {pool.submit(crawl_one, v, save_dir): v for v in candidates} for fut in as_completed(futures): bvid, ok, msg fut.result() print(bvid, ok, msg)这段代码有三个设计细节值得说。第一断点续传依赖Range头下载前先看本地文件是否存在存在就从已有字节数开始请求如果服务端返回416说明文件其实已经完整。第二下载请求必须带RefererB站的流媒体CDN会校验这个头不带Referer的请求大概率得到403。第三ThreadPoolExecutor的线程数要克制max_workers4是起步值网络好的时候可以提到8但继续往上加就要面对风控风险了。每一轮批量任务跑完后同目录下会生成对应的json文件里面存着标题、时长、UP主、cid等元数据供后续检索和筛选。想要更高画质就把qn改为80但代价是流量成倍上涨并且高清直链往往要求登录cookie没有登录态可能被降级。4. 批量采集b站小视频的5个常见翻车现场与排查方法4.1 风控码 -352请求一频繁就被后台丢进黑匣子现象脚本跑得好好的突然某一次请求返回JSON里的code变成-352message是“风控校验失败”之后再怎么重试都拿不到结果。原因-352是B站风控系统的统一拒绝码可能由三类情况触发——wbi签名错误、请求频率过快、请求头特征太像爬虫。我见过有同学凌晨跑脚本把并发开到16半小时后所有请求集体-352明显是频率触发风控。解决先确认签名没问题。把enc_wbi生成后的params打印出来去B站网页版抓一个正常搜索请求做对比wts和w_rid的生成逻辑对不对一眼就能看出。如果签名正常那就是频率问题解决方法很简单降低并发、拉大请求间隔。我常用的间隔是每次请求之间随机sleep 0.8到1.8秒夜间跑批量任务时再加一倍。需要提醒的是不要试着绕过风控被标记后最好的处理是停下来等几分钟再恢复。4.2 下载直链403Referer和UA是必填项现象playurl接口能正常返回直链但拿着直链去下载时requests返回403浏览器里打开却正常。原因B站的流媒体CDN单独做了防盗链校验只认带正确Referer的请求。直链本身是公开的但裸请求拿不到。解决下载请求必须带上Referer: https://www.bilibili.com/同时保持常见的浏览器UA。我在download_video函数里把这两个头都写死这是踩过坑之后养成的习惯。还有一个细节容易被忽略不要复用搜索接口的Session却清空headersB站对header指纹的一致性有校验Session的headers在进程内要保持统一。4.3 下载到一半卡死没有超时、没有重试现象日志停在某个视频的下载过程十分钟都没有输出程序也不退出。原因requests默认没有超时会一直挂起更常见的是B站CDN断流客户端一直等数据但连接已经死了。解决所有请求都要显式设置超时我习惯用元组表示连接超时和读取超时比如timeout(3, 20)下载用stream加iter_content分块写盘避免一次性载入内存把机器拖垮。更进一步的做法是给下载函数包一层重试遇到断流就重新解析playurl拿到新直链后配合Range头续传。注意直链本身有有效期过了几小时再续传会失效所以重试时一定要重新获取直链而不是死磕旧URL。4.4 返回-404BV号失效与稿件状态判断现象view接口返回code -404message是“啥都木有”但同一个BV号在浏览器里能正常打开。原因这里可能是接口侧的风控误判也可能是稿件本身状态异常。UP主删除稿件、转为自己可见、撞了版权被屏蔽都会导致接口侧查无此稿另外BV号大小写或复制时漏字符也会导致-404。解决先把BV号拿去浏览器验证手工打开确认稿件是否真实存在。如果浏览器能打开但接口报-404那就是请求被风控误伤了等待一段时间再重试。如果稿件真的没了处理方式很简单记录日志、跳过、继续下一个。批量采集时不要因为某个-404中断整个任务用try/except包住单条处理逻辑保证异常不影响整体进度。4.5 清晰度全线降级登录态过期与cookie保活现象前几天还能拿到720P直链某天开始所有视频都只能拿到240Pqn64形同虚设。原因B站对高清直链有登录态校验。没登录或cookie过期时接口默认返回最低清晰度。我的Session是复用的cookie过期后整个进程都受影响。解决把浏览器登录后拿到的cookie注入SESSION特别是SESSDATA字段。具体做法是在代码开头加一行SESSION.cookies.set(SESSDATA, 你的cookie值)。要注意B站账号同一时间只允许有限设备在线用脚本频繁刷新登录态会顶掉浏览器端所以要避免并发登录同一账号。我一般会单独用一个不常用的小号跑采集浏览器端不受影响。cookie有效期通常为一到两个月过期后接口会静默降级而不是报错所以下载后要抽检文件大小发现异常立刻检查登录态。5. 把脚本升级成工具箱并发、限速、去重与验证的最后一公里5.1 并发线程数、延时区间和重试次数怎么配批量采集跑长任务时参数配置直接影响存活率。我在多次翻车后总结了一套比较稳的参数供你参考。并发线程数默认4网络条件好可以提到6到8但不要再高每次请求完成后sleep 0.8到1.8秒翻页时额外加0.5秒超时用连接3秒、读取10秒重试3次每次退避等待时间按2倍递增。这套参数跑300到500个视频基本不会被风控卡死。参数项推荐值说明并发线程数4风控偏紧时可降到2请求间隔0.8 - 1.8秒随机模拟人工节奏连接/读取超时3秒 / 10秒防止请求挂死重试次数3每次退避等待递增视频时长上限300秒按“小视频”目标设定5.2 增量去重只爬没见过的视频批量任务最怕重复劳动。我习惯在保存目录里放一个candidates_seen.txt每次爬取前把bvid读进内存集合新请求到的bvid如果在集合里就跳过没见过的才加入下载队列。另外下载前检查同名mp4文件是否已存在存在但大小只有几KB就删除重下存在且大于1MB就跳过。这个逻辑看起来简单却能把重复采集和半截文件带来的麻烦一次解决。5.3 验证下载质量文件大小与ffprobe抽检下载完不是终点。我每次跑完批量任务都会做两步抽检第一步看文件大小分布批量统计数据里很多小于100KB的mp4基本是坏文件第二步用ffprobe读取随机抽出的几个文件确认时长和分辨率符合预期。发现问题就回到第4章的排查清单看是直链解析异常还是下载断流。整套脚本加个入口函数后启动命令长这样python bili_small_video.py --keyword 萌宠 --pages 3 --save_dir videos --max_workers 4我第一次做b站小视频采集时以为拿到直链就万事大吉结果一个晚上被-352、403、半截文件轮着教育。后来把并发调低、把重试和去重补齐脚本才真正能挂着跑完一整夜。现在每次发布新任务前我都会先跑10条验证通过再放开全量这个习惯让我少熬了好几个夜。希望帮到你。本文还有配套的精品资源点击获取