Skip to content
Go back

视频爬虫系统不能只写一个脚本跑到底

视频提链刚开始很容易写成一个脚本:

读取一批 URL  -> 一个个打开页面  -> 找到视频地址  -> 下载到本地

这个版本可以验证思路,但不能长期运行。原因很简单:视频页面加载慢,部分页面会失败,Chrome Headless 比普通 HTTP 请求重,视频下载也耗时。如果所有逻辑都在一个进程里顺序执行,速度慢、稳定性差,出错后也很难恢复。

所以这套系统后面更接近一个任务队列,而不是一个爬虫脚本。

任务队列是系统的核心

视频抓取任务可以抽象成一条任务记录:

{  "id": "task_10001",  "url": "https://example.com/video/123",  "status": "pending",  "retryCount": 0,  "createdAt": 1503000000000}

任务状态流转大概是:

pending  -> running  -> success  -> failed  -> retrying

每个 worker 从队列里取一个 pending 任务,标记为 running,然后执行抓取。

async function runWorker() {  while (true) {    const task = await taskQueue.take();    if (!task) {      await sleep(1000);      continue;    }    try {      await processTask(task);      await taskQueue.markSuccess(task.id);    } catch (error) {      await taskQueue.markFailed(task.id, error);    }  }}

队列化以后,系统就具备几个能力:

这比一个线性脚本可靠得多。

一个任务内部可以拆成几个阶段

视频抓取任务本身也不是一个步骤。

可以拆成:

加载页面  -> 等待页面稳定  -> 触发播放  -> 监听网络请求  -> 识别视频资源  -> 下载视频  -> 校验文件  -> 保存结果

用代码表达,大概是:

async function processTask(task) {  const page = await browser.newPage();  try {    const collector = createVideoCollector(page);    await page.goto(task.url, {      waitUntil: "networkidle2",      timeout: 30000    });    await tryClickPlayButton(page);    const video = await collector.waitForVideoUrl(15000);    const file = await downloadVideo(video);    await saveResult(task.id, {      videoUrl: video.url,      filePath: file.path,      size: file.size    });  } finally {    await page.close();  }}

这里最重要的是 finally。Chrome 页面必须及时关闭,否则长时间运行后内存会不断上涨。Headless 系统的稳定性,经常不是毁在提链逻辑,而是毁在资源没有释放。

Node.js 负责调度,不负责重计算

这套系统用 Node.js 比较合适,因为它主要是 I/O 密集型:

打开网页等待网络请求下载视频写文件记录任务状态

Node.js 很适合做这种异步调度。

但要注意,Node.js 主线程不适合做大量 CPU 计算。比如视频转码、复杂文件处理、分片合并等,如果直接在 Node.js 里做,会阻塞事件循环。

对于 M3U8 合并,应该交给 ffmpeg

import { spawn } from "child_process";function mergeM3u8(input, output) {  return new Promise((resolve, reject) => {    const ffmpeg = spawn("ffmpeg", [      "-i",      input,      "-c",      "copy",      output    ]);    ffmpeg.on("exit", code => {      if (code === 0) resolve();      else reject(new Error(`ffmpeg exited with ${code}`));    });  });}

Node.js 做调度,Chrome Headless 做页面运行,ffmpeg 做视频处理。每个工具负责自己擅长的部分。

PM2 多进程用于并行处理

单进程跑 Headless Chrome,吞吐很有限。

如果任务很多,可以通过 PM2 启动多个 worker 进程:

pm2 start worker.js -i 4 --name video-crawler

这里的 -i 4 表示启动 4 个进程。每个进程都是一个独立 worker,从同一个任务队列里取任务。

并行模型是:

Task Queue  -> Worker 1  -> Worker 2  -> Worker 3  -> Worker 4

多进程的好处是:

但多进程不是越多越好。每个 worker 可能都会启动 Chrome 页面,CPU、内存和带宽都会被占用。并发过高会导致整体更慢,甚至让机器不稳定。

更稳妥的方式是给每个进程设置内部并发上限:

const CONCURRENCY = 2;async function startWorkerPool() {  await Promise.all(    Array.from({ length: CONCURRENCY }).map(() => runWorker())  );}

最终并发数是:

PM2 进程数 × 单进程并发数

这个值要根据机器配置、目标站点速度、下载带宽和 Chrome 内存占用调。

多进程下任务领取要避免重复

多进程消费同一个队列时,最重要的是避免重复领取任务。

错误做法是:

Worker A 查到 task_1 是 pendingWorker B 也查到 task_1 是 pending两个 worker 同时处理同一个任务

任务领取必须是原子的。

如果用数据库,可以通过状态更新做抢占:

update tasksset status = 'running',    worker_id = $workerId,    started_at = now()where id = (  select id  from tasks  where status = 'pending'  order by created_at asc  limit 1  for update skip locked)returning *;

如果用 Redis,可以用 list 或 stream。核心原则是一样的:一个任务同一时间只能被一个 worker 拿到。

这个问题在单进程脚本里不存在,但一旦上 PM2 多进程,就必须解决。

失败重试要分类型

视频抓取失败很常见,但不是所有失败都应该重试。

可以把失败分成几类:

类型是否重试说明
页面超时可以重试可能是网络抖动
视频地址未找到可以有限重试可能是页面加载慢
404不重试资源不存在
解析失败不一定可能需要适配规则
下载中断可以断点或重试网络或目标站点问题
ffmpeg 合并失败看错误原因可能是分片缺失

重试也不能立即无限重试。可以使用退避策略:

第 1 次失败:1 分钟后重试第 2 次失败:5 分钟后重试第 3 次失败:30 分钟后重试超过上限:进入 failed

这样不会因为一批坏任务把 worker 全部拖住。

下载阶段要和提链阶段解耦

一个任务里最耗时的可能不是打开页面,而是下载视频。

如果 worker 一直等待大文件下载完成,Chrome 资源可能被长期占用。更合理的方式是把“提链”和“下载”拆成两个队列:

提链队列  -> 找到视频地址  -> 写入下载队列下载队列  -> 下载 MP4 或 M3U8  -> 保存文件  -> 更新任务结果

这样 Chrome worker 专注于页面提链,download worker 专注于文件下载。两类任务可以设置不同并发:

Chrome worker 并发低一些Download worker 并发按带宽控制

这比一个 worker 从头干到尾更容易扩展。

需要保留可观察性

爬虫系统如果没有日志,很快会变成黑盒。

至少要记录:

任务总数pending / running / success / failed 数量平均处理耗时提链成功率下载成功率失败原因分布单个 worker 当前处理任务Chrome 页面异常退出次数

任务日志可以按阶段记录:

[task_10001] start[task_10001] page loaded 1820ms[task_10001] video url found mp4[task_10001] download success 35MB[task_10001] done 12.4s

有了这些日志,才能判断是页面提链慢、下载慢、M3U8 合并慢,还是某个站点规则失效。

小结

视频爬虫系统不能只写一个脚本跑到底。

真正可用的系统至少要有:

单个脚本能证明方案可行,但任务队列和多进程模型才能让它持续运行。对这种 I/O 密集、失败率不低、耗时不可控的系统来说,工程化设计比某一段爬取代码更重要。


Share this post on: