开发了个抖音资源下载CLI工具,思路笔记(含源码分享)

开发了个抖音资源下载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. 我最开始对这个需求的理解

刚开始看这个需求时,表面上像是:
  1. 用户给一个抖音分享链接
  1. 脚本请求一下
  1. 拿到视频地址
  1. 下载完事
但真正做起来才发现,事情没这么简单。
真实输入并不是一个干净的 mp4 链接,而往往是:
  • 一整段抖音分享文案
  • 中间夹着一个 v.douyin.com 的短链
  • 短链跳转之后,页面也不一定直接带完整作品信息
  • 页面里的资源不只有视频,还有音频、封面、头像、图片
所以这个问题本质上不是“下载一个文件”,而是:
如何从真实世界里的抖音分享文案出发,稳定定位到目标作品,再把页面里的各种资源提取出来,并按需下载。

2. 当前这个项目已经做到什么程度了

现在这个脚本已经不只是“能解析链接”,而是已经能真实跑通整条链路。

当前已经成功实现的能力

  1. 从一整段分享文案中提取短链接
  1. 解析短链接跳转,拿到真实作品页
  1. 提取目标作品 aweme_id
  1. 尝试多个候选页面来源
  1. 从页面结构化数据里匹配目标作品对象
  1. 提取候选资源
  1. 按资源类型下载到本地

当前已经验证成功的资源类型

  • 视频 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 我为什么觉得这套技术栈够用

它的好处是:
  1. 依赖少
  1. 每个库职责清晰
  1. 适合快速试错与真实调试
  1. 目前没有过度设计
我现在很认可这套组合,因为它没有花哨,但确实把问题解决了。

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} 页应该就能直接解析出详情
但事实不是这样。
有时候这个页面:
  • 只有路由参数
  • 没有完整作品对象
  • 或者状态不完整
所以后面我引入了候选页面回退:
  1. 真实作品页 URL
  1. discover?modal_id={aweme_id}
  1. iesdouyin/share/video/{aweme_id}
这个决定非常关键。

RENDER_DATA 为什么成为主战场

因为我后来发现,最稳定的数据不在 DOM,而在 script 里。
其中 RENDER_DATA 尤其重要,因为它经常包含 URL 编码后的 JSON。
所以解析器的工作方式变成了:
  1. 找 script
  1. 提内容
  1. 解码
  1. 解析 JSON
  1. 匹配目标作品
这一套下来,成功率就明显上来了。

8. 资源分类这件事,我最后是怎么收敛的

现在我对资源分类的理解很明确:

视频相关

  • direct_mp4
  • play_api
  • dash_video
  • unknown_video

非视频资源

  • audio
  • cover
  • avatar
  • image

为什么视频优先选 direct_mp4

因为它最像真正可直接落盘的视频文件。
当前顺序是:
  1. direct_mp4
  1. play_api
  1. dash_video
  1. unknown_video

为什么 all 模式不下载所有候选 URL

因为页面里的候选链接可能非常多。
如果真的全部下载,会产生:
  • 大量重复
  • 资源混乱
  • 用户根本不知道哪个有用
所以我最后把 all 的语义收敛成:
  • 一个最佳视频
  • 一个音频
  • 一个封面
  • 一个头像
也就是“按资源类型各取一个代表资源”。
这个决定是我觉得很实用的一步。

9. 当前 CLI 是怎么组织的

resolve

作用:
  • 解析分享文案
  • 输出真实 URL 和 aweme_id

inspect

作用:
  • 抓元数据
  • 看资源分组
  • 看视频优选结果

debug-page

作用:
  • 看某个页面是否命中关键标记:
    • RENDER_DATA
    • playAddr
    • awemeId
    • authorInfo

download

作用:
  • 按类型下载资源
当前支持:

10. 真实验证这件事为什么很重要

我现在很强调“真实验证”,因为这类脚本最容易陷入一种错觉:
代码写出来了,看起来也合理,但其实没有在真实场景里跑过。
当前这个项目已经真实验证过:
  • 手机 APP 分享链接
  • 浏览器分享链接
  • 浏览器 Cookie
  • 视频下载
  • 音频下载
  • 封面下载
  • 头像下载
  • all 模式下载
这比单纯测试通过更有说服力。

11. 当前我认为最容易失效的地方

如果以后这个项目出问题,我最先会怀疑的是:
  1. 页面里的 RENDER_DATA 结构变了
  1. 候选页面策略失效了
  1. Cookie 不再能让页面返回完整状态
  1. 资源 URL 规则变了
所以以后排查时,我会优先看:
  1. resolve 是否还能拿到真实作品页
  1. aweme_id 是否还对
  1. debug-page 是否还能看到关键标记
  1. inspect 是否还能命中目标作品
  1. 资源分组是否还合理

12. 当前已知局限

有几件事我认为需要明确记下来:
  1. 仍然依赖有效 Cookie
  1. 当前候选资源主要来自页面状态,不是完整官方详情接口体系
  1. 图集类作品还没有单独建模
  1. image 目前主要来自封面和页面图片候选,不等于完整图集下载
  1. 还没有做批量任务
这些都不影响当前方案可用,但属于后面继续演进时要面对的问题。

13. 当前最常用的命令

解析链接

查看资源信息

下载视频

下载音频

下载封面

下载头像

下载全部资源


推荐云服务

雨云 - 云服务器首选

稳定 · 高速 · 性价比超高

使用优惠码立享折扣,开启你的云端之旅~

一元试用秒级开通24h在线客服

优惠码

zqf
立即访问
Loading...
灵心小窝

灵心小窝

这里不是一个喧闹的地方,只是用来存放一些还不想遗忘的东西。

声明 © 2026 早清风
加载中...