提示

返回博客列表

抖音和快手无水印下载——三种技术路线的完整对比与选型指南

抖音和快手无水印下载——三种技术路线的完整对比与选型指南

抖音和快手的视频下载,可能是所有平台里需求最大、方案最杂的。市面上有几十种"抖音去水印"工具,但它们的原理无非三种路线:网页解析(抓页面直链)、手机抓包(Hook App 的 HTTPS 请求)、API 逆向(还原官方接口)。每种路线各有优劣——有的维护成本低但不稳定,有的稳定但门槛高。这篇文章把三条路线的原理、代码实现、维护成本和适用场景全部摊开对比,让你根据自己的需求选对方案。

TL;DR:三条路线对比——① 网页解析:最简单,Python + requests 抓分享页获取 video_id → 调官方接口 → 拼接无水印直链。门槛最低但平台更新就失效。② 手机抓包:用 mitmproxy + 手机代理拦截 App 的视频请求。最稳定(直接拿到 CDN 地址)但需要手机配合。③ API 逆向:还原 App 的签名算法(X-Gorgon/X-Argus/X-Ladon),用纯代码请求。最可靠但门槛最高。推荐策略:先用路线①快速验证,失效后升级到路线②或③。

目录

一、抖音和快手的视频 ID 与分享链接

1.1 链接形式

抖音的几种链接形式:
  https://v.douyin.com/xxxxx/          ← 短链接(分享出来是这个)
  https://www.douyin.com/video/123456789  ← 长链接(跳转后)
  https://www.iesdouyin.com/share/video/123456789/  ← 网页版

快手的几种链接形式:
  https://v.kuaishou.com/xxxxx         ← 短链接
  https://www.kuaishou.com/short-video/xxx  ← 长链接

第一步永远是获取真实 video_id。 短链接需要先跟一次重定向拿到长链接:

import requests
import re

def resolve_short_link(url):
    """短链接 → 长链接 + video_id"""
    session = requests.Session()
    session.headers['User-Agent'] = (
        'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) '
        'AppleWebKit/605.1.15 (KHTML, like Gecko) '
        'Version/16.0 Mobile/15E148 Safari/604.1'
    )

    # 禁止自动重定向,手动获取
    resp = session.get(url, allow_redirects=False, timeout=10)

    # 从重定向的 Location 中提取长链接
    location = resp.headers.get('Location', '')
    if location:
        return location

    # 如果没有重定向,从 HTML 中提取
    match = re.search(r'href="([^"]+video/(\d+)[^"]*)"', resp.text)
    if match:
        return match.group(1), match.group(2)

    raise ValueError("无法解析短链接")

注意 User-Agent:抖音的短链接服务会根据 UA 返回不同的跳转目标。用桌面端 UA 会跳到 douyin.com,用移动端 UA 会跳到 iesdouyin.com。后者通常更容易解析。

二、路线一:网页解析——最简单但不稳定

2.1 原理

抖音网页版(douyin.com)的分享页内嵌了一个 JSON 数据块,包含了无水印视频的直链。你不需要逆向任何签名——直接抓 HTML 然后正则提取。

流程:
1. 访问 https://www.douyin.com/video/{video_id}
2. 在 HTML 中搜索 <script id="RENDER_DATA">
3. URL decode → JSON → 提取视频地址

2.2 代码实现

import requests
import re
import json
from urllib.parse import unquote


def douyin_web_parse(url):
    """路线一:网页解析获取抖音无水印视频"""
    session = requests.Session()
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/120.0.0.0 Safari/537.36',
        'Referer': 'https://www.douyin.com/',
        'Cookie': '',  # 可选,登录后能获取更高质量
    })

    # 1. 如果是短链接,先解析
    if 'v.douyin.com' in url:
        url = resolve_short_link(url)

    # 2. 提取 video_id
    match = re.search(r'/video/(\d+)', url)
    if not match:
        raise ValueError("无法提取 video_id")
    video_id = match.group(1)

    # 3. 访问视频页面
    resp = session.get(f'https://www.douyin.com/video/{video_id}')
    html = resp.text

    # 4. 提取 RENDER_DATA(抖音把视频数据藏在这里)
    match = re.search(r'<script id="RENDER_DATA"[^>]*>(.*?)</script>', html)
    if not match:
        raise Exception("未找到 RENDER_DATA(页面可能改版)")

    # URL decode
    raw = unquote(match.group(1))
    data = json.loads(raw)

    # 5. 从嵌套的 JSON 中提取无水印地址
    # RENDER_DATA 的结构很深,需要逐层穿透
    try:
        # 路径:app → videoInfoRes → item_list[0] → video → play_addr
        app_data = data.get('app', {})
        # 实际路径取决于抖音当前的版本,以下是常见路径
        video_info = _deep_get(data, 'app', 'videoInfoRes', 'item_list')
        if not video_info:
            video_info = _deep_get(data, 'app', 'defaultFeedData')
            if video_info:
                video_info = video_info[0]

        if video_info:
            video_data = video_info[0] if isinstance(video_info, list) else video_info
            video = video_data.get('video', {})

            # 无水印地址(替换 watermark=1 为 watermark=0)
            play_addr = video.get('play_addr', {})
            url_list = play_addr.get('url_list', [])

            if url_list:
                raw_url = url_list[0]
                # 去掉水印:把 playwm 换成 play,或者替换 watermark 参数
                clean_url = raw_url.replace('playwm', 'play')
                clean_url = re.sub(r'watermark=1', 'watermark=0', clean_url)
                return clean_url

    except Exception as e:
        raise Exception(f"解析失败: {e}")

    raise Exception("未找到视频地址")


