ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

批量下载工具设计:解析与下载两段式架构及无水印保存实践

批量下载工具设计:解析与下载两段式架构及无水印保存实践

1. 批量下载这件事,为什么值得认真做一套工具

做内容运营和素材整理的朋友大概都有过这种体验:刷到一个账号的系列作品,想整体保存下来做竞品分析或者二次剪辑参考,结果只能一条一条手动存,几十条视频点下来手指都酸了,存完还发现画面上挂着平台水印,裁掉水印又破坏了构图。更麻烦的是,有些作品是图文合集、有些是带背景音乐的原声、有些是直播回放,类型不一样,保存方式也不一样,手动操作根本没法统一处理。

这就是我最初动手折腾批量下载工具的出发点。标题里提到的 douyin-downloader,本质上是一套面向短视频平台的批量内容归档方案,核心能力可以拆成三块:批量解析(一次性拿到某个账号或某个合集下的全部作品列表)、无水印保存(拿到原始媒体文件而不是带水印的播放流)、全类型覆盖(视频、图文、原声、合集、直播回放都能处理)。它解决的不是“能不能下载”这个初级问题,而是“能不能高效、干净、成体系地归档”这个进阶需求。

这套东西适合谁?我梳理了一下,大致三类人用得上:一是做竞品分析和选题调研的内容从业者,需要把对标账号的作品成批存下来反复看;二是做二次创作的剪辑爱好者,需要干净的无水印素材;三是做个人数字资产管理的普通用户,想把自己喜欢的创作者作品存到本地长期保存。不管你是完全没写过代码的小白,还是有一定脚本基础的半技术流,下面的内容我都会尽量把原理讲透、把步骤拆细,让你照着做就能跑起来。

需要先说明一点:批量下载工具的使用边界要自己把握好,保存下来的内容仅限个人学习、研究和合理引用,不要用于商业传播或二次分发,这是所有做内容归档的人都应该守住的基本线。工具本身是中性的,怎么用取决于使用者。

2. 整体方案设计:为什么是“解析+下载”两段式架构

2.1 核心思路拆解:先拿清单,再取文件

很多人第一次接触这类工具,会以为它是“模拟点击播放然后录屏”,其实完全不是。真正高效的批量下载走的是接口解析路线,整个流程拆成两个独立阶段:

第一阶段是列表解析。你给它一个账号主页链接或者合集链接,它去请求平台返回的作品列表数据,拿到每一条作品的唯一标识、标题、类型、发布时间等元信息。这一步只拿“清单”,不碰媒体文件,速度快、开销小。

第二阶段是媒体下载。拿着清单里每一条作品的标识,逐条去请求对应的媒体资源地址,把视频流或图片集下载到本地。这一步才是真正消耗带宽的环节。

为什么要把两段拆开?我踩过的坑告诉我,合在一起做会非常难受。列表解析如果失败,你根本不知道是网络问题还是链接问题;下载如果中途断了,你也没法单独重试某一条。拆成两段之后,清单可以先落盘成 JSON 文件,下载阶段读清单执行,断了就断点续传,某一条失败就单独重跑那一条,整个流程的可控性完全不一样。

提示:清单落盘这个设计是整套方案里最值得抄的作业。哪怕你后面不用现成工具,自己写脚本也强烈建议把“解析结果”和“下载动作”解耦,这是批量任务稳定性的根基。

2.2 无水印的原理:拿原始资源而不是播放流

水印这件事得说清楚。平台在网页端或 App 端播放时,给你的是经过转码、叠加了水印的播放流;但作品本身在服务端存的是原始上传文件,通常不带水印。批量下载工具之所以能拿到无水印版本,是因为它请求的是原始媒体资源地址,而不是播放器实际渲染的那条流。

这里有个关键细节:原始资源地址往往不是直接暴露在页面 HTML 里的,而是藏在页面加载时请求的某个数据接口的返回体里。工具要做的就是模拟这个数据请求,从返回的 JSON 结构里把媒体地址字段提取出来。不同作品类型(视频、图文)对应的字段结构不一样,所以解析逻辑要按类型分支处理,这也是为什么“全类型覆盖”本身就是一个技术难点,不是简单加个判断就行。

2.3 方案选型:为什么用脚本而不是现成软件

市面上确实有一些图形化的下载软件,但我最终选择脚本方案,理由有三个。第一是可控性,脚本能精确控制请求头、并发数、重试策略,出问题能定位到具体哪一行;第二是可扩展,今天想加个“只下载最近30天作品”的过滤逻辑,改几行代码就行,图形软件只能干瞪眼;第三是可批量,脚本天然适合挂定时任务,比如每天凌晨自动同步某个账号的新作品,这是图形软件做不到的。

