当时做视频资源抓取时,最开始的思路其实很直接:请求页面 HTML,解析里面的视频地址,然后把视频下载到本地。
但真正跑起来后会发现,这个方式只能处理很简单的页面。很多视频网站的视频地址并不会直接出现在首屏 HTML 里,而是在页面脚本执行后,通过接口异步返回,或者在播放器初始化时才拼出来。还有一些站点会根据用户环境、播放器配置、清晰度、鉴权参数动态生成播放地址。
这时再靠普通 HTTP 请求和字符串匹配,就会非常脆弱。
Chrome Headless 的价值就在这里:它不是模拟一个浏览器,而是真的启动一个没有界面的 Chrome。页面里的 JavaScript 会执行,DOM 会更新,接口会请求,播放器逻辑也会按真实浏览器环境运行。我们可以在这个过程中监听网络请求,找到真正的视频资源地址。
普通爬虫为什么不够用
如果目标页面是纯静态 HTML,普通爬虫足够了:
请求 HTML -> 正则或 DOM 解析 -> 找到 video src -> 下载文件
但视频页面往往不是这样。
实际会遇到几类情况:
- 页面首屏 HTML 里没有视频地址。
- 视频地址通过接口异步返回。
- 播放器脚本会根据配置动态生成地址。
- 页面需要执行一段 JS 后才出现播放器。
- 视频资源可能是 MP4,也可能是 M3U8。
- 部分资源 URL 带有临时 token 或签名。
这种场景下,如果只用 request 或 axios 拉 HTML,拿到的只是一个还没执行脚本的页面。真正的视频地址还没有出现。
所以系统需要的不是“下载网页”,而是“让网页真实运行起来,然后观察它做了什么”。
Chrome Headless 解决的是运行环境问题
Chrome 59 开始支持 Headless 模式。所谓 Headless,就是没有可视化窗口的 Chrome。
它仍然具备普通 Chrome 的能力:
执行 JavaScript加载 CSS 和图片发起 XHR / Fetch 请求运行播放器脚本暴露 DevTools Protocol支持截图和调试
区别只是没有 GUI。对服务端自动化来说,这正好合适。
启动方式很简单:
chrome --headless --remote-debugging-port=9222 https://example.com
如果需要调试,可以打开:
http://localhost:9222
通过远程调试端口,可以看到当前 Headless Chrome 里的页面,并用 DevTools 观察网络请求、DOM 和控制台输出。
这点对爬虫系统非常重要。因为视频提链失败时,不是简单看日志就能定位。你需要知道页面到底有没有加载、播放器有没有初始化、接口有没有请求、资源地址有没有返回。
视频提链的核心是监听网络请求
用 Headless Chrome 做视频抓取时,不一定要从 DOM 里找 <video> 标签。
更稳定的做法是监听浏览器网络请求。只要页面或播放器请求了视频资源,我们就有机会捕获它。
一个简化流程是:
打开目标页面 -> 等待页面脚本执行 -> 监听所有网络请求 -> 识别 MP4 / M3U8 / FLV 等资源 -> 记录候选视频地址 -> 选择最合适的地址 -> 下载到本地
使用 Chrome DevTools Protocol 时,可以监听 Network 事件:
client.on("Network.responseReceived", event => { const { response } = event; const url = response.url; if (isVideoUrl(url, response.mimeType)) { collectVideoUrl(url); }});
判断视频资源不能只靠后缀。有些 URL 没有 .mp4 后缀,但 Content-Type 是视频类型;有些 M3U8 是播放列表,需要继续解析里面的 .ts 分片。
可以同时看:
URL 后缀Content-Type请求路径关键词响应头资源大小播放器接口返回内容
这样命中率会更高。
为什么要让页面真的播放
有些页面只打开还不够,必须触发播放按钮后才会请求视频资源。
这类页面的流程可能是:
页面加载 -> 初始化播放器壳 -> 用户点击播放 -> 请求播放配置 -> 获取视频地址 -> 开始拉取视频资源
如果爬虫只等待 load 事件,可能永远等不到视频地址。
所以系统需要支持页面动作:
await page.goto(url);await page.waitForSelector(".play-button");await page.click(".play-button");await waitForVideoRequest();
有时候还要滚动页面、关闭弹窗、等待登录态、切换清晰度。Chrome Headless 的好处是这些动作都可以用浏览器自动化完成,而不是靠猜接口。
MP4 和 M3U8 的处理不一样
抓到视频地址以后,下载阶段也要区分资源类型。
如果是 MP4,处理相对直接:
拿到 MP4 URL -> HTTP 下载 -> 写入本地文件 -> 校验文件大小
如果是 M3U8,就复杂一些。M3U8 本身不是视频文件,而是一个索引文件,里面记录了很多 .ts 分片地址。
处理流程是:
下载 m3u8 文件 -> 解析 ts 分片列表 -> 按顺序下载所有 ts -> 使用 ffmpeg 合并成 MP4 -> 清理临时文件
例如:
ffmpeg -i input.m3u8 -c copy output.mp4
实际系统里还要处理相对路径、分片下载失败、重试、超时和文件命名问题。M3U8 的处理不能简单理解成“拿到链接就结束”,它只是进入了另一个下载流程。
Headless Chrome 不是万能的
Chrome Headless 能提高提链成功率,但也有成本。
相比普通 HTTP 请求,它更重:
启动浏览器进程需要资源页面执行 JS 需要时间并发太高会吃 CPU 和内存页面卡死时需要超时回收网络请求需要更细的日志
所以它更适合作为“需要真实浏览器环境”的提链方案,而不是所有页面都无脑使用。
比较稳妥的策略是分层:
简单页面:HTTP 解析动态页面:Chrome Headless特殊站点:单独适配策略失败任务:进入重试和人工排查队列
这样可以避免系统成本过高。
调试能力很重要
视频提链最怕失败原因不清楚。
一个任务失败时,至少要记录:
- 目标页面 URL。
- 页面加载是否成功。
- 最后一次跳转地址。
- 关键网络请求列表。
- 命中的候选视频地址。
- 页面控制台错误。
- 任务超时时间。
- 截图或 HTML 快照。
Headless Chrome 支持截图:
await page.screenshot({ path: `debug/${taskId}.png`, fullPage: true});
这类调试产物看起来不是核心功能,但真正排查线上失败任务时非常有用。
小结
Chrome Headless 在这套视频爬取系统里的作用,不是“更高级的请求库”,而是提供真实浏览器运行环境。
它解决的是几个普通爬虫很难稳定处理的问题:
- 页面脚本需要真实执行。
- 视频地址异步生成。
- 播放器逻辑需要初始化。
- 资源请求需要从网络层捕获。
- MP4 和 M3U8 需要不同下载策略。
对这类系统来说,核心不是写一个能打开页面的脚本,而是把“打开页面、触发播放、监听请求、识别资源、下载文件、保存调试信息”串成一条稳定链路。
这也是我后来理解爬虫系统的一个变化:爬虫不只是解析页面,很多时候是在复现用户环境,然后从真实运行过程中提取数据。