def _deep_get(d, *keys):
    """安全地从嵌套字典中取值"""
    for key in keys:
        if isinstance(d, dict):
            d = d.get(key, {})
        elif isinstance(d, list) and d:
            d = d[0]
        else:
            return None
    return d if d else None

2.3 优缺点

优点:
  ✅ 不需要手机,纯代码搞定
  ✅ 不需要逆向签名算法
  ✅ 代码量少,几十行就能跑

缺点:
  ❌ 抖音改版就会失效(RENDER_DATA 结构经常变)
  ❌ 部分视频需要登录才能获取无水印地址
  ❌ 无法获取私密视频或好友可见视频
  ❌ 对快手支持较差(快手网页版数据更隐蔽)

三、路线二:手机抓包——最稳定但需要设备

3.1 原理

这条路线不解析网页,而是直接在手机和服务器之间"偷看"通信。抖音 App 在播放视频时,一定会向 CDN 请求 .mp4 文件——拦截这个请求,拿到 URL。

流程:
1. 电脑上启动 mitmproxy(HTTPS 代理)
2. 手机设置代理 → 安装 mitmproxy 的 CA 证书
3. 手机上打开抖音 → 播放目标视频
4. mitmproxy 拦截到视频 CDN 请求 → 提取 URL
5. URL 可以直接用(不需要 Cookie,CDN 只验证 Referer)

3.2 自动抓包脚本

# capture_douyin.py
# 用 mitmproxy 的 Python API 写一个自动拦截脚本
# 运行:mitmdump -s capture_douyin.py

from mitmproxy import http
import re

VIDEO_PATTERNS = [
    r'\.mp4\?',           # 抖音 CDN 视频
    r'v\d+-cs\.',         # 抖音备用 CDN
    r'tos-',              # 字节跳动 CDN
    r'ixigua\.com',       # 西瓜视频 CDN
    r'kwai\.com.*\.mp4',  # 快手 CDN
]

captured_urls = []


def response(flow: http.HTTPFlow):
    """拦截所有响应"""
    url = flow.request.pretty_url
    content_type = flow.response.headers.get('content-type', '')

    # 检查是否匹配视频特征
    is_video = (
        'video' in content_type or
        any(re.search(p, url) for p in VIDEO_PATTERNS)
    )

    if is_video:
        captured_urls.append({
            'url': url,
            'content_type': content_type,
            'size': flow.response.headers.get('content-length', 'unknown'),
            'referer': flow.request.headers.get('referer', ''),
        })
        print(f"[捕获] {url[:100]} ({content_type})")


def done():
    """脚本退出时输出所有捕获的 URL"""
    print(f"\n共捕获 {len(captured_urls)} 个视频地址:")
    for i, v in enumerate(captured_urls):
        print(f"\n{i+1}. {v['url']}")
        print(f"   Content-Type: {v['content_type']}")
        print(f"   Size: {v['size']}")

3.3 优缺点

优点:
  ✅ 最稳定——直接拿到 CDN 地址,不受前端改版影响
  ✅ 无水印——App 请求的就是无水印版本
  ✅ 可以抓取需要登录的内容(手机已登录)
  ✅ 对抖音和快手都有效

缺点:
  ❌ 需要一台手机(或安卓模拟器)
  ❌ 需要配置 HTTPS 代理和安装证书(对普通用户有门槛)
  ❌ 抖音/快手可能检测代理并拒绝连接(需要额外对抗)
  ❌ 自动化程度低(需要手动播放视频)

