资讯动态

Python爬虫实战:视频站m3u8地址解析与分片下载全流程

发布时间:2026/10/14 20:06:44 来源:尧图企业网站定制
1. 从零拆解一个视频站爬取项目的完整思路视频类站点的数据抓取一直是爬虫练手和实战里绕不开的一类场景。相比抓新闻列表、商品价格视频站点的结构更复杂页面里既有列表页的封面和标题又有详情页的播放地址还有分集、清晰度、播放量这些动态字段。很多人第一次上手抓到的往往只是一堆封面图链接真正想要的视频文件地址却拿不到。这篇内容就围绕“爬取某看视频网视频”这个项目把我在实际做这类抓取时踩过的坑、用过的方案、以及能直接抄作业的代码结构完整地摊开讲一遍。先说清楚这个项目到底在做什么。所谓“某看视频网”指的是那种以影视剧、短剧、动漫、综艺为主的在线观看站点页面通常由首页推荐、分类列表、详情页、播放页几层组成。我们要做的事情是把这些页面里的结构化信息标题、封面、简介、播放量、更新集数以及视频播放地址通常是 m3u8 索引文件或 mp4 直链抓下来最终整理成一份可用的数据表或者进一步做成本地可播放的媒体文件。它解决的问题很实际手动一集一集点开复制链接效率太低而批量获取后无论是做数据分析、做个人媒体库归档还是研究视频分发结构都方便得多。适合谁来参考如果你已经会写基础的 requests 请求、看得懂 HTML 结构、知道 XPath 或 CSS 选择器怎么用那这篇内容能帮你把零散的知识串成一个完整项目。如果你是完全的新手也不用慌我会把每一步的意图和判断依据都讲清楚遇到需要补基础的地方会额外说明。整个项目不依赖什么高深框架核心就是请求、解析、存储三件事外加一层对动态加载和反爬的处理。在动手之前有一个前置认知必须先建立视频站点的数据分两类一类是静态渲染的直接躺在返回的 HTML 里另一类是动态加载的页面初始 HTML 里没有靠 JavaScript 请求接口后再渲染出来。这两类的抓取思路完全不同前者用 requests 解析库就能搞定后者要么找到背后的接口直接请求要么用浏览器自动化工具渲染后再取。判断方法很简单打开浏览器开发者工具看 Network 面板里 XHR/Fetch 请求有没有返回你要的数据。这个判断决定了后面整个技术选型所以务必先做。提示任何抓取行为都要控制频率、遵守目标站点的 robots 协议和服务条款仅用于个人学习研究不要对站点造成压力也不要传播抓取到的受版权保护的内容。这是做爬虫的基本底线。2. 技术选型与整体架构设计2.1 为什么选 requests 解析库而不是一上来就上框架很多人一提到爬虫就想到 Scrapy觉得不上框架就不专业。但在这个项目里我的建议是先用 requests lxml/BeautifulSoup 把单页逻辑跑通再考虑要不要上框架。原因很直接视频站的难点不在并发调度而在地址解析和反爬应对。Scrapy 的优势是分布式、去重、管道化但这些在你还没搞清楚播放地址怎么来的之前一点用都没有。先用轻量方案把核心链路打通等确认要抓几万条、需要断点续爬和并发控制时再迁移到框架这才是合理的演进路径。具体到库的选择请求层用 requests 足够它处理 session、cookie、header 都很顺手。解析层我倾向 lxml 的 XPath因为视频站的 DOM 结构往往嵌套很深XPath 在定位“第几个 div 下的第几个 a 标签”这类场景比 CSS 选择器更灵活。如果页面结构规整BeautifulSoup 的可读性更好看个人习惯。数据存储先用 CSV 或 SQLite量小的时候 CSV 直观量大了换 SQLite 或 MySQL避免一开始就引入数据库运维成本。2.2 动态加载的两种应对路线前面提到动态加载这里展开说。假设你在详情页看不到播放地址只看到一个播放器容器那地址大概率是通过接口异步拿的。这时候有两条路第一条是接口逆向。打开开发者工具切到 Network筛选 XHR刷新页面逐个看返回的 JSON。通常你会找到一个返回播放信息的接口里面带着 m3u8 地址或者加密后的参数。找到后直接用 requests 请求这个接口带上必要的 header 和参数即可。这条路效率最高速度快、资源占用低是首选。第二条是浏览器自动化。如果接口参数被加密得很复杂或者地址是运行时动态拼接的逆向成本太高那就用 Playwright 或 Selenium 驱动真实浏览器等页面渲染完再取 DOM 或拦截网络请求。这条路慢、吃资源但胜在稳定不用跟加密算法死磕。我的经验是能用接口就别用浏览器浏览器方案留作兜底。2.3 整体架构分层把整个项目拆成四层逻辑会清晰很多层级职责关键技术请求层发送 HTTP 请求、管理 session 与 headerrequests / httpx解析层从 HTML 或 JSON 中提取字段lxml / BeautifulSoup / json调度层管理待抓 URL 队列、控制并发与频率queue / asyncio / 线程池存储层落盘为 CSV、数据库或媒体文件csv / sqlite3 / ffmpeg这个分层的好处是每一层可以独立替换。比如请求层从 requests 换成 httpx 支持异步解析层从 XPath 换成正则都不影响其他层。做项目最怕的就是所有逻辑揉在一个函数里改一处崩一片。3. 核心细节解析与实操要点3.1 请求头的构造与伪装逻辑视频站对请求头的校验通常比普通站点严。最基础的 User-Agent 必须带否则很多站点直接返回 403。但光有 UA 不够Referer 往往也是必填项——因为视频地址通常做了防盗链服务器会检查请求来源是不是自家域名。构造 header 时Referer 一般填详情页或播放页的 URLUA 用一个常见的桌面浏览器标识。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://example-video-site.com/, Accept-Language: zh-CN,zh;q0.9, }这里有个细节Referer 不是随便填的要跟目标资源的实际来源匹配。比如抓 m3u8 文件时Referer 应该是播放页地址而不是首页。填错了照样 403。我踩过的坑就是一开始 Referer 填了首页结果列表页能抓视频文件全部拒绝排查了半天才发现是来源校验。3.2 列表页与详情页的字段拆解列表页通常返回一批卡片每个卡片包含标题、封面图、详情页链接、更新状态。解析时要注意相对链接转绝对链接否则后续请求会 404。用 urljoin 处理最稳妥from urllib.parse import urljoin detail_url urljoin(base_url, relative_href)详情页字段更丰富简介、主演、导演、地区、年份、播放量、集数列表。这里的关键是集数列表的解析因为它决定了后面要抓多少个播放地址。有些站点把集数放在一个 ul 里每集一个 li有些用 JavaScript 动态生成HTML 里只有空容器。遇到后者就得回到动态加载的判断流程。3.3 播放地址的三种常见形态抓视频最核心的就是拿到播放地址。实际遇到的形态主要有三种第一种是mp4 直链一个完整的 .mp4 URL直接下载即可。这种最简单但越来越少因为不利于防盗链和分片分发。第二种是m3u8 索引文件返回一个 .m3u8 文本里面是一系列 .ts 分片地址。这是目前最主流的形态。拿到 m3u8 后需要解析里面的分片列表逐个下载再合并。合并可以用 ffmpeg 一条命令搞定也可以手动拼接二进制。第三种是加密的 m3u8索引文件里带 #EXT-X-KEY 标签分片用 AES-128 加密。这种需要先下载密钥文件再对每个分片解密。密钥地址通常也在 m3u8 里跟着一起请求就行。注意m3u8 里的分片地址可能是相对路径解析时要基于 m3u8 文件自身的 URL 做 urljoin不能基于详情页 URL否则路径会错。3.4 频率控制与重试机制视频站对高频请求很敏感尤其是详情页和接口。我的做法是每个请求之间随机 sleep 0.5 到 1.5 秒用 random.uniform 生成避免固定间隔被识别出机器行为。重试机制用 requests 的 Retry 配合 HTTPAdapter对 429、500、502、503 这类状态码自动重试重试次数设 3 次退避因子设 0.5。from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry(total3, backoff_factor0.5, status_forcelist[429, 500, 502, 503]) session.mount(https://, HTTPAdapter(max_retriesretry))这套组合下来稳定性提升非常明显。之前不加重试跑几百条就断一次加上之后基本能连续跑完。4. 完整实操流程与关键环节实现4.1 环境准备与依赖安装先把环境搭起来。Python 3.8 以上都行依赖装这几个pip install requests lxml beautifulsoup4如果要处理 m3u8 合并还需要 ffmpeg这个不是 Python 包得单独装。Windows 下去官网下压缩包解压后把 bin 目录加进 PATHmacOS 用 brew install ffmpegLinux 用 apt 或 yum。装完在命令行敲 ffmpeg -version 能出版本号就说明好了。4.2 第一步抓取分类列表页先从一个分类页入手比如“最新更新”列表。目标是拿到这一页所有视频的详情页链接。import requests from lxml import etree from urllib.parse import urljoin BASE https://example-video-site.com LIST_URL f{BASE}/list/latest resp requests.get(LIST_URL, headersheaders, timeout10) resp.encoding resp.apparent_encoding html etree.HTML(resp.text) links html.xpath(//div[classvideo-card]/a/href) detail_urls [urljoin(BASE, link) for link in links] print(f本页拿到 {len(detail_urls)} 个详情链接)这里 resp.encoding 用 apparent_encoding 自动判断编码能避免中文乱码。XPath 里的 class 名要根据实际页面调整用开发者工具选中元素后右键 Copy XPath 可以快速拿到参考路径但别直接照抄那个路径往往带一堆索引页面一改就失效最好自己提炼成基于 class 的稳定路径。4.3 第二步解析详情页字段拿到详情链接后逐个请求并解析。def parse_detail(url): resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding html etree.HTML(resp.text) data { url: url, title: html.xpath(//h1/text())[0].strip(), cover: html.xpath(//div[classposter]/img/src)[0], desc: .join(html.xpath(//div[classintro]//text())).strip(), episodes: html.xpath(//ul[classep-list]/li/a/href), } return dataepisodes 拿到的是各集的相对链接后面要转绝对链接再请求。注意 desc 用 join 拼接所有文本节点因为简介经常被拆在多个标签里直接取 text() 只能拿到第一段。4.4 第三步定位播放地址接口这一步是整个项目的分水岭。打开某一集的播放页按 F12 进 Network筛选 XHR刷新。通常能看到一个类似 /api/play?vidxxx 的请求返回 JSON里面有 m3u8 地址。找到后用代码复现这个请求def get_play_url(episode_url): # 先从播放页 HTML 里提取 vid 等参数 resp requests.get(episode_url, headersheaders, timeout10) html etree.HTML(resp.text) vid html.xpath(//div[idplayer]/data-vid)[0] api f{BASE}/api/play params {vid: vid} api_resp requests.get(api, headersheaders, paramsparams, timeout10) return api_resp.json().get(data, {}).get(m3u8)如果接口返回的 m3u8 是加密的字段里可能还有 key 或 sign 参数一并带上即可。这一步的关键是参数完整性少一个参数接口就返回错误。对着 Network 里的请求参数逐个核对别凭感觉猜。4.5 第四步下载并合并 m3u8 分片拿到 m3u8 地址后先请求它拿到分片列表。def parse_m3u8(m3u8_url): resp requests.get(m3u8_url, headersheaders, timeout10) lines resp.text.splitlines() ts_urls [urljoin(m3u8_url, l) for l in lines if l and not l.startswith(#)] return ts_urls然后逐个下载分片写入同一个文件def download_ts(ts_urls, out_path): with open(out_path, wb) as f: for i, ts in enumerate(ts_urls): r requests.get(ts, headersheaders, timeout15) f.write(r.content) print(f分片 {i1}/{len(ts_urls)} 完成)下载完的 .ts 文件其实已经是可播放的但为了兼容性更好用 ffmpeg 转成 mp4ffmpeg -i output.ts -c copy output.mp4-c copy 表示不重新编码直接复制流速度快、无损。如果遇到音画不同步去掉 -c copy 让它重新封装。4.6 第五步数据落盘与去重详情数据存 CSV用追加模式写入避免每次覆盖import csv, os def save_row(row, pathvideos.csv): exists os.path.exists(path) with open(path, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesrow.keys()) if not exists: writer.writeheader() writer.writerow(row)encoding 用 utf-8-sig这样 Excel 打开不会乱码。去重的话维护一个已抓 URL 的集合或者抓之前先读一遍 CSV 把已有 URL 装进 set新链接判断不在 set 里再抓。5. 常见问题与排查技巧实录5.1 请求返回 403 或 404 怎么排查403 基本是请求头问题按这个顺序查UA 有没有带、Referer 对不对、Cookie 需不需要。有些站点会校验 Cookie 里的某个 token那就得先请求首页拿到 Set-Cookie再用同一个 session 请求后续页面。404 通常是链接拼接错了检查 urljoin 的 base 是不是漏了路径层级。5.2 抓到的 m3u8 无法播放先确认 m3u8 内容是不是完整的有时候接口返回的是被截断的或者带错误提示的文本。再看分片地址能不能单独请求成功如果分片 403多半是 Referer 问题。如果 m3u8 带 #EXT-X-KEY确认密钥有没有正确下载和解密。还有一种情况是 m3u8 里嵌套了子 m3u8多清晰度需要再解析一层。5.3 分片下载速度慢或中断分片数量多的时候串行下载很慢。可以上线程池开 5 到 10 个并发但别开太大容易被限流。中断的话记录已下载的分片索引下次从断点继续避免重头再来。问题现象可能原因解决方向403 Forbidden请求头缺失或错误补 UA、Referer、Cookie404 Not Found链接拼接错误检查 urljoin 基准 URLm3u8 无法播放分片地址错误或加密未处理核对 Referer、处理 KEY下载中断网络波动或限流加重试、降并发、断点续传中文乱码编码判断错误用 apparent_encoding5.4 页面结构变了怎么办视频站改版是常态今天能用的 XPath 明天可能就失效。应对办法是把选择器集中管理写成一个配置字典改版时只改配置不改逻辑。另外解析时加容错某个字段取不到不要让整个程序崩用 try 包起来给个默认值先把能抓的抓下来。def safe_xpath(html, path, default): try: result html.xpath(path) return result[0].strip() if result else default except Exception: return default这个小函数在实际项目里救了我很多次页面小改版时程序照样能跑只是个别字段为空不至于全军覆没。5.5 实操心得与避坑清单最后分享几条实打实的经验。第一先手动跑通一集再批量别一上来就写循环抓几百个链路没通之前批量只会浪费时间和 IP。第二日志要打全每个请求的 URL、状态码、耗时都记下来出问题时能快速定位是哪一步断的。第三存储要幂等重复跑不会产生重复数据靠 URL 去重最可靠。第四别把并发开太大视频站带宽敏感你并发一高对方服务器压力大你自己也容易被封稳扎稳打比追求速度重要。第五定期备份已抓数据爬到一半程序崩了前面的成果不能丢。这套流程我在几个不同类型的视频站上都验证过核心逻辑是通用的变的只是选择器和接口参数。把请求、解析、下载、存储这四块拆清楚遇到新站点无非是重新对一遍结构。真正花时间的从来不是写代码而是搞清楚数据到底藏在哪、怎么拿。

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

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

免费获取报价 →
↑