脚本方案的语言选择上,Python 是首选。原因很实际:请求库成熟(requests 一把梭)、JSON 处理方便、文件操作简单,而且社区里同类项目的参考实现多,遇到问题好搜。如果你只会 JavaScript,Node.js 也能做,但 Python 在数据处理上的顺手程度确实更高一档。

3. 核心细节解析:请求构造与数据提取的关键点

3.1 请求头构造:让服务端认你是正常访问

批量下载第一个卡点永远是请求被拒。服务端会检查请求头里的多个字段来判断这是不是正常访问,最常被检查的是 User-Agent、Referer 和 Cookie 这三样。

User-Agent 要伪装成主流浏览器的标识,不能是脚本默认的 python-requests 字样,否则一眼就被识别。Referer 要带上平台域名,表示你是从站内跳转过来的。Cookie 是最关键也最麻烦的一项,它承载了你的登录态和会话信息,没有有效 Cookie,很多接口会直接返回空数据或者要求验证。

我的做法是把浏览器里正常访问时抓到的请求头完整复制下来,存成一个配置字典,脚本里统一带上。这里有个实操心得:Cookie 是有有效期的,跑批量任务前先手动测一条,确认能拿到数据再开跑,别等跑了几百条才发现 Cookie 过期了,白等半天。

HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...", "Referer": "https://平台域名/", "Cookie": "从浏览器开发者工具里复制的完整 Cookie 字符串", "Accept": "application/json, text/plain, */*", }

注意:Cookie 属于个人敏感凭证,配置文件不要提交到公开仓库,本地跑完记得清理。这是很多人容易忽略的安全细节。

3.2 列表接口的翻页逻辑:游标比页码更靠谱

拿到作品列表不是一次请求就完事的,一个账号可能有几百上千条作品,接口一定是分页返回的。这里要区分两种分页模式:页码分页(page=1,2,3...)和游标分页(cursor=xxx)。

短视频平台的列表接口大多用游标分页,返回体里会带一个has_more标志和一个max_cursor或类似的游标值,下一次请求把这个游标带上,直到has_more为 false 为止。为什么用游标而不是页码?因为作品列表是动态变化的,用页码翻页在数据变动时会出现重复或遗漏,游标则相对稳定。

写循环的时候要加两个保护:一是最大页数限制,防止接口异常导致死循环;二是请求间隔,每翻一页 sleep 一到两秒,别把服务端打得太狠,也降低被限流的概率。我一般设 1.5 秒,实测下来既不影响效率又比较稳。

3.3 媒体地址提取:按作品类型分支处理

清单拿到手之后,每条作品的元信息里会有一个类型字段,常见取值包括视频、图文、合集等。不同类型的媒体地址藏在不同的字段路径下,解析逻辑必须分支。

视频类相对简单,元信息里通常有一个播放地址列表,取其中码率最高或者标注为原始的那一条即可。图文类要复杂一些,它返回的是一个图片地址数组,需要遍历数组逐张下载,而且图片可能有多种尺寸,要挑原图尺寸的那一版。合集类则是一个嵌套结构,里面又包含多条子作品,需要递归展开。

这里有个容易踩的坑:有些作品的媒体地址是带时效签名的,过期就失效。所以解析和下载之间的间隔不能太长,最好是解析完立刻下载,或者下载前重新请求一次地址。我早期的版本就是先解析全部再统一下载,结果跑到后面一批全部 403,排查了半天才发现是签名过期。

3.4 文件命名与目录结构:归档规范决定后期可用性

下载下来的文件如果命名一团糟,后期根本没法用。我的命名规范是发布时间_作品标题_作品ID.扩展名,发布时间用YYYYMMDD格式,这样按文件名排序就是按时间排序。标题里如果有特殊字符(斜杠、冒号、问号等)要替换掉,否则在某些操作系统上会创建文件失败。

目录结构按账号名/年份/月份/三层组织,这样一年下来几千条作品也能快速定位。图文作品单独建一个子目录,里面按01.jpg、02.jpg顺序编号,方便还原发布顺序。

归档维度具体规则解决的问题
文件名时间_标题_ID.扩展名排序即时间线,ID 保证唯一
目录层级账号/年/月大量文件快速定位
图文处理子目录+序号命名还原图片发布顺序
特殊字符统一替换为下划线避免文件系统报错