3.4 安卓模拟器替代真机

如果不想用真机,可以用安卓模拟器:

1. 安装 MuMu 模拟器 或 雷电模拟器
2. 模拟器中安装抖音 + 目标版本(有些版本更容易抓包)
3. 模拟器设置代理 → 指向宿主机的 mitmproxy 端口
4. 安装证书:模拟器浏览器访问 mitm.it → 下载 Android 证书
5. 后续流程和真机一样

四、路线三:API 逆向——最可靠但门槛最高

4.1 原理

抖音 App 的所有请求都带三个自定义请求头:

X-Gorgon: 840400600000019ba8c8...   ← 请求签名(最核心)
X-Argus: qwerty12345...              ← 设备指纹
X-Ladon: abcdef...                   ← 行为验证(部分接口需要)

这三个值由 App 内部的 Native 层(C/C++ 代码)生成,不是 JS。所以要逆向它们,需要: - 反编译抖音 APK → 定位签名函数所在的 .so 文件 - 用 IDA Pro / Ghidra 分析 Native 代码 - 还原算法或用 Frida Hook 动态调用

4.2 Frida Hook 方案(降低门槛)

与其完全逆向算法,不如用 Frida Hook 抖音 App 的签名函数,让 App 帮你生成签名:

// frida_hook.js - 注入到抖音 App
// 运行:frida -U -l frida_hook.js com.ss.android.ugc.aweme

Java.perform(function() {
    // Hook 抖音的网络请求库
    var Retrofit = Java.use('retrofit2.Retrofit');
    // ... 具体实现依赖于抖音版本

    // 更简单的方式:直接 Hook OkHttp 的拦截器
    var Interceptor = Java.use('okhttp3.Interceptor');
    // 打印所有请求的 URL 和响应
});

// 或者用更暴力的方式:Hook Native 签名函数
var gorgon_addr = Module.findExportByName(
    'libcms.so', 'generate_gorgon'
);
if (gorgon_addr) {
    Interceptor.attach(gorgon_addr, {
        onEnter: function(args) {
            console.log('Gorgon input:', args[0].readCString());
        },
        onLeave: function(retval) {
            console.log('Gorgon output:', retval.readCString());
        }
    });
}

4.3 半逆向方案:提取设备注册信息

抖音的 API 签名依赖设备注册——每个设备第一次启动时会向服务器注册,获取 device_idinstall_id 等。如果能提取一次注册信息,后续可以在代码中复用。

# 思路:用 Frida 一次性提取设备信息,然后纯代码请求
DEVICE_INFO = {
    'device_id': '12345678901234567',
    'install_id': '98765432109876543',
    'openudid': 'abcdef1234567890',
    'clientudid': '...',
    # 这些值从一台真机上提取,有效期很长(几个月)
}

4.4 优缺点

优点:
  ✅ 完全自动化,不需要手机参与
  ✅ 可以批量、高频请求
  ✅ 对抖音和快手都有效

缺点:
  ❌ 门槛最高(需要逆向 Native 代码或使用 Frida)
  ❌ 抖音每个版本可能更新签名算法
  ❌ 设备注册信息可能过期或被封
  ❌ 法律风险较高(逆向 App 可能违反 ToS 和 DMCA)

五、三条路线横向对比

            路线一(网页解析)    路线二(手机抓包)    路线三(API逆向)
───────────────────────────────────────────────────────────────
实现难度    ⭐(简单)           ⭐⭐(中等)          ⭐⭐⭐⭐(难)
维护成本    ⭐⭐⭐⭐(高)       ⭐(低)              ⭐⭐(中)
稳定性      ⭐⭐(经常失效)     ⭐⭐⭐⭐(最稳定)    ⭐⭐⭐(较稳定)
自动化程度  ⭐⭐⭐(全自动)     ⭐⭐(半自动)        ⭐⭐⭐(全自动)
设备要求    无                  需要手机/模拟器        需要逆向工具
无水印      ✅                   ✅                    ✅
需要登录    部分视频需要         不需要(手机已登录)    需要
法律风险    低                  低                    中-高
适用场景    个人临时下载         长期稳定使用           商业/批量下载

推荐策略

如果你是个人用户,偶尔下载几个视频:
  → 路线一(网页解析),坏了再说

如果你需要长期稳定下载(比如做内容搬运):
  → 路线二(手机抓包),配合模拟器自动化

如果你在做视频下载服务(需要批量、高频):
  → 路线三(API逆向),投入时间一次,长期受益

