开发了个抖音资源下载CLI工具,思路笔记(含源码分享)
2026-03-08
杂乱的知识
type
Post
status
Published
date
Mar 8, 2026
slug
douyin-video-no-watermark-downloader-source-code
summary
一个最新实现的抖音视频下载脚本,从真实开发与调试结果出发,完整说明脚本如何将抖音分享文案中的短链接解析为真实作品页面,并进一步提取
aweme_id、解析页面结构化数据(如 RENDER_DATA),最终获取视频、音频、封面、头像等资源并下载到本地。文章不仅讲解了整体技术流程,还深入分析了项目架构设计(请求层、解析层、服务层、下载层)、核心技术栈(Python、curl_cffi、typer、tenacity 等)以及资源分类与下载策略。通过真实案例验证,该脚本已经能够稳定解析抖音分享链接并按需下载多种资源,是一个可实际运行的完整实现方案。tags
推荐
category
杂乱的知识
icon
password
源码完善后晚些时候分享
1. 我最开始对这个需求的理解
刚开始看这个需求时,表面上像是:
- 用户给一个抖音分享链接
- 脚本请求一下
- 拿到视频地址
- 下载完事
但真正做起来才发现,事情没这么简单。
真实输入并不是一个干净的 mp4 链接,而往往是:
- 一整段抖音分享文案
- 中间夹着一个
v.douyin.com的短链
- 短链跳转之后,页面也不一定直接带完整作品信息
- 页面里的资源不只有视频,还有音频、封面、头像、图片
所以这个问题本质上不是“下载一个文件”,而是:
如何从真实世界里的抖音分享文案出发,稳定定位到目标作品,再把页面里的各种资源提取出来,并按需下载。
2. 当前这个项目已经做到什么程度了
现在这个脚本已经不只是“能解析链接”,而是已经能真实跑通整条链路。
当前已经成功实现的能力
- 从一整段分享文案中提取短链接
- 解析短链接跳转,拿到真实作品页
- 提取目标作品
aweme_id
- 尝试多个候选页面来源
- 从页面结构化数据里匹配目标作品对象
- 提取候选资源
- 按资源类型下载到本地
当前已经验证成功的资源类型
- 视频
video
- 音频
audio
- 封面
cover
- 头像
avatar
- 图片
image
- 全部资源
all
这件事为什么让我觉得它已经“可用了”
因为我不是只在本地拿几个假数据测,而是已经用:
- 手机 APP 里的分享链接
- 电脑浏览器里的分享链接
- 真实浏览器导出的 Cookie
做了端到端验证,并且真的在
downloads/ 目录下看到了下载出来的文件。3. 我在这个过程中真正学到的关键点
这里我不想只写“技术栈”,更想先写我学到的几个事实。
3.1 分享链接不是资源链接
这是最基本但也最容易被低估的一点。
用户给我的输入往往像这样:
真正有用的东西只有其中的 URL,而 URL 还只是一个短链。
所以第一步不是“下载”,而是“提取短链 + 解短链”。
3.2 真实作品页不一定有完整数据
我原本以为:
- 既然已经跳到
https://www.douyin.com/video/{id}
- 那这个页面里肯定有完整作品详情
但实际调试发现并不是。
有时
video 页里只有:- 路由参数
- 一部分壳页面
- 甚至没有目标作品详情对象
反而
discover?modal_id={aweme_id} 这种页面,有时候更容易拿到完整数据。这个结论不是看文档得来的,而是我一点点调页面调出来的。
3.3 页面里真正有价值的东西通常在 script 里
我一开始也会下意识想去解析页面文本、标题、DOM。
但做着做着就会发现,真正有用的信息,比如:
awemeId
groupId
authorInfo
video
playAddr
coverUrlList
这些通常不在普通 HTML 结构里,而在页面内嵌的结构化 JSON 里。
特别是
RENDER_DATA,它几乎是整个项目能不能成功的重要支点。3.4 不做目标匹配,拿到的数据很可能是错的
这是一个很容易掉坑的点。
页面里有时会有:
- 当前作品
- 推荐作品
- 相关推荐
- 其他资源块
如果不拿
aweme_id 去做严格匹配,很可能解析出来的根本不是用户想要的那条作品。所以我后面非常明确地把“目标作品匹配”作为解析器的核心逻辑之一。
3.5 “只下载视频”这个思路太窄了
最开始我把这个项目理解成“视频下载脚本”,所以一开始会天然偏向:
- 找到视频链接
- 选一个最像 mp4 的
- 下载
但后来你提出一个很重要的需求:
头像、图片、音频等也应该能下载,而且要能选择下载,不是只下载视频。
这一下就把思路彻底打开了。
从那之后,下载层不再只是“视频优选器”,而是变成了:
资源分类器 + 按类型下载器
这也是当前版本最关键的演化点之一。
4. 我最后采用的整体方案
我现在回头看,当前方案可以概括成一句话:
先把“作品定位”做好,再把“资源分类”做好,最后才谈下载。
整体流程
为什么我觉得这个流程是对的
因为它不是围绕某一个固定页面模板搭起来的,而是围绕“真实页面不稳定”这个事实搭起来的。
也就是说,这套方案的核心不是“某个接口成功了”,而是:
- 页面变了也有 fallback
- script 容器变了也还有其他候选
- 资源类型变了也有分类逻辑
这让它更像一个工程方案,而不是一个赌某个页面结构不变的小脚本。
5. 完整技术栈(结合我为什么这么选)
5.1 运行环境
- Windows
- Python 3.12.10
venv虚拟环境
5.2 第三方依赖
当前依赖非常少:
curl_cffi
这是整个项目里最关键的第三方库。
我用它的原因很直接:
- 抖音网页对浏览器行为敏感
- 普通请求方式不一定稳定
curl_cffi能模拟更像浏览器的请求行为
在这个项目里它负责:
- 跳转短链接
- 抓页面 HTML
- 下载资源文件
tenacity
这个库不是“核心业务库”,但很实用。
它主要用来给短链接跳转解析做重试,避免因为一次临时网络失败就整条链路挂掉。
typer
我用它来做 CLI。
原因也很朴素:
- 写法清晰
- 参数定义方便
- 帮助信息直观
现在
resolve / inspect / debug-page / download 都是基于 typer。pytest
当前项目已经不是那种“一边改一边祈祷别坏”的状态了,所以测试必须跟上。
pytest 现在主要帮我兜住:- URL 提取
aweme_id提取
- 页面结构化数据解析
- 资源分类
- 资源优选
5.3 标准库
虽然依赖少,但标准库用了不少:
re:正则提取与规则匹配
json:解析结构化数据
urllib.parse:处理 URL 和unquote
html:做 HTML 实体解码
pathlib:管理本地文件路径
dataclasses:数据模型
logging:日志
typing/collections.abc:类型标注
5.4 我为什么觉得这套技术栈够用
它的好处是:
- 依赖少
- 每个库职责清晰
- 适合快速试错与真实调试
- 目前没有过度设计
我现在很认可这套组合,因为它没有花哨,但确实把问题解决了。
6. 代码结构我是怎么理解的
我更愿意把当前结构理解成 4 层。
6.1 请求层:client.py
这一层负责:
- 创建浏览器风格 Session
- 注入 Cookie
- 跳转短链
- 抓页面 HTML
这一层的意义是:
先把“像浏览器一样请求页面”这件事做好。
没有这一层,后面很多页面根本拿不到完整状态。
6.2 解析层:parser.py
这一层负责:
- 从文案里提取 URL
- 从 URL 里提取
aweme_id
- 从 HTML 里提取 JSON
- 匹配目标作品对象
- 提取候选资源
这一层真正解决的问题是:
在结构不稳定的真实网页里,怎么尽可能稳定地拿到目标作品的数据。
6.3 流程层:service.py
这一层负责把:
- 请求
- 解析
- 资源选择
- 下载
串成完整流程。
它的价值在于把 CLI 和底层实现解耦。
6.4 下载层:downloader.py
这一层负责:
- 资源分类
- 资源优选
- 文件命名
- 文件下载
从“只下视频”升级到“按类型下载资源”,核心变化几乎都在这一层。
7. 我是怎么处理页面结构不稳定这件事的
这是整个项目里最真实的难点。
我最开始踩到的坑
我最开始会下意识觉得:
video/{id}页应该就能直接解析出详情
但事实不是这样。
有时候这个页面:
- 只有路由参数
- 没有完整作品对象
- 或者状态不完整
所以后面我引入了候选页面回退:
- 真实作品页 URL
discover?modal_id={aweme_id}
iesdouyin/share/video/{aweme_id}
这个决定非常关键。
RENDER_DATA 为什么成为主战场
因为我后来发现,最稳定的数据不在 DOM,而在 script 里。
其中
RENDER_DATA 尤其重要,因为它经常包含 URL 编码后的 JSON。所以解析器的工作方式变成了:
- 找 script
- 提内容
- 解码
- 解析 JSON
- 匹配目标作品
这一套下来,成功率就明显上来了。
8. 资源分类这件事,我最后是怎么收敛的
现在我对资源分类的理解很明确:
视频相关
direct_mp4
play_api
dash_video
unknown_video
非视频资源
audio
cover
avatar
image
为什么视频优先选 direct_mp4
因为它最像真正可直接落盘的视频文件。
当前顺序是:
direct_mp4
play_api
dash_video
unknown_video
为什么 all 模式不下载所有候选 URL
因为页面里的候选链接可能非常多。
如果真的全部下载,会产生:
- 大量重复
- 资源混乱
- 用户根本不知道哪个有用
所以我最后把
all 的语义收敛成:- 一个最佳视频
- 一个音频
- 一个封面
- 一个头像
也就是“按资源类型各取一个代表资源”。
这个决定是我觉得很实用的一步。
9. 当前 CLI 是怎么组织的
resolve
作用:
- 解析分享文案
- 输出真实 URL 和
aweme_id
inspect
作用:
- 抓元数据
- 看资源分组
- 看视频优选结果
debug-page
作用:
- 看某个页面是否命中关键标记:
RENDER_DATAplayAddrawemeIdauthorInfo
download
作用:
- 按类型下载资源
当前支持:
10. 真实验证这件事为什么很重要
我现在很强调“真实验证”,因为这类脚本最容易陷入一种错觉:
代码写出来了,看起来也合理,但其实没有在真实场景里跑过。
当前这个项目已经真实验证过:
- 手机 APP 分享链接
- 浏览器分享链接
- 浏览器 Cookie
- 视频下载
- 音频下载
- 封面下载
- 头像下载
all模式下载
这比单纯测试通过更有说服力。
11. 当前我认为最容易失效的地方
如果以后这个项目出问题,我最先会怀疑的是:
- 页面里的
RENDER_DATA结构变了
- 候选页面策略失效了
- Cookie 不再能让页面返回完整状态
- 资源 URL 规则变了
所以以后排查时,我会优先看:
resolve是否还能拿到真实作品页
aweme_id是否还对
debug-page是否还能看到关键标记
inspect是否还能命中目标作品
- 资源分组是否还合理
12. 当前已知局限
有几件事我认为需要明确记下来:
- 仍然依赖有效 Cookie
- 当前候选资源主要来自页面状态,不是完整官方详情接口体系
- 图集类作品还没有单独建模
image目前主要来自封面和页面图片候选,不等于完整图集下载
- 还没有做批量任务
这些都不影响当前方案可用,但属于后面继续演进时要面对的问题。
13. 当前最常用的命令
解析链接
查看资源信息
下载视频
下载音频
下载封面
下载头像
下载全部资源
Loading...