4. 实操过程:从零跑通一套批量下载流程

4.1 环境准备与依赖安装

先把运行环境搭起来。Python 版本建议 3.8 以上,太老的版本有些库装不上。依赖主要就三个:requests负责网络请求,tqdm负责下载进度条(批量下载没有进度条会很难受,不知道跑到哪了),fake-useragent可选,用来随机化 User-Agent。

pip install requests tqdm

安装完之后建一个项目目录,里面放三个文件:config.py存请求头和配置参数,parser.py负责列表解析,downloader.py负责媒体下载。分文件的好处是职责清晰,调试的时候知道该看哪个文件。

配置参数里我建议把这几项单独拎出来:并发下载数(建议 3 到 5,太高容易被限流)、请求超时时间(建议 15 秒)、重试次数(建议 3 次)、下载间隔(建议 0.5 到 1 秒)。这些参数不是拍脑袋定的,并发数 3 到 5 是实测下来在稳定性和效率之间的平衡点,再高就容易触发风控。

4.2 列表解析的完整实现

解析阶段的入口是一个账号主页链接或合集链接。第一步是从链接里提取出账号标识或合集标识,这个标识是后续请求列表接口的必要参数。提取方式看链接结构,通常在路径的某一段里。

拿到标识后构造列表请求,带上前面说的请求头和分页参数,循环请求直到has_more为 false。每一页返回的作品元信息追加到一个总列表里,最后把总列表序列化成 JSON 落盘。

import json, time, requests from config import HEADERS def fetch_work_list(account_id, max_pages=50): all_items, cursor, page = [], 0, 0 while page < max_pages: params = {"sec_user_id": account_id, "max_cursor": cursor, "count": 20} resp = requests.get(LIST_API, headers=HEADERS, params=params, timeout=15) data = resp.json() items = data.get("aweme_list", []) if not items: break all_items.extend(items) if not data.get("has_more"): break cursor = data.get("max_cursor", 0) page += 1 time.sleep(1.5) with open("work_list.json", "w", encoding="utf-8") as f: json.dump(all_items, f, ensure_ascii=False, indent=2) return all_items

这段代码里max_pages=50就是前面说的死循环保护,time.sleep(1.5)是请求间隔。落盘的 JSON 文件既是下载阶段的输入,也是排查问题的依据——如果下载阶段发现某条作品缺字段,回头翻 JSON 就能看到解析时到底拿到了什么。

4.3 媒体下载与断点续传

下载阶段读work_list.json,逐条处理。每条作品先判断类型,视频走视频下载分支,图文走图片批量下载分支。下载前先检查本地是否已存在同名文件,存在就跳过,这就是最简单的断点续传——不用维护额外的状态文件,靠文件系统本身就能判断。

视频下载用流式写入,避免大文件一次性读进内存:

def download_video(url, save_path): if os.path.exists(save_path): return "skipped" for attempt in range(3): try: with requests.get(url, headers=HEADERS, stream=True, timeout=30) as r: r.raise_for_status() with open(save_path, "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) return "ok" except Exception as e: if attempt == 2: return f"failed: {e}" time.sleep(2)

重试逻辑是必须的,网络抖动太常见了。三次重试之间间隔 2 秒,给服务端一点喘息时间。下载失败的条目单独记录到一个failed.log里,跑完之后针对这个日志再补跑一轮,基本能把失败率压到很低。

4.4 并发控制与进度反馈

单线程下载几百条视频会很慢,所以要上并发。但并发不能无脑开,我的做法是用concurrent.futures.ThreadPoolExecutor,最大工作线程设 4 个,配合tqdm显示总进度。

from concurrent.futures import ThreadPoolExecutor from tqdm import tqdm with ThreadPoolExecutor(max_workers=4) as pool: futures = [pool.submit(process_item, item) for item in work_list] for f in tqdm(futures, desc="下载进度"): f.result()

线程数设 4 的理由:实测 4 线程时下载速度已经能跑满普通家用带宽,再往上加收益递减,反而更容易触发限流。如果你的网络环境特殊,可以先从 2 线程试起,稳定了再往上调。

实操心得:并发下载时每个线程的请求间隔要错开,不要所有线程同一时刻发请求。可以在每个线程的任务开头加一个time.sleep(random.uniform(0, 1)),把请求打散,稳定性会明显提升。

5. 常见问题与排查技巧实录

5.1 请求返回空数据或验证页

这是最高频的问题,表现是接口返回 200 但内容是空的,或者返回一段要求验证的 HTML。原因基本锁定在请求头不完整或 Cookie 失效。排查顺序:先检查 Cookie 是否过期(浏览器里重新登录一次再复制),再检查 User-Agent 和 Referer 是否带全,最后看请求频率是不是太高被临时限制了。

如果是频率问题,把并发降到 2、间隔加到 2 秒,等十几分钟再试。我遇到过连续跑了两千多条之后开始返回空数据的情况,停半小时再跑就恢复了,说明是触发了频率限制而不是封禁。

5.2 下载到一半中断或文件损坏

文件损坏的典型表现是视频能下载但播放器打不开,或者图片只显示一半。原因通常是下载过程中连接中断但代码没捕获到,写入了不完整的数据。解决办法是下载完成后校验文件大小,和响应头里的Content-Length对比,不一致就删掉重下。

还有一种情况是磁盘空间不足,写到一半报错。批量下载前先估算一下总大小:单条视频按平均 10MB 算,一千条就是 10GB,图文按每张 500KB、每条 9 张算,一千条图文大概 4.5GB。心里有个数,提前清理磁盘。

5.3 图文作品的图片顺序错乱

图文作品下载下来如果顺序乱了,后期根本没法用。问题出在并发下载图片时没有保证顺序。解决办法是图片下载不走并发,或者并发但按索引命名后统一重命名。我现在的做法是图片串行下载,反正单条图文的图片数量有限,串行的速度损失可以接受,换来的是顺序绝对可靠。

5.4 常见问题速查表

现象最可能原因处理方式
接口返回空数据Cookie 失效或请求头不全重新复制 Cookie,补全请求头
返回验证页面请求频率过高降并发、加间隔,等待后重试
视频无法播放文件下载不完整校验大小,删除重下
图片顺序错乱并发下载未保序改为串行或按索引重命名
媒体地址 403签名过期解析后立即下载,或重新请求地址
文件名创建失败标题含特殊字符统一替换为下划线

5.5 几个容易被忽略的细节

第一个细节是时间戳时区。作品发布时间返回的通常是 Unix 时间戳,转换成本地时间时要确认时区,否则归档目录的月份可能错位。第二个细节是重复作品去重。同一个作品可能因为转发出现在多个列表里,下载前用作品 ID 做一次去重,能省不少空间。第三个细节是日志留存。每次跑批量任务都留一份运行日志,记录开始时间、处理条数、成功失败数,出问题的时候这份日志就是最好的排查线索。

6. 工具选型与扩展思路

6.1 现成工具与自建脚本的取舍

如果你只是想偶尔存几条作品,现成的图形化工具足够用,拖进去点几下就完事。但如果你要做的是成体系的归档——比如持续跟踪几十个账号、每天增量同步、按规则自动分类——那自建脚本的优势就体现出来了。现成工具的通病是参数不可调、失败不可控、扩展不可做,而脚本方案在这三点上都是开放的。

我的建议是:先用现成工具跑通一次完整流程,理解“解析-下载”这个基本模型,然后再迁移到脚本方案。直接上脚本对纯小白来说门槛偏高,容易在环境配置阶段就放弃。

6.2 增量同步的扩展做法

基础版跑通之后,最值得加的扩展是增量同步。做法很简单:每次解析完列表后,和本地已有的作品 ID 集合做差集,只下载新增的部分。本地 ID 集合可以维护在一个文本文件里,每下载成功一条就追加一行。这样每天定时跑一次,只处理新作品,几分钟就能完成同步。

再进一步可以加过滤规则,比如只下载点赞数超过某个阈值的作品,或者只下载指定时间段内的作品。这些规则在解析阶段就能过滤,减少无效下载。

6.3 长期维护的注意事项

平台的数据接口不是一成不变的,字段结构、参数名、请求头校验规则都可能调整。所以这套脚本需要定期检查,发现解析失败时第一时间对比接口返回结构有没有变化。我的习惯是每个月手动跑一次全量解析,看看返回的字段和上次有没有差异,提前发现问题比事后排查省事得多。

另外,归档目录要定期做一次完整性检查,随机抽几条文件打开看看能不能正常播放或显示,避免长期积累下来一堆损坏文件自己还不知道。这个习惯看起来笨,但确实能救命。

我在实际使用中最大的体会是,批量下载这件事的难点从来不在“下载”本身,而在“稳定”和“可维护”。一套能跑一次的脚本和一套能跑一年的脚本,差距全在异常处理、断点续传、日志留存这些不起眼的地方。把这几块做扎实了,剩下的就是按需扩展的事了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进