资讯动态

Python网页视频下载实战:从m3u8解析到yt-dlp封装

发布时间:2026/10/20 19:11:59 来源:尧图企业网站定制
简介四个Python脚本构成的视频下载工具包压缩包仅2KB体积小巧、源码开源面向想解决网页视频无法直接保存问题的Python初学者、爬虫爱好者与前端开发者也适用于在线课程、短视频、直播回放等常见下载场景。脚本分工清晰有负责动态网页请求与源码获取的模块有解析页面并定位真实视频地址的模块还有专门处理m3u8流媒体索引及分段下载的模块基本覆盖主流网页视频抓取流程代码注释详细Windows、macOS、Linux环境均可直接运行调试。资源包已有352人学习下载从零基础语法入门到实战抓取均可一步步对照验证。通过该工具包读者可快速掌握Python基础语法、requests库请求网页、HTML与CSS结构分析等技能再结合多线程下载与断点续传的优化思路即可举一反三为独立编写针对不同网站的下载脚本打下扎实基础。1. Python 网页视频下载先搞清楚你面对的是哪种视频流做爬虫和自动化的人迟早会碰到一个需求网页上有一段视频既没有下载按钮右键菜单也被禁用但你就是想把它保存下来。用 Python 写一个视频下载脚本本质上不是在“破解”什么而是把浏览器播放视频时发出的网络请求复现一遍——能播放的视频就一定能下载这个结论对绝大多数站点都成立。唯一的例外是带 DRM 加密的内容那个属于商业保护范畴本文不展开也不建议碰。这篇文章会把完整链路拆开从浏览器 F12 里找到视频真实地址到用 Python 脚本分段抓取流媒体再到用 yt-dlp 这类现成工具做工程化封装最后讲清楚限速、断点、批量下载和最常见的翻车点。适合三类人想批量保存课程视频的从业者、负责给项目做视频素材归档的开发、以及好奇“视频到底是怎么从网页流到本地”的爬虫入门者。跟着走完你不只能下视频还能知道下不下来的时候该看哪个报错。2. 从浏览器 F12 到 Python 脚本抓出视频真实地址的底层逻辑2.1 为什么浏览器能播脚本却下不了网页视频看起来是“嵌在页面里的”实际的播放链路通常是HTML 里的 video 标签指向一个 URL浏览器拿到这个 URL 后发起 HTTP 请求服务器返回视频文件或流媒体索引文件浏览器再边下边播。你手动访问这个 URL 时大概率会碰到两类拦截一类是服务器校验请求头Referer 和 User-Agent换一个客户端就拒绝响应另一类是视频被切成几十上百个分片URL 暴露的只是索引文件真正的数据块散落在不同地址上。在动手写脚本之前我一般会先做一次“手工侦察”打开某视频站的播放页按 F12 进入开发者工具切到 Network 面板筛选 media 类型再点播放。观察新出现的请求重点看两项一是响应的 Content-Type如果是video/mp4说明是直链文件直接下载如果是application/vnd.apple.mpegurl或application/x-mpegurl说明是 HLS 流媒体拿到的是 m3u8 索引如果看到video/webm但是分片请求URL 带range或segment字样则是流式分片。这一步确定了下游脚本该走哪条路。2.2 m3u8 流媒体解析分段下载的本质HLS 是网页视频最常见的分发形式。服务器把视频切成 3 到 10 秒不等的 TS 分片生成一个 m3u8 索引文件播放器读索引、按顺序拉分片、拼接播放。用 Python 下载这种视频思路就是先拿索引文件解析出所有分片 URL然后逐个下载最后拼成一个完整文件。下面的代码演示了最核心的解析逻辑import requests def parse_m3u8(url, headersNone): 获取 m3u8 索引返回分片地址列表 headers headers or { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } resp requests.get(url, headersheaders, timeout15) resp.raise_for_status() lines resp.text.strip().splitlines() base_url url.rsplit(/, 1)[0] # 用于拼接相对路径的分片地址 segments [] for line in lines: line line.strip() if line.startswith(#): # 跳过 m3u8 的标签行 continue if line.startswith(http): # 绝对地址直接使用 segments.append(line) else: # 相对地址按索引所在目录拼接 segments.append(f{base_url}/{line}) return segments这段脚本的逻辑很直接requests.get拉取 m3u8 文件内容splitlines按行拆分跳过所有#开头的标签比如#EXTM3U、#EXTINF剩下的就是分片地址。相对路径的分片需要拿索引文件的 URL 做一次目录拼接因为大多数服务器把索引和分片放在同一路径下但前缀是域名加目录不能直接用。参数上headers里至少带一个主流浏览器的 User-Agent否则不少站点会直接拒绝非浏览器客户端的请求timeout15是防止某个分片地址响应缓慢时脚本卡死。拿到分片列表后第二步是逐个下载并拼接。注意分片多的时候不要用单线程顺序下载——几十个分片每个 200KB顺序下可能要等一两分钟用concurrent.futures开 8 到 16 个线程可以压缩到十秒内但并发数不要开太大否则容易触发服务器的限流策略返回 429 或 403 就把整个任务废了。2.3 HTTP Range 直链下载断点续传的底层实现另一类更简单的场景是直链视频单个 MP4 文件URL 直接指向资源。但这不意味着没有坑——很多站点对直链也做防盗链处理校验 Referer 和 Cookie。简单场景下Python 的requests配合streamTrue就能下载大文件但如果网络中断整个文件就得重新下。更实用的方案是支持 Range 断点续传import os import requests def download_with_resume(url, filepath, headersNone, chunk_size8192): 支持断点续传的文件下载 headers headers or {} resume_pos os.path.getsize(filepath) if os.path.exists(filepath) else 0 if resume_pos 0: headers[Range] fbytes{resume_pos}- # 从已有进度继续 resp requests.get(url, headersheaders, streamTrue, timeout30) if resp.status_code 206: # 服务器支持 Range mode ab elif resp.status_code 200: # 服务器忽略 Range重新下载 mode wb resume_pos 0 else: resp.raise_for_status() with open(filepath, mode) as f: for chunk in resp.iter_content(chunk_sizechunk_size): if chunk: f.write(chunk)这里的关键是 HTTP 的 Range 请求头bytes32505856-表示“从第 32505856 字节开始传输”。服务器如果支持断点续传会返回状态码 206正文只包含剩余部分如果服务器不支持或校验不通过返回 200就降级为全量重新下载。chunk_size8192指的是每次从网络读取 8KB 写入磁盘这个值不宜太大或太小——太大会占用内存太小会频繁调用写入实测 8KB 到 256KB 之间对大多数机械硬盘和 SSD 都比较友好。这个代码逻辑在后面的 yt-dlp 封装里其实用不到但理解 Range 原理你才知道为什么那些下载工具能断点续传、能边下边播。3. 一键下载的工程化封装用 yt-dlp 当“指挥中心”3.1 为什么选 yt-dlp 而不是自己写解析器从零写一个视频下载脚本在单个站点上是可行的但放到真实场景里有几个绕不开的问题站点前端改版导致解析逻辑失效、不同视频清晰度对应不同的分片地址规则、下载完还需要音视频合并。这些问题不是不能解决而是维护成本极高——你花两周写好的解析器可能一个月后站点改动一次就用不了了。yt-dlp 是目前社区维护最活跃的命令行视频下载工具支持几千个站点内置了格式选择、限速、断点、合并、字幕、批量任务等能力。它不是一个“破解工具”而是把各家视频站的播放协议和反爬策略做成了可插拔的解析插件。用它的正确姿态是把它当成下载引擎你自己只需要管“下哪个 URL、存成什么格式、做不做后处理”。对于内部素材收集和课程下载这是投入产出比最高的方案。3.2 基础调用与格式选择安装和基础调用非常简单一行 pip 命令即可pip install -U yt-dlp然后直接指定 URL 下载。这里拿某视频站的单集页面举例不写具体站点原理通用yt-dlp https://example.com/watch/12345默认行为是下载最高画质但 yt-dlp 的“最高画质”是按分辨率判断的不一定是最佳选择。实际使用中我基本都显式指定格式参数。下面的命令是更实用的写法yt-dlp -f bv*[height1080]ba --merge-output-format mp4 \ -o %(title)s.%(ext)s --limit-rate 5M https://example.com/watch/12345拆开解释这几个参数-f bv*[height1080]ba表示优先选 1080p 及以下的视频流bv 是 best video 的缩写*允许有多个候选加上一条最佳音频流ba二者用加号合并--merge-output-format mp4告诉工具合并产物用 MP4 封装-o %(title)s.%(ext)s设定保存文件名为视频标题加扩展名--limit-rate 5M把下载速度限制在每秒 5MB避免占满带宽影响其他业务。参数里容易踩坑的地方是bv*和bv的区别——bv只选一个最佳视频流bv*允许选出一条视频之后再用算法挑选能匹配的最佳组合后者在音视频分离的站点上是必需项。3.3 合并与转封装FFmpeg 搭档很多视频站尤其是高清内容会把视频轨和音频轨分开传输视频是无声音的高码率流音频是单独的 m4a 或 webm 文件。yt-dlp 把两者都下载到本地后需要 FFmpeg 来完成合并。如果没有安装 FFmpegyt-dlp 会报错提示缺少二进制文件但不会告诉你具体怎么装。在 Windows 上我一般从 FFmpeg 官方构建站点下载静态编译版解压后把bin目录加入系统 PATHLinux 上直接apt install ffmpeg或yum install ffmpeg即可。安装完成后可以用一个命令验证ffmpeg -version看到版本输出就说明可用了。yt-dlp 会自动探测系统 PATH 中的 ffmpeg不需要额外配置。但如果你把 ffmpeg 放在非标准路径比如项目目录下的tools/里就需要显式指定yt-dlp --ffmpeg-location ./tools/ffmpeg/bin/ffmpeg.exe ...--ffmpeg-location指向的是ffmpeg.exe所在目录不是可执行文件本身经常有人在这里写错。另外注意--merge-output-format mp4并不是万能的——如果视频轨是 AV1 编码合并成 MP4 没问题但兼容性取决于播放器如果遇到 HDR 内容合并后的 MP4 在部分设备上颜色会发灰那就需要保留原始封装格式用默认的mkv更稳妥。4. 参数调优与批量落地把下载做成自动化流水线4.1 常见下载参数矩阵从“能下载”到“下得好用”中间隔着几个参数。我把日常最常用的参数组合整理成一张表方便直接抄目标参数写法说明限制下载速度--limit-rate 2M带宽占用控制在 2MB/s跑批任务不卡办公网断点续传-c或--continue默认开启分片下载中断后从断点继续失败重试--retries 5单个分片失败重试 5 次超过则跳过跳过已下载--download-archive archive.txt记录已下载的 URL下次自动跳过只下音频-f ba --extract-audio --audio-format mp3适合播客、音频课程场景下载字幕--write-subs --sub-langs zh-Hans,en先写中英字幕语言代码按 ISO 639-1 填自定义文件名-o %(channel)s/%(playlist_index)s - %(title)s.%(ext)s按频道建目录序号加标题命名两张表之外有三个容易忽略的点。第一--download-archive是跑批任务的后悔药——它会持续追加已下载 URL 到文本文件任务中断后重新执行能自动跳过已完成的条目不会重复下。第二--limit-rate的单位是字节B/s不是比特bps5M等于 5MB/s家用宽带上行基本不会被占满下行则会接近占满。第三--extract-audio --audio-format mp3不是转码而是提取如果源视频是 AAC 音频工具会先解封装再编码为 MP3耗时会和视频时长成正比批量转音频时要有心理预期。4.2 批量下载与队列管理单个 URL 下完下一条这是最笨的用法。实际收集整套课程或某个系列时URL 可能有几十上百个需要批量喂给 yt-dlp。最省事的做法是把所有 URL 写进一个文本文件每行一条然后用--batch-file参数导入yt-dlp -f bv*[height720]ba \ --batch-file urls.txt \ -o %(playlist_index)s.%(title)s.%(ext)s \ --download-archive archive.txt--batch-file让工具顺序处理文件里的每一条 URL其中任意一条失败不会中断整体任务配合--download-archive做去重第二次运行同一份 urls.txt 时已完成的 URL 会被自动跳过。为了跑批不翻车我还会加两个参数--socket-timeout 30防止某个站点响应慢导致连接卡死和--retries 3控制单条 URL 的重试次数超过次数直接跳到下一条。还有一个不常用但很实用的参数是--playlist-start和--playlist-end——当某个“合集”页面里包含 100 集但你只需要第 20 到 40 集时这两个参数能精确切片不用手动整理 URLyt-dlp --playlist-start 20 --playlist-end 40 https://example.com/playlist/xxx别小看这个切片能力。某次我帮一个同事处理课程归档合集里有 200 集他只需要最近更新的 30 集用 URL 列表整理要花掉半天用切片参数改两个数字就搞定了。4.3 命名与归档避免重复下载批量任务最大的隐性成本不是带宽而是文件命名混乱导致的“以为没下过”。yt-dlp 的--output模板语法足够应付大部分命名需求常用的变量有%(title)s视频标题、%(id)s视频 ID、%(uploader)s上传者、%(playlist_index)s在合集里的序号、%(ext)s封装格式。组合方式决定目录结构yt-dlp -o %(uploader)s/%(playlist_index)s_%(title)s.%(ext)s ...这段模板会把文件保存到以“上传者”命名的子目录下文件名是“序号_标题.扩展名”。用序号开头的好处是文件管理器里排序稳定不会因为标题首字符不同而乱序带上传者是为了多套课程并存时不会被同名标题覆盖。模板变量里不要包含/或\等路径分隔符字符yt-dlp 虽然会自动替换但替换后的可读性很差。如果你需要更精细的归档比如按年份、按项目分类可以嵌套目录模板——--output %(uploader)s/%(upload_date%Y-%m-%d)s/%(title)s.%(ext)s。这里的%(upload_date%Y-%m-%d)s是模板的格式化语法把原始日期字段如20240315重排成2024-03-15的可读格式。注意老版本 yt-dlp 对这个语法的支持不完整跑之前先yt-dlp --version确认版本是 2024 年之后的否则会报模板解析错误。5. 避坑指南从反爬到 DRM 的四个关键排查点5.1 现象下载报 403 Forbidden用 yt-dlp 或自写脚本下载时服务器返回 403最常见的是两种情况第一视频站点要求请求必须携带 Referer而你的脚本没有带第二站点对请求频率有限制短时间大量请求触发了风控。处理方式自写脚本时从浏览器复制完整的请求头包括 User-Agent、Referer、Cookie 等填入代码用 yt-dlp 时执行前先确认是否可以访问该页面如果页面在浏览器能打开但脚本 403多半是缺少 Cookie可以用--cookies-from-browser chrome直接把浏览器已登录的 Cookie 导出给工具使用。某开发者遇到过一个奇怪案例同一站点同一 URL浏览器打开正常但命令行下载 403排查到最后发现是请求头里缺少Accept-Language——某些防爬系统会校验这个头补上后问题消失。像这类“玄学”问题第一反应是开着 F12 对比浏览器请求和脚本请求的完整请求头字段通常能找到突破口。5.2 现象只下到音频没有视频画面使用-f bv*ba时某个视频下载后打开只有声音、画面黑屏。原因有两种一是该视频流的视频轨和音频轨都下载成功了但合并失败FFmpeg 合并时选了错误的编码参数二是合并后封装格式没有正确匹配。先看 FFmpeg 合并时的日志如果提示类似 “Stream mapping” 的警告说明音视频流在时间戳上不对齐。常见解法是改用--merge-output-format mkv强制走 MKV 封装因为 MKV 对时间戳不齐的容忍度更高。如果合并日志正常但播放黑屏用 FFprobe 检查输出文件的编码信息——如果视频轨是 AV1 而你的播放器不支持画面自然黑掉。这种情况要么换播放器要么用--format-sort av01参数让 yt-dlp 优先排除 AV1 编码选择 H.264 或 H.265。5.3 现象批量下载中途突然全部超时批量任务跑了一半日志里开始连续出现timed out或ReadTimeout且之前能正常下载的站点也一并失败。这不是单个请求的问题大概率是请求频率过高触发了对方的限流机制。某些站点对单个 IP 的并发连接数有硬限制超过阈值后所有新请求都会被挂起或拒绝。解法分三步第一步调小并发yt-dlp 默认并发请求数不高但如果用的是自写脚本要把线程数从 16 降到 4第二步加长超时时间--socket-timeout 60给慢请求更多时间第三步是降低限速--limit-rate 1M反而比限速 5M 更不容易触发风控。如果还是超时加一个随机延迟机制自写脚本在每次请求前time.sleep(random.uniform(1, 3))把请求间隔拉大到每秒最多一个通常能恢复到稳定下载。这是跑批任务最快的止损手段。5.4 现象页面显示视频加密下载后无法解析这是最让人头疼的一类页面能播放但抓不到任何 m3u8 或直链或者抓到了分片但下载的解码失败。这种视频通常是经过自定义加密的——分片本身被没意义化处理真正的解密逻辑在页面 JavaScript 里解密密钥也硬编码在部分 JS 文件中。我的建议是不要硬刚加密逻辑。解密整个 JS 代码链路的调试成本通常在十几个小时以上而且一旦站点更新前端代码解密脚本就失效了。可操作的做法有两个一是找该站点是否提供了清晰度较低的“免密流”部分站点为了兼容老设备会保留非加密的低清流二是用屏幕录制的方式保存虽然码率有损失但至少内容拿到了。前者适用于批量场景后者适用于单集场景。从长期维护的角度看下载这种加密站点的时间成本几乎总是高于收益偶尔处理一次可以反复处理就不值得了。某项目组的同事曾花了一周时间逆向某个视频站的加密逻辑结果第二周对方前端一改整个脚本作废白白消耗了精力——从那以后遇到加密就先用排除法判断是否值得投入。5.5 现象下载完成但播放时没有声音分片下载的视频在本地用播放器打开后出现音画不同步或只有画面没有声音。通常发生在两个场景一是多音轨视频yt-dlp 默认选了第一个音轨但该音轨在合并时没有被正确封装二是音视频流来自不同编码器时间基准不同步。排查时分两步走先用 FFprobe 检查文件的流信息ffprobe -show_streams output.mp4 | grep -E codec_name|channels看视频轨和音频轨的codec_name是否匹配比如视频是 H.264音频是 AAC这是正常组合如果音频轨信息缺失很可能是合并时音频流丢失。此时重新下载并指定音频语言优先级yt-dlp -f bv*ba/b --audio-multi-lang --sub-langs zh-Hans,en ...ba/b表示优先最佳音频若没有则降级到最佳任意格式。需要明确的是真正解决音画不同步的底层手段是转封装——用 FFmpeg 把音频流的时间基准重置为视频流的基准但这会消耗大量 CPU 时间对批量场景不划算所以重点是预防而不是事后修复。以我的经验下载后立刻用播放器抽检前 30 秒的音画同步比等全量任务跑完再检查要节省大量返工时间——最好养成这个习惯。6. 把下载流程固化成自己的 CLI 工具箱三个能直接抄的落地习惯6.1 配置化用一个脚本管理所有下载参数不用每次都在命令行敲漫长参数把常用配置写进一个 Python 脚本通过读取 JSON 配置文件来切换“课程下载”“播客提取”“单集高清”等场景。这里给出一个极简实现思路import json import subprocess def build_cmd(config: dict, url: str) - list: cmd [yt-dlp] if config.get(format): cmd [-f, config[format]] if config.get(output_template): cmd [-o, config[output_template]] if config.get(rate_limit): cmd [--limit-rate, config[rate_limit]] cmd.append(url) return cmd with open(config.json, r, encodingutf-8) as f: cfg json.load(f) url input(请输入视频 URL) subprocess.run(build_cmd(cfg, url))这个脚本的逻辑是把 yt-dlp 的参数拆成配置项build_cmd函数按字典内容拼接命令行列表。config.json里只放常用参数比如{format: bv*[height1080]ba, output_template: %(title)s.%(ext)s, rate_limit: 5M}。好处是参数集中在配置文件中换台电脑不用重新记忆命令语法同时也避免日志里出现超长命令行导致 shell 转义出问题。6.2 日志与断点让下载任务跑过夜不翻车跑批任务最怕的是“半夜 3 点脚本崩了第二天起床发现白跑一夜”。我强制自己在所有下载命令里加两个参数--newline让日志实时换行输出--verbose打印更详细的调试信息。但比这更重要的是把输出重定向到日志文件yt-dlp --newline --verbose --batch-file urls.txt -o %(title)s.%(ext)s download.log 21是追加写日志21把标准错误也并进同一文件。第二天检查日志时主要看两处日志中出现的ERROR和WARNING行数以及最后一次成功的下载条目时间。如果任务在凌晨 2 点中断日志末尾会留最后一条 URL 的处理痕迹先把断点文件再跑一遍就能从已完成的位置继续。这样做的核心价值是把“不确定跑没跑完”变成“看日志就能确定”。6.3 验证输出下载完成后自动校验文件完整性批量下载完成后还需要一套“交货标准”。文件不是下完就完事的——未合并成功、命名冲突、编码异常都可能让某个文件不可用。我习惯下载后跑一遍批量校验脚本for f in *.mp4; do if ffprobe -v error -show_entries formatduration -of defaultnoprint_wrappers1:nokey1 $f /dev/null 21; then echo OK: $f else echo FAIL: $f fi done这个循环的逻辑是逐个检查当前目录下所有 MP4 文件ffprobe读取文件时长能读出说明文件结构完整读不出说明文件已经损坏。配合前面所有流程这就是一套完整的“从网页视频 URL 到本地可用文件”的流水线先用 yt-dlp 拉流再用模板管理命名跑批时挂好日志最后批量校验完整性。每当我拿到一个新的下载需求都会先问三个问题这个站点的流是直链还是 HLS、需不需要带 Cookie、能不能跑批再用这套固定流程去执行。从最早的 F12 手工翻包到现在一套脚本走完规避问题的时间远大于解决问题的时间。希望这些拆解过的流程和踩过的坑能帮到你让你在做视频归档时少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