1. 先搞清楚视频文件是怎么在网页里藏的
很多初学者上手爬虫,第一反应就是打开网页、找到视频、右键另存为。但真正进入抓视频这个场景后,你会发现浏览器里看到的视频,和HTML源码里看到的HTML标签,完全不是一回事。我最早写这类脚本时也犯过傻:以为视频地址就躺在<video>标签的src属性里,结果一抓返回一堆看不懂的JS代码,视频却一个都没有。
要抓视频,第一步不是写代码,而是先搞清楚视频网站的分发方式。现在的网页视频基本分成三类形态:
- 静态直链:视频文件存放在一个固定的URL上,
mp4、webm等格式直接打开就能播放,适合做教学视频、个人博客、部分小型视频站。这种最简单,requests拿到页面源码,用正则或xpath把src扒出来就能下载。 - HLS流媒体:主流视频平台基本都走这条路,视频被切成无数个小的
ts分片,播放器通过一个叫m3u8的索引文件按顺序拉取。你在浏览器里看到的画面,其实是成百上千个小片段拼接出来的。 - 播放器数据接口:页面本身只是一个播放器壳子,真正的视频地址由前端JS向后端接口发起请求后动态拿到,通常还得带上签名、时间戳、防盗链参数。这种对初级爬虫来说难度最高,但也不是完全没法处理,后面我会细讲。
对初级爬虫来说,最实用的组合拳就是requests加xpath。requests负责把网页源码原样取回来,xpath负责在HTML结构里精准提取目标数据。这一套搭配已经能覆盖大量静态网页和部分动态页面的抓取需求,而且在学习过程中能把网页结构、节点层级、属性选择这些基本功一起练扎实。很多培训班一上来就教Selenium和浏览器自动化,看起来"高级",但遇到反爬的第一道门槛就懵了,原因就是连页面源码都没吃透。
2. 第一步实战:用requests加xpath抓出一个网页视频直链
2.1 搭建最基础的抓取环境
我的建议是直接用Python 3.10以上的版本,装好requests和lxml这两个库就够了。lxml是xpath解析的后端引擎,解析速度比标准库的html.parser快不少,而且对残缺HTML的容错性更强。
pip install requests lxml这里有个很容易被忽略的点:写爬虫时一定要设置User-Agent。很多网站的服务器会检查请求头,默认的Python-requests/2.28这种标识大概率会被直接拒绝访问。我在初学阶段就被403错误卡了一整天,后来才发现只是没伪装UA。
import requests 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.com/", } resp = requests.get("https://example.com/video-page", headers=headers, timeout=10) resp.encoding = resp.apparent_encoding # 防止中文乱码 html = resp.text2.2 xpath解析与text()函数的两个易错点
拿到HTML源码后,下一步就是用lxml解析。这里我重点说一下text()这个函数,因为它在初学阶段非常容易踩坑,而且搜索热词里反复出现,说明大家普遍在这个点上卡过。
text()的作用是提取某个节点下的直接文本内容。注意是直接文本,不会递归获取子标签里的文本。如果HTML结构是这样的:
<div class="title"> 我是外层文本 <span>我是子标签文本</span> </div>用//div[@class="title"]/text()拿到的只是"我是外层文本",而span里的内容要用//div[@class="title"]/span/text()才能取到。很多初学爬虫的人在解析标题、描述、作者信息时拿到的列表是空的,或者内容缺胳膊少腿,多半就是这个原因。
视频直链的抓取逻辑则要看具体的页面结构。如果是下面这种:
<video controls> <source src="https://media.example.com/videos/demo.mp4" type="video/mp4"> </video>直接写:
from lxml import etree tree = etree.HTML(html) src = tree.xpath('//video/source/@src') print(src)如果页面里不是video标签而是普通的超链接,就写//a/@href,思路完全一样。xpath的本质就是"按路径找节点、按属性取内容",一旦想通这一点,绝大多数静态页面的数据提取都不在话下。
2.3 文件的稳定保存:别小看这一步
地址拿到后就直接requests.get()下载,这也是新手经常翻车的地方。视频文件动不动几十MB甚至几百MB,用resp.content一次性读进内存,然后再一次性写入文件,机器内存小一点或者网络稍有波动,程序就直接崩了。
正确的做法是开启流式响应,一边下载一边写入磁盘:
download_headers = { "User-Agent": headers["User-Agent"], "Referer": "https://example.com/", } with requests.get(video_url, headers=download_headers, stream=True, timeout=30) as r: r.raise_for_status() with open("demo_video.mp4", "wb") as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk)这里要特别提醒两个坑:
- Referer不能丢。很多视频服务器做了防盗链,只允许特定来源的请求访问资源。如果下载请求里没有带上页面地址作为Referer,服务器会直接返回403。哪怕你在浏览器里能正常播放,代码里也是下载失败的。
- 下载路径里尽量只保留英文和数字。中文文件名在部分系统上没问题,但如果配合一些代理工具或脚本框架使用,很容易出现乱码或找不到文件的情况。稳妥做法是先保存成
demo_video.mp4,下载完成后再统一重命名。
3. 进阶玩法:m3u8流媒体视频的抓取与分片合并
3.1 理解m3u8的切片机制
当你打开一个主流视频网站的播放页,查看开发者工具里的网络请求,十有八九能看到一个index.m3u8之类的文件请求。这个文件本身不是视频,而是一个"播放清单",里面记录的是一串ts分片文件的地址,以及它们的时间长度、顺序等信息。浏览器播放器拿到这个清单后,会按顺序把一个个小分片拉下来,无缝拼接成完整画面。
为什么主流平台都采用这种方案?最核心的考虑是防盗链与流畅性。切成小分片后,服务器可以动态生成短时效的鉴权地址,单个分片即使泄露也只是一两秒的画面。同时,播放过程中可以根据网络状况灵活切换不同的码率和分片长度,实现自适应清晰度。对爬虫而言,抓m3u8视频就变成了"下载清单、提取分片地址、逐个下载、最后合并"四步操作。
3.2 抓取m3u8文件并提取分片地址
第一步还是拿播放页源码,但这次的目标不是找视频地址,而是找m3u8链接。多数情况下,播放页源码里能直接看到https://xxx.com/play/o7w3abcd/index.m3u8这类地址,用xpath加正则一起处理就能提取出来。如果在页面源码里找不到,就需要到开发者工具的Network面板里过滤m3u8关键字,手动把这个地址复制下来。需要注意的是,有些m3u8是加密的,会配套一个key文件,但那属于另一层知识,初学阶段先跳过加密场景。
拿到m3u8地址后,接着请求它,就能看到类似下面的内容:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:11 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, segment_00001.ts #EXTINF:10.0, segment_00002.ts真正有用的就是#EXTINF下方的ts文件名。如果ts地址是相对路径,还要用urljoin拼出完整地址:
from urllib.parse import urljoin base_url = "https://media.example.com/hls/o7w3abcd/" base_m3u8_url = "https://media.example.com/hls/o7w3abcd/index.m3u8" resp = requests.get(base_m3u8_url, headers=headers) ts_list = [] for line in resp.text.splitlines(): line = line.strip() if line and not line.startswith("#"): ts_url = urljoin(base_url, line) ts_list.append(ts_url)3.3 分片下载与合并的完整代码
下载分片时,既要控制速度,也要考虑稳定性。控制速度是为了不给对方服务器太大压力,这也是爬虫从业者应该有的基本素养。用一个小循环加延时就能实现:
import time import os os.makedirs("ts_files", exist_ok=True) for idx, ts_url in enumerate(ts_list): try: ts_resp = requests.get(ts_url, headers=download_headers, timeout=20) if ts_resp.status_code == 200: with open(f"ts_files/{idx:05d}.ts", "wb") as f: f.write(ts_resp.content) print(f"已完成 {idx + 1}/{len(ts_list)}") else: print(f"分片 {idx} 下载失败,状态码: {ts_resp.status_code}") except Exception as e: print(f"分片 {idx} 出现异常: {e}") time.sleep(0.5)分片全部下载完成后,合并就非常简单了。ts分片本质上就是MPEG传输流,直接按顺序以二进制方式拼接即可:
cat ts_files/*.ts > merged_video.ts在Windows上则用copy /b命令:
copy /b ts_files\*.ts merged_video.ts如果你还想把这个ts文件转换成更通用的mp4格式,可以用FFmpeg处理:
ffmpeg -i merged_video.ts -c copy merged_video.mp43.4 请求头里的Referer和Origin如何设置
m3u8场景下,请求头的设置比普通视频直链更讲究。一些平台会同时校验Referer和Origin两个字段。Origin标识的是跨域请求来源,如果漏掉,服务器会认为你是非浏览器环境发来的请求。
一个实用的小技巧是:打开浏览器开发者工具的Network面板,找到任意一个ts分片请求,右键将请求头复制为cURL格式,再转成Python字典。这样拿到的请求头是服务器真正认可的一组参数,几乎不会出现403问题。
这里也要提醒一下:有些平台的分片地址带有短时效的签名,过期时间可能在几分钟到几小时之间。所以整个抓取流程要尽快完成,不能今天抓到地址明天再来下载,否则签名失效后所有请求都会被拒。
4. 初学爬虫必知的防爬识别与合规红线
4.1 robots协议和抓取边界
搜索热词里出现了"前端安全技术操作""如何防止爬虫"这些,说明很多人爬到一半之后,开始思考站在网站维护者的角度怎么防御。这个视角切换非常值得鼓励,因为写爬虫的人和防爬的人本质上是攻防双方,理解防守方的思路才能写出更高质量的抓取代码。
先说最基本的合规问题。每个正规网站都会提供robots.txt文件,访问https://example.com/robots.txt就能看到站点对爬虫的允许与禁止规则。虽然它是一个约定俗成的协议而非法律强制,但尊重它是这个行业的基本礼仪。初级爬虫学习阶段,建议优先选择公开开放的数据源,比如公开的开放数据接口、个人博客、以及明确允许抓取的教育类网站。
4.2 识别常见反爬措施
我在实际测试中总结过一套高频反爬手段,整理成表格方便对照:
| 反爬手段 | 典型表现 | 初级应对思路 |
|---|---|---|
| UA检测 | 返回403或要求验证 | 设置浏览器UA和完整请求头 |
| Referer防盗链 | 资源下载403 | 请求头里带上页面地址 |
| IP频率限制 | 请求几次后出现验证码 | 降低请求频率,增加随机延时 |
| 动态渲染 | 源码中找不到目标数据 | 抓取数据接口而非静态HTML |
| 数据加密 | 接口返回乱码/密文 | 断点调试前端JS,找到解密逻辑 |
这些手段里,IP频率限制是初级爬虫最容易触发的。我之前测试一个数据站时,脚本每秒钟发10个请求,用了不到两分钟IP就被临时封禁了。后续改成单线程加随机延时2到4秒,反而稳稳地跑完了全部数据。很多初学者觉得加延时是在浪费时间,实际上延时恰恰是爬虫稳定性的保障。
4.3 前端防爬的识别意义
搜索词里的"防止查看页面源码""防止打开"这类操作,本质上防不了真正决心抓数据的人。因为前端代码最终都要下发到浏览器才能执行,任何混淆、加密、删除右键菜单的手段,都只能增加门槛,无法彻底阻断。对爬虫学习者来说,了解这些手段的意义在于:看到页面结构异常复杂、源码被混淆时,不要硬刚静态解析,果断换思路去找数据接口。
比如有些网站把页面中所有文字都拆成一个一个span标签,让你在页面源码里搜关键词搜不到。这种时候不是你的xpath写错了,而是页面故意做成了这个形态。应对方法是去Network面板里找后端返回的JSON数据接口,而不是死磕渲染后的HTML。
4.4 视频版权和隐私内容必须避开
抓视频这个领域,版权风险比抓普通文字数据高得多。我个人给自己定过几条规矩,这里分享出来:
- 只抓取无版权争议的免费公开视频,如开放课程、公共科普内容、原创授权的素材。
- 不抓取需要付费订阅、会员专享、登录鉴权后才能观看的内容。即使技术上能绕过,这类行为也明确越界。
- 不抓取涉及个人隐私的录屏内容,比如带账号信息的课程回放、会议录制文件。搜索词里出现的"腾讯会议视频抓取工具"这类需求,我不建议也不鼓励做——会议录制涉及与会者隐私,不该成为爬虫练习的对象。
- 下载后仅用于学习分析,不二次分发、不用于商业用途。
爬虫本身是工具,工具的价值取决于使用目的。我接触过很多把爬虫玩得很好的同行,大家交流时都会先问一句"这个数据源允许爬吗"——这是行业默契,也是避免给自己惹麻烦的基本判断标准。
5. 高频报错与排查技巧:抓不到视频的几种典型原因
5.1 xpath返回空列表,先别急着改表达式
初学xpath时最容易陷入的循环是:表达式写一大串,返回空数组,然后反复修改、越改越复杂。我现在的排查顺序是固定的,效率高很多:
- 先确认页面在浏览器里能看到目标元素,按F12在Elements面板中复制它的
xpath或css path,看是否和自己的写法一致。 - 确认
resp.text里确实包含关键字,用Python的in运算符直接搜一遍。如果源码里搜不到,说明数据是JS动态渲染的,不是你的表达式有问题。 - 确认页面结构是否用了iframe。视频网站经常把播放器嵌在iframe里,外层页面源码里永远找不到视频地址,需要先找到
//iframe/@src再二次抓取。
我之前帮一个新手排查了两小时的视频抓取问题,最后发现就是iframe的问题,外层页面里压根没有视频标签。这个坑非常隐蔽。
5.2 403 Forbidden的逐个排查
403是抓视频场景最高频的状态码,处理思路按以下顺序走:
- 先看有没有设置UA,没有的话立刻补上。
- 再看Referer有没有写对,确认是完整的页面URL,不是只写了域名。
- 检查请求头里是否少了必要的Cookies。有些平台首次访问会下发会话Cookie,不带Cookie的请求会被判定为非法访问。
- 如果前几步都没问题,提高两次请求之间的延时,多试几遍。有些反爬策略是触发后临时标记IP,等一段时间自动恢复。
5.3 个别ts分片下载失败
下载几百个分片时偶发失败属于常态,不建议因此中断整个任务。我在代码里加了失败重试机制,效果很好:
def download_ts(ts_url, retries=3): for attempt in range(retries): try: resp = requests.get(ts_url, headers=download_headers, timeout=20) if resp.status_code == 200: return resp.content except Exception: pass time.sleep(1 + attempt * 2) return None另一个更隐蔽的问题是合并顺序错乱。如果文件命名用的是原始ts名称,而原始名称包含了不定长度的动态字符串,排序方式会和实际播放顺序不一致。我惯用的方法是统一重命名为固定位数序号:0开头补足长度,保证字符串排序和数字排序一致。这个细节看着不起眼,但能直接决定合并出来的视频能不能正常播放。
5.4 动态页面拿不到视频地址,切数据接口
如果页面源码里完全没有视频相关字段,这基本说明视频地址是JS动态获取的。处理思路是打开开发者工具,切到Network面板,先按media类型过滤,看看有没有mp4或m3u8请求。如果有,直接复制地址。如果没有,再按xhr类型过滤,逐个查看返回的JSON,通常在某个接口返回里能找到一个video_url或play_url字段。
找到接口后,直接请求这个接口即可,不需要经过页面。写代码时注意接口对请求头的要求一般比页面严格,Request Header里的Accept、Referer、X-Requested-With都要尽量完整地带上。这些字段在Network面板里都能看到,不需要凭空猜测。
6. 抓取完视频之后,我建议你继续做的事
视频抓下来只是起点,不是终点。我见过很多新手跑通一个下载脚本后激动不已,然后就没有然后了。但真正让爬虫技能发生质变的,是抓完之后对数据进一步处理和反思。比如这个视频页面里除了视频文件,还有标题、标签、分类、播放量等结构化信息,你完全可以在一个脚本里把这些数据一并提取出来,存入SQLite或CSV文件,形成自己的视频库。这一步练的是数据清洗与存储能力,和爬虫本身一脉相承。
再往深走,你可以对比一下浏览器和脚本发出的请求有什么区别,试着把请求头精简化,看看哪个字段是服务器真正检查的。这种"做减法式"的实验,比光看教程更加能锻炼对HTTP协议的理解。等你能做到不带Cookie、只用最精简的headers就拿到数据时,你对爬虫底层原理的掌握就已经超过大多数人。我自己就是这样一步步从"能用别人的代码跑通"练到"理解每一步为什么这么做"的,这段路没有捷径,但每一步都值。