
简介本资源是一份面向Python初学者与爬虫入门者的实用脚本聚焦B站小视频的批量采集需求解决网页结构解析、登录态维持、分页请求及基础反爬应对等典型问题。压缩包为单文件ZIP格式内含1个核心Python脚本.py体积仅2KB代码精简涵盖requests模拟请求、Cookie会话管理、HTML内容提取与异常重试逻辑适合作为教学示例或轻量级爬取起点。目前已有932人学习下载脚本已封装基础流程读者可直接运行调试快速掌握从登录鉴权、URL参数构造、视频链接抽取到结果初步输出的完整链路同时为后续扩展存储模块如CSV/JSON导出与并发优化提供清晰的代码骨架与排错切入点。1. 项目缘起为什么需要批量获取B站视频内容最近在做一个内容分析的小项目需要收集一批特定主题的短视频作为研究素材。B站哔哩哔哩无疑是国内最大的年轻人文化社区和视频平台之一上面有海量的UGC用户生成内容和PGC专业生产内容。手动一个个去下载效率实在太低而且容易出错。比如我需要分析某个UP主近三个月所有视频的标题关键词趋势或者想批量下载某个教程合集离线学习手动操作几乎不可能完成。这时候Python爬虫就成了一个非常自然的解决方案。用脚本自动化地获取视频信息、解析真实播放地址并下载可以极大地解放生产力。不过B站作为一个大型平台其前端架构和反爬机制也在不断演进直接写个简单的requests请求就想拿到数据大概率会碰壁。这背后涉及到网页动态渲染、API接口调用、参数签名等一系列技术点。今天我就结合自己最近的一次实践来详细拆解一下如何用Python相对稳定、合规地批量爬取B站的小视频。这里强调“合规”是指我们的操作应仅限于个人学习、研究之目的严格遵守网站的robots.txt协议控制请求频率避免对服务器造成不必要的压力。2. 核心思路拆解从网页到视频文件的完整链路要实现批量爬取我们得先理清楚从我们在浏览器看到视频到最终得到一个.mp4文件中间经历了哪些步骤。理解了这个链路我们才能知道代码应该在哪里“介入”。2.1 目标视频的定位与列表获取首先我们需要一个视频列表。这个列表的来源可以是多样的单个UP主的主页获取该UP主发布的所有视频。某个频道的合集或列表比如一个教学课程的所有分集。搜索关键词的结果获取与特定主题相关的一批视频。一个包含多个BVIDB站视频ID的文本文件这是最直接的方式。无论来源是什么我们最终都需要得到一系列视频的唯一标识符。B站目前主要使用两种IDBVID这是现在最常见的视频ID是一串由数字和大小写字母组成的字符串例如BV1xx411c7mh。它出现在视频的URL中如https://www.bilibili.com/video/BV1xx411c7mh。AVID这是较早的数字ID现在依然被内部API使用例如170001。通常可以通过BVID转换得到。我们的第一步脚本就是要根据输入如UP主UID、搜索词来获取一个干净的BVID列表。2.2 视频元数据与播放地址的解析拿到BVID后下一步是获取视频的详细信息标题、作者、分P信息、封面图等和最关键的——视频和音频的真实直链地址。这里就是主要的技术攻坚点。B站的前端页面是动态渲染的视频数据通过API接口异步加载。直接解析https://www.bilibili.com/video/BVxxxxxx这个页面的HTML源码是找不到.mp4链接的。这些链接被保护了起来需要通过调用B站的内部分享API来获取。一个常用的、公开的API是https://api.bilibili.com/x/web-interface/view?bvid{BVID}。这个接口会返回视频的详细信息包括标题、描述、分Ppage列表等。但对于播放地址它返回的仍然是需要进一步解析的playurl信息而不是直接的HTTP链接。更直接的地址获取往往需要模拟播放器的请求其中可能涉及到一个叫cid内容ID的参数和session会话概念。这个过程可能需要解析更复杂的接口并且接口参数可能带有加密签名如w_rid和wts以防范简单的爬取。2.3 音视频流的下载与合并B站的高清视频通常会将视频流无声音和音频流分开存储这是为了适配不同的码率和实现高效的流媒体传输如DASH协议。因此我们通过API获取到的可能是分开的video.m4s和audio.m4s文件地址或者flv分段地址。我们需要分别下载视频流和音频流文件然后使用本地工具如FFmpeg将它们合并成一个完整的.mp4或.flv文件。FFmpeg是一个强大的开源音视频处理库通过命令行即可完成合并操作。2.4 合规性与稳健性设计在整个过程中我们必须考虑请求头Headers需要模拟一个真实浏览器的请求特别是User-Agent和Referer字段。Referer通常需要设置为视频所在页面的URL否则服务器可能拒绝返回数据。访问频率在循环中批量请求时必须在请求之间加入随机延时例如time.sleep(random.uniform(1, 3))避免触发服务器的频率限制或封禁IP。错误处理网络超时、API接口变更、视频失效被删除或转为私密等情况都需要被捕获和处理避免一个视频失败导致整个脚本崩溃。Cookie对于某些需要登录才能观看的视频如大会员专享可能需要提供有效的登录Cookie。但这涉及到账户安全需格外谨慎且仅用于个人合法授权的资源。3. 技术栈选择与工具准备工欲善其事必先利其器。下面是我本次项目选择的核心工具库及其原因。3.1 网络请求库requests 与 httpxrequestsPython社区最知名、最易用的HTTP库文档丰富生态完善。对于大多数常规GET/POST请求它是不二之选。httpx一个现代化的HTTP客户端支持HTTP/1.1和HTTP/2并且完全兼容requests的API。它的异步支持async/await在处理大量IO密集型任务如批量下载时性能远超同步的requests。在本项目中我们可以用requests进行简单的页面和API抓取用httpx的异步客户端来高效并发下载音视频流文件。安装命令pip install requests httpx3.2 数据解析库BeautifulSoup4 与 jsonBeautifulSoup4 (bs4)当我们需要从UP主主页、搜索列表页面等地方提取视频BVID时这些信息通常直接嵌在HTML中。BeautifulSoup可以帮我们轻松地解析HTML文档使用CSS选择器或标签导航来提取所需数据。jsonPython标准库。B站的API接口返回的数据基本都是JSON格式直接用json.loads()就能解析成Python的字典或列表非常方便。安装命令pip install beautifulsoup43.3 音视频处理核心FFmpeg这不是一个Python库而是一个必须预先安装在操作系统环境中的命令行工具。它负责将下载好的独立视频流和音频流文件合并封装。下载与安装前往FFmpeg官网下载对应系统的静态编译版本解压后将bin目录的路径添加到系统的环境变量PATH中。验证安装打开命令行CMD或Terminal输入ffmpeg -version如果显示版本信息则安装成功。3.4 其他辅助库asyncio / aiofilesPython内置的异步IO框架和异步文件操作库与httpx异步客户端配合实现高性能的并发下载。tqdm一个强大的进度条库可以在循环中显示精美的进度条让我们对下载进度一目了然。pandas可选。如果我们获取到的元数据标题、播放量、上传时间等后续需要进行数据分析pandas是处理表格数据的利器。安装命令pip install aiofiles tqdm pandas4. 实战步骤详解从零到一构建爬虫下面我将以一个具体的例子来演示批量下载某个UP主最新发布的20个视频。4.1 第一步获取UP主主页视频列表首先我们需要找到UP主的空间主页URL。通常格式为https://space.bilibili.com/{UID}其中UID是一串数字。我们需要从这个页面提取出视频列表。注意直接访问空间主页视频列表可能是通过滚动动态加载的。更稳定可靠的方法是访问其投稿页面例如https://space.bilibili.com/{UID}/video。我们可以通过查看这个页面的网络请求浏览器开发者工具 - Network - XHR找到一个返回视频列表数据的API。一个常见的模式是https://api.bilibili.com/x/space/arc/search?mid{UID}ps30tid0pn1keywordorderpubdatejsonpjsonp参数说明mid: UP主的UID。ps: page size每页数量这里设为30。pn: page number页码从1开始。order: 排序方式pubdate按发布时间排序。我们写一个函数来获取BVID列表import requests import json from typing import List def get_bvid_list_by_uid(uid: str, max_count: int 20) - List[str]: 根据UP主UID获取其视频BVID列表 :param uid: UP主的UID :param max_count: 想要获取的最大视频数量 :return: BVID列表 bvid_list [] page 1 page_size 30 # 每页请求数量B站API最大支持50 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: fhttps://space.bilibili.com/{uid} } while len(bvid_list) max_count: url fhttps://api.bilibili.com/x/space/arc/search?mid{uid}ps{page_size}pn{page}orderpubdate try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() # 检查HTTP状态码是否为200 data resp.json() if data[code] ! 0: print(fAPI返回错误: {data[message]}) break vlist data[data][list][vlist] if not vlist: print(视频列表已全部获取或为空。) break for video in vlist: bvid_list.append(video[bvid]) if len(bvid_list) max_count: break page 1 # 礼貌性延时避免请求过快 time.sleep(1) except requests.exceptions.RequestException as e: print(f请求失败: {e}) break except (KeyError, json.JSONDecodeError) as e: print(f解析响应数据失败: {e}) break return bvid_list[:max_count] # 使用示例 uid 12345678 # 替换为目标UP主的真实UID bvid_list get_bvid_list_by_uid(uid, 20) print(f获取到 {len(bvid_list)} 个BVID: {bvid_list})4.2 第二步根据BVID获取视频详情与播放地址这是最关键也最复杂的一步。我们需要通过BVID获取到cid再用cid去获取音视频流的实际地址。首先通过官方API获取视频基本信息其中包含cid。def get_video_info(bvid: str): 通过BVID获取视频基本信息包括标题、cid等 url fhttps://api.bilibili.com/x/web-interface/view?bvid{bvid} headers { User-Agent: Mozilla/5.0 ..., Referer: fhttps://www.bilibili.com/video/{bvid} } try: resp requests.get(url, headersheaders, timeout10) data resp.json() if data[code] 0: info data[data] title info[title] cid info[pages][0][cid] # 默认取第一P的cid return {title: title, cid: cid, bvid: bvid} else: print(f获取{bvid}信息失败: {data[message]}) return None except Exception as e: print(f获取{bvid}信息时发生异常: {e}) return None接下来使用cid和bvid去获取播放地址。这里情况比较复杂B站有多种画质和格式。一个相对稳定的方法是尝试获取视频的“播放器信息”接口。请注意以下接口和参数可能会随时间变化需要根据实际情况调整。def get_play_url(bvid: str, cid: int): 获取视频播放地址示例接口可能失效 注意此接口需要构造签名参数且经常变动。这里仅为演示逻辑。 # 这是一个示例URL实际参数需要动态生成包括w_rid和wts等签名 # 签名算法是逆向工程的重点也是爬虫与反爬对抗的核心。 # 为了教程的简洁与合规此处不展开逆向细节。 # 一个替代方案是使用一些开源、已维护的第三方封装库。 params { bvid: bvid, cid: cid, qn: 80, # 视频清晰度标识80代表1080P64代表720P fnval: 16, # 视频流格式标识16代表DASH格式 fourk: 1, # ... 其他必要参数如签名 } url https://api.bilibili.com/x/player/playurl headers { User-Agent: ..., Referer: fhttps://www.bilibili.com/video/{bvid} } resp requests.get(url, paramsparams, headersheaders) data resp.json() # 解析data提取出video和audio的urlbase_url segment_base[initialization]等 # 这里需要非常仔细地解析返回的JSON结构 # ... # return video_url, audio_url return None, None # 示例返回重要提示直接解析B站播放接口是技术难点涉及加密参数。对于学习和简单使用我强烈建议优先考虑使用一些成熟、开源且维护良好的第三方Python库例如bilibili-api-python或you-get。这些库已经封装了复杂的接口调用和参数生成逻辑。本教程旨在讲解原理实际生产代码中应优先使用这些工具。4.3 第三步下载音视频流文件假设我们已经通过某种方式比如使用you-get库或成功解析了上述API获得了视频流和音频流的直接URL以.m4s或.flv结尾。我们可以使用httpx进行异步并发下载提升效率。import asyncio import aiofiles import httpx from pathlib import Path async def download_file_async(client: httpx.AsyncClient, url: str, file_path: Path, headers: dict): 异步下载单个文件 try: async with client.stream(GET, url, headersheaders, timeout30.0) as response: response.raise_for_status() total_size int(response.headers.get(content-length, 0)) async with aiofiles.open(file_path, wb) as f: downloaded 0 async for chunk in response.aiter_bytes(chunk_size8192): await f.write(chunk) downloaded len(chunk) # 这里可以添加进度条更新逻辑 print(f下载完成: {file_path.name}) except Exception as e: print(f下载失败 {url}: {e}) # 可以选择删除不完整的文件 if file_path.exists(): file_path.unlink() async def download_video_and_audio(info_dict): 并发下载一个视频的音视频流 bvid info_dict[bvid] title info_dict[title].replace(/, _).replace(\\, _)[:100] # 清理非法文件名 video_url info_dict.get(video_url) audio_url info_dict.get(audio_url) if not video_url or not audio_url: print(f{title} 获取播放地址失败跳过。) return None save_dir Path(./downloads) / bvid save_dir.mkdir(parentsTrue, exist_okTrue) video_path save_dir / video.m4s audio_path save_dir / audio.m4s headers { User-Agent: ..., Referer: fhttps://www.bilibili.com/video/{bvid} } async with httpx.AsyncClient() as client: tasks [] tasks.append(download_file_async(client, video_url, video_path, headers)) tasks.append(download_file_async(client, audio_url, audio_path, headers)) await asyncio.gather(*tasks) return {title: title, bvid: bvid, video_path: video_path, audio_path: audio_path, save_dir: save_dir} # 在主循环中调用 async def main_download_tasks(video_info_list): 主下载任务 tasks [download_video_and_audio(info) for info in video_info_list] results await asyncio.gather(*tasks) return [r for r in results if r] # 过滤掉失败的4.4 第四步使用FFmpeg合并音视频下载完成后我们得到两个独立的文件video.m4s纯画面和audio.m4s纯声音。需要使用FFmpeg将它们合并。import subprocess import sys def merge_media_with_ffmpeg(video_path: Path, audio_path: Path, output_path: Path): 使用FFmpeg合并视频和音频 :param video_path: 视频流文件路径 :param audio_path: 音频流文件路径 :param output_path: 输出文件路径如 .mp4 # 确保FFmpeg在系统PATH中 command [ ffmpeg, -i, str(video_path), -i, str(audio_path), -c:v, copy, # 视频流直接复制不重新编码速度快 -c:a, aac, # 音频流编码为AAC格式通用兼容 -strict, experimental, -y, # 覆盖已存在文件 str(output_path) ] try: # 运行命令并捕获输出避免打印到控制台造成混乱 result subprocess.run(command, capture_outputTrue, textTrue, checkTrue) print(f合并成功: {output_path}) # 合并成功后可选择性删除原始的.m4s文件 # video_path.unlink() # audio_path.unlink() return True except subprocess.CalledProcessError as e: print(fFFmpeg合并失败: {e}) print(f标准错误输出: {e.stderr}) return False except FileNotFoundError: print(错误未找到FFmpeg。请确保已安装FFmpeg并添加到系统环境变量PATH中。) return False # 在下载完成后调用 def process_downloaded_items(download_results): for item in download_results: output_filename item[save_dir] / f{item[title]}.mp4 success merge_media_with_ffmpeg( item[video_path], item[audio_path], output_filename ) if success: print(f视频处理完成: {output_filename})5. 工程化优化与常见问题排查一个能稳定运行的脚本不能只考虑“跑通一次”还要考虑异常处理、日志记录、配置化管理等。5.1 配置与日志记录将关键参数如请求头、延时范围、下载目录等写入配置文件如config.yaml或config.ini或通过命令行参数传入。使用Python内置的logging模块记录运行日志方便出错时回溯。import logging import yaml import random import time # 配置日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(bilibili_downloader.log), logging.StreamHandler() ] ) logger logging.getLogger(__name__) # 加载配置 with open(config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) HEADERS config[headers] REQUEST_DELAY config.get(request_delay, [1, 3]) # 请求延时范围 def safe_request(url, **kwargs): 带延时和重试的请求包装函数 headers kwargs.pop(headers, HEADERS) max_retries kwargs.pop(max_retries, 3) for i in range(max_retries): try: resp requests.get(url, headersheaders, **kwargs, timeout10) resp.raise_for_status() # 随机延时模拟人工操作 time.sleep(random.uniform(*REQUEST_DELAY)) return resp except requests.exceptions.RequestException as e: logger.warning(f请求失败 (尝试 {i1}/{max_retries}): {url}, 错误: {e}) if i max_retries - 1: time.sleep(2 ** i) # 指数退避 else: logger.error(f请求最终失败: {url}) raise return None5.2 常见问题与排查思路返回412状态码或-403错误码这通常是签名校验失败或Cookie无效。检查请求参数是否完整特别是w_rid和wts等签名参数是否最新。如果使用第三方库请更新到最新版本。对于需要登录的视频检查Cookie是否有效且未过期。返回404或视频信息为空视频可能已被UP主删除、设为私密或审核不通过。脚本中应做好判断跳过此类视频。下载的文件大小为0或很小可能是播放地址失效或请求头不正确特别是Referer和User-Agent。确保Referer正确设置为对应的B站视频页面URL。另外有些CDN地址可能有地域或时间限制。FFmpeg合并失败检查FFmpeg命令是否正确输入文件路径是否存在。有时下载的音频流编码不是FFmpeg默认支持的可能需要指定特定的解码器如-c:a copy尝试直接复制如果不行再换aac。查看FFmpeg的错误输出是定位问题的关键。IP被暂时限制访问这是请求频率过高的典型表现。立即停止脚本大幅增加请求间隔时间例如增加到5-10秒并考虑使用代理IP池。务必遵守爬虫礼仪控制访问速度。5.3 使用现成工具简化流程正如前文所述自己处理所有细节非常繁琐。对于大多数实际应用我建议you-get一个强大的命令行视频下载工具支持B站、油管等众多网站。只需一行命令you-get https://www.bilibili.com/video/BV1xx411c7mh。它内部处理了所有解析逻辑。bilibili-api-python一个Python库封装了B站的各种API包括获取视频信息、弹幕、评论等。用它来获取元数据和播放地址比自己逆向要稳定得多。使用you-get配合Python的subprocess模块可以极大简化我们的脚本import subprocess import os def download_with_you_get(bvid_list, save_dir./downloads): 使用you-get下载视频列表 for bvid in bvid_list: url fhttps://www.bilibili.com/video/{bvid} cmd [you-get, -o, save_dir, url] try: subprocess.run(cmd, checkTrue) print(f成功下载: {bvid}) except subprocess.CalledProcessError as e: print(f下载失败 {bvid}: {e}) # 仍然建议添加延时 time.sleep(random.uniform(2, 5))这种方法将复杂性转移给了成熟的工具我们的脚本只需要负责组织任务队列和流程控制更加稳健。6. 伦理、法律与最佳实践最后也是最重要的一部分我们必须讨论这样做的边界。尊重版权与用户协议B站上的视频是创作者和平台的财产。批量下载的内容必须用于个人学习、研究、欣赏等法律允许的合理使用范围。严禁用于商业用途、重新上传、盗用等侵犯版权的行为。请仔细阅读B站的用户协议。遵守robots.txt网站的robots.txt文件指明了哪些路径允许或禁止爬虫访问。虽然B站对部分API没有严格禁止但我们的访问行为应尽可能友好。控制访问频率这是最基本的“爬虫礼仪”。在请求间设置随机延时如2-5秒避免以极高的频率轰击服务器这既是对平台资源的保护也能让你的脚本跑得更久、更稳。标识自己在请求头中可以使用一个合理的User-Agent字符串例如包含你的项目名称和联系方式非强制以示坦诚。处理个人数据如果你的脚本涉及处理用户信息如评论、弹幕需格外注意隐私保护问题。技术本身是中立的但使用技术的人需要承担责任。通过这个项目我们学习的是网络请求、数据解析、异步编程和多媒体处理等通用的编程技能而不是鼓励侵犯版权的行为。请务必在法律和道德框架内合理使用这些知识。本文还有配套的精品资源点击获取