六、实战:一个支持多路线的下载器框架

class DouyinDownloader:
    """支持多路线的抖音下载器"""

    def __init__(self, method='web', **kwargs):
        self.method = method
        self.kwargs = kwargs
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                          'AppleWebKit/537.36'
        })

    def download(self, url, output_path='./douyin_video.mp4'):
        """主入口"""
        if self.method == 'web':
            video_url = self._method_web(url)
        elif self.method == 'api':
            video_url = self._method_api(url)
        elif self.method == 'capture':
            video_url = self._method_capture(url)
        else:
            raise ValueError(f"未知方法: {self.method}")

        # 下载视频
        headers = {
            'Referer': 'https://www.douyin.com/',
            'User-Agent': self.session.headers['User-Agent'],
        }

        resp = self.session.get(video_url, headers=headers, stream=True)
        resp.raise_for_status()

        total = int(resp.headers.get('content-length', 0))
        downloaded = 0

        with open(output_path, 'wb') as f:
            for chunk in resp.iter_content(8192):
                f.write(chunk)
                downloaded += len(chunk)
                if total:
                    print(f'\r{downloaded/total*100:.1f}%', end='')

        print(f'\n完成: {output_path}')
        return output_path

    def _method_web(self, url):
        """路线一"""
        return douyin_web_parse(url)

    def _method_api(self, url):
        """路线三(需要实现签名算法)"""
        # 提取 video_id
        video_id = self._extract_video_id(url)

        # 调用抖音 API(需要 X-Gorgon 等签名头)
        headers = self._generate_signature_headers()
        resp = self.session.get(
            f'https://www.douyin.com/aweme/v1/web/aweme/detail/'
            f'?aweme_id={video_id}',
            headers=headers
        )
        data = resp.json()
        video_url = data['aweme_detail']['video']['play_addr']['url_list'][0]
        return video_url.replace('playwm', 'play')

    def _method_capture(self, url):
        """路线二(需要 mitmproxy 配合)"""
        # 从捕获文件或 API 读取最近的视频 URL
        raise NotImplementedError("需要 mitmproxy 配合")

    def _extract_video_id(self, url):
        match = re.search(r'/video/(\d+)', url)
        if match:
            return match.group(1)
        # 处理短链接
        if 'v.douyin.com' in url:
            resolved = resolve_short_link(url)
            return self._extract_video_id(resolved)
        raise ValueError(f"无法提取 video_id: {url}")

    def _generate_signature_headers(self):
        """生成 X-Gorgon 等签名头(需要实现)"""
        # 这是路线三的核心,需要逆向 App
        raise NotImplementedError("需要实现签名算法")

七、快手 vs 抖音的差异点

虽然思路类似,但快手有几个特殊之处:

差异点 抖音 快手
分享页格式 网页版完整展示 网页版高度简化,数据更隐蔽
无水印地址 替换 watermark=10 部分接口直接返回无水印地址
签名体系 X-Gorgon / X-Argus 自己的签名体系(不同算法)
CDN 特征 v\d+-cs / tos- kwai\.com.*\.mp4
图片/图文 视频和图文分开 快手有很多"图文"内容(不是视频)

快手网页版的 RENDER_DATA 比抖音更隐蔽,路线一(网页解析)对快手成功率较低。建议快手优先考虑路线二(抓包)。

八、合规与温馨提示

  • 本文所有技术讨论仅限个人学习研究和下载自己上传的已获授权的内容
  • 逆向 App(路线三)可能违反平台的用户协议和当地法律(如 DMCA 反规避条款),请谨慎
  • 抓包方案(路线二)属于网络调试的合法用途,但用于批量下载他人内容可能侵权
  • 抖音和快手的视频受版权保护,未经授权下载和再分发属于侵权
  • 更多讨论见 下载视频算侵权吗?聊聊个人备份与版权的那条线

抖音和快手的下载,三条路线我实际都用过。结论是:如果你只是偶尔下载几个视频自用,路线一(网页解析)足够了。 但如果你要做长期维护的下载工具,建议直接上路线二(抓包)——虽然前期要多花半小时配环境,但一旦跑通,三个月不用管。路线三(API 逆向)除非你是安全研究员或者要做商业产品,否则不太建议投入,投入产出比不高。

本文由 VidDown 技术博客原创发布。VidDown 的抖音/快手解析器结合了网页解析和 API 逆向两条路线——优先用网页解析快速响应,解析失败时自动降级到备用方案。访问 VidDown 了解更多。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部