抖音无 Cookie 下载方案(AI 视频下载总结器的反爬适配)

PC 网页端几乎强制登录、滑块验证和签名校验一环接一环——抖音在用户量占比不小的情况下,成了整个下载器里投入精力最多的平台。用户量占比不小,但 PC 网页端几乎强制登录,滑块验证、签名校验一环接一环,拿不到登录态就只能干瞪眼。我翻了 yt-dlp 的 douyin extractor 源码,试出”移动端 UA + 特定 API 参数”这条不用 Cookie 的路,把下载成功率稳定在七成上下。这篇把抖音的反爬机制、无 Cookie 方案的具体参数、失效降级策略和合规边界完整写清楚,给同样被卡住的人一点参考。

一、抖音反爬机制

要绕开反爬,得先知道它堵在哪。抖音 PC 端的请求流程大致是这样:

浏览器打开 douyin.com
    ↓
检测 UA,是 PC 则跳转到 www.douyin.com(Web 版)
    ↓
Web 版要求登录/滑块验证
    ↓
登录后拿到 cookie 才能下视频

PC 端 Web 版基本强制登录,但移动端 API 路径(/aweme/v1/web/...)可以匿名访问。

这个流程里真正的突破口是第二行:抖音靠 UA 区分客户端。桌面浏览器走 Web 版,一堆验证伺候着;移动端路径当年留了匿名访问的口子。拿一个真实 iPhone 的 UA 伪装过去,就走进了相对宽松的通道。这条路不是永远有效,抖音迟早会收紧,所以后文才要准备降级方案。

二、yt-dlp 内部策略

yt-dlp 为抖音专门维护了一个 extractor(youtube_dl/extractor/douyin.py),它的核心策略就三步:

  1. 用移动端 UA 伪装;
  2. 调移动端 API 拿视频信息;
  3. 用特定的 app_name / aid 参数。

对应的 yt-dlp 配置长这样:

ydl_opts = {
    'http_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'
    },
    'extractor_args': {
        'douyin': {
            'app_name': 'douyin_web',
            'aid': '6383'
        }
    }
}

aid=6383 是抖音 Web 版的固定应用 ID。

三个参数各有讲究:UA 负责伪装客户端身份;app_name 声明走哪个应用通道;aid 是抖音 Web 版的固定应用 ID,写死在请求里。我试过只换 UA 不带 aid,部分视频能过,但稳定性差一截,三个参数齐了成功率才上去。

三、平台封装的实现

直接把这套参数写死在业务代码里不行,我封装进下载器,用 Android 的 UA 作为默认值:

# services/downloader.py
class VideoDownloader:
    DOUYIN_UA = ('Mozilla/5.0 (Linux; Android 13; Pixel 7) '
                 'AppleWebKit/537.36 (KHTML, like Gecko) '
                 'Chrome/116.0.0.0 Mobile Safari/537.36')
    
    def _douyin_opts(self) -> dict:
        return {
            **self._base_opts,
            'format': 'best',
            'http_headers': {
                'User-Agent': self.DOUYIN_UA,
                'Referer': 'https://www.douyin.com/',
                'Accept-Language': 'zh-CN,zh;q=0.9'
            },
            'extractor_args': {
                'douyin': {
                    'app_name': 'douyin_web',
                    'aid': '6383',
                }
            }
        }

我比 yt-dlp 默认配置多补了 Referer 和 Accept-Language 两个头,实测对成功率有正向影响。封装的好处是:抖音哪天真把这条路堵了,我只改这一个方法,其他平台不受影响。

四、Cookie 模式 vs 无 Cookie 模式

无 Cookie 是默认,但有些内容必须登录才能看,所以我也保留了 Cookie 入口,做成双模式:

1. 无 Cookie(默认)

async def get_info(self, url: str, cookies: str = None):
    opts = self._douyin_opts()
    if cookies:
        opts['cookiefile'] = cookies
    # 否则用无 Cookie 选项

适用:未登录用户

限制:部分视频因版权或地区限制无法下载。

2. Cookie 模式

用户提供 cookie 后能下:

  • 私密账号视频;
  • 需登录的”仅好友可见”内容;
  • 会员专享视频。

两个模式共用同一个方法,多一个 cookies 参数就切过去。产品上我把 Cookie 入口藏得比较深,避免引导用户做高风险操作,只有当无 Cookie 失败时才提示。

五、链接格式识别

抖音的链接有好几种长相,分享出来尤其喜欢用短链:

https://www.douyin.com/video/7234567890123456789
https://v.douyin.com/iJ5k2q3a/  (短链)
https://www.douyin.com/discover?modal_id=7234567890123456789

短链要先展开:

async def _expand_short_url(self, short_url: str) -> str:
    """展开抖音短链"""
    async with httpx.AsyncClient(follow_redirects=True) as client:
        r = await client.get(short_url, headers={
            'User-Agent': self.DOUYIN_UA
        })
        return str(r.url)

短链展开必须带上移动端 UA,否则服务端会按桌面请求处理,返回的可能不是期望的落地页。还有一种 discover?modal_id= 的写法,里边的视频 ID 要单独抽出来拼成标准链接,这些格式我在 _detect_platform 里都做了归一化。

六、API 响应处理

抖音返回的元数据是嵌套 JSON,视频地址、封面、作者都埋在很深的位置:

{
    "aweme_list": [{
        "aweme_id": "7234567890123456789",
        "desc": "视频标题",
        "duration": 15000,
        "video": {
            "play_addr": {
                "url_list": [
                    "https://v26-cold.douyinvod.com/.../video.mp4"
                ]
            },
            "cover": {
                "url_list": [
                    "https://p3-sign.douyinpic.com/.../cover.jpeg"
                ]
            },
            "bit_rate": [{
                "bit_rate": 1000000,
                "play_addr": {...}
            }]
        },
        "author": {
            "nickname": "作者名"
        }
    }]
}

yt-dlp 自动解析这个结构。

这段 JSON 里藏了两个关键字段:play_addr.url_list 是视频直链,cover.url_list 是封面图。直链是 CDN 的临时地址,过两三个小时就失效,所以下载必须紧随解析之后立即执行,这是踩坑清单里的一条。

七、限流处理

抖音对高频请求盯得很紧,我写了个简单的限流器,每个 IP 每秒最多发一个请求:

# 限流:每个 IP 每秒最多 1 个请求
class RateLimiter:
    def __init__(self, rate: float = 1.0):
        self.interval = 1.0 / rate
        self.last_call = 0
    
    async def acquire(self):
        now = time.time()
        wait = self.last_call + self.interval - now
        if wait > 0:
            await asyncio.sleep(wait)
        self.last_call = time.time()

douyin_limiter = RateLimiter(rate=0.5)  # 0.5 req/s

async def get_info(self, url: str):
    if 'douyin' in url:
        await douyin_limiter.acquire()
    # ... 调用 yt-dlp

我实际把频率压到 0.5 次/秒,比 1 次/秒更稳。限流牺牲了一点吞吐,换来的是 IP 活得更久,对生产环境来说是划算的交易。等以后账号多了,可以按账号维度再做一层。

八、地区限制

部分视频标注”仅限大陆播放”,海外用户点了就白等:

def _is_china_only(info: dict) -> bool:
    region = info.get('region', '')
    return region in ('CN', ' mainland_china')

平台对大陆用户加白名单:

if self._is_china_only(info) and not self._is_cn_user():
    raise DownloadError('该视频仅限大陆地区')

这里有个产品决策:我们没有用代理去绕地区限制,而是直接对非大陆用户提示”仅限大陆地区”。原因很简单,绕地区限制的法律风险不值得为一个视频承担,合规红线不能碰。

九、备用方案

无 Cookie 方案随时可能失效,我写了三级降级链路:

async def get_info_with_fallback(self, url: str):
    # 1) 优先无 Cookie
    try:
        return await self.get_info(url, cookies=None)
    except (NetworkError, VideoUnavailable) as e:
        log.warning('无 Cookie 失败: {}', e)
    
    # 2) 备用:爬虫模拟
    try:
        return await self._crawler_scrape(url)
    except Exception as e:
        log.warning('爬虫失败: {}', e)
    
    # 3) 提示用户提供 Cookie
    raise NeedCookieError('请提供抖音 Cookie')

第一级失败就换第二级的页面解析,再不行就请用户提供 Cookie。每一级都打日志,方便我判断是平台临时风控还是方案整体失效——如果日志里大量走到第三级,就该去升级 yt-dlp 或调整参数了。

十、Cookie 加密存储

用户提供的 Cookie 是敏感数据,明文落库等于裸奔,我用 Fernet 对称加密后入库:

from cryptography.fernet import Fernet

class CookieManager:
    def __init__(self):
        self.cipher = Fernet(settings.COOKIE_ENCRYPT_KEY.encode())
    
    def encrypt(self, cookie: str) -> str:
        return self.cipher.encrypt(cookie.encode()).decode()
    
    def decrypt(self, encrypted: str) -> str:
        return self.cipher.decrypt(encrypted.encode()).decode()

存数据库:

cookie_record = UserCookie(
    user_id=user.id,
    platform='douyin',
    encrypted=self.cipher.encrypt(cookie),
    expires_at=datetime.now() + timedelta(days=30)
)
db.add(cookie_record)

加密之外,我还设了 30 天有效期,过期自动失效并要求用户重新提供。数据库字段的权限也收紧了,后端代码里只有 CookieManager 能接触解密后的值,日志和监控里一律脱敏。

十一、User-Agent 池

单 UA 用久了容易被特征识别,我维护了一个 UA 池轮换:

UA_POOL = [
    'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) ...',
    'Mozilla/5.0 (Linux; Android 13; SM-S908B) ...',
    'Mozilla/5.0 (Linux; Android 13; Pixel 7) ...',
    'Mozilla/5.0 (Linux; Android 12; M2102J20SG) ...',
]

def _get_random_ua(self) -> str:
    return random.choice(UA_POOL)

池子里我刻意混合了 iPhone 和不同品牌 Android 机型,模拟真实用户分布。这个池子要定期维护,UA 版本太老一样会被识别,我把它列进了月度巡检清单。

十二、IP 代理池

抖音风控收紧时,单一 IP 很快会被限流甚至封禁。代理池作为最后一道手段:

# config.py
PROXY_POOL = [
    'http://proxy1:port',
    'http://proxy2:port',
    # ...
]

def _get_proxy(self) -> Optional[str]:
    if settings.USE_PROXY:
        return random.choice(PROXY_POOL)
    return None
ydl_opts = {
    'proxy': self._get_proxy(),
    # ...
}

代理池我没默认开启,只在 USE_PROXY 开关打开时生效。因为代理质量参差不齐,反而可能拉低成功率,这属于”备而不用”的配置,遇到大规模风控时才切。

十三、签名算法绕过

新版抖音的请求带 _signature 参数,是一种防爬签名。yt-dlp 维护团队会持续更新提取器来适配,我要做的只有一件事——跟着升版本:

# requirements.txt
yt-dlp>=2024.5.27  # 锁定最低版本

每月升级一次 yt-dlp 拉新版。

升级节奏我定成每月一次,太频繁怕引入回归,太慢怕签名失效。每次升级前先在测试环境跑一轮各平台下载用例,通过再上生产。

十四、用户引导

方案失效时,用户不该对着报错码发呆。我做了清晰的引导文案:

⚠️ 抖音视频下载失败

可能原因:
1. 视频已删除
2. 视频仅限登录用户
3. 触发抖音风控

建议:
- 在「设置 → 抖音 Cookie」粘贴你的浏览器 Cookie
- 或换个视频试试

把”为什么失败、下一步怎么办”一次说清楚,客服工单量肉眼可见地下降。这个经验后来也复制到了其他平台的错误提示里。

十五、合规边界

做下载工具容易踩红线,我在产品里明确了四条底线:

  • 不破解付费内容;
  • 不绕过登录验证(用户主动提供 cookie 视为授权);
  • 不存储视频超过 7 天(自动清理);
  • 不公开分享用户 Cookie。

这四条写进产品文档和代码注释,是团队共识。视频文件 7 天自动清理,既是给用户留取回时间,也是控制存储成本。

十六、踩过的坑

抖音这块的坑数不清,我按”先查什么”的顺序整理了一个清单:

  1. 先看 yt-dlp 是不是旧版本,抖音改接口后老版本必挂;
  2. 再看 UA 池里有没有过时的 UA,太老的会被特征识别;
  3. 再看 Cookie 格式,浏览器导出的是 JSON,yt-dlp 只认 Netscape;
  4. 最后看并发和频次,是不是把自己给封了。
  • yt-dlp 升级导致 API 失效:抖音改接口后老 yt-dlp 失败。每月升级。
  • UA 太老被识别:保持 UA 池更新。
  • Cookie 格式错误:浏览器导出的 cookie 是 JSON 格式,yt-dlp 要 Netscape 格式。
  • 频次过高被封:加 Semaphore + 限流。
  • SSL 证书问题:抖音 API 偶尔 SSL 异常。配置 nocheckcertificate: True 兜底。
  • 下载链接过期:抖音的 CDN 链接 2-4 小时过期,必须立即下载。
  • B 站 Referer 缺失:必须 Referer: https://www.bilibili.com。

其中”下载链接过期”最隐蔽:解析完拖一会儿再下载,直链就失效了,重试也没用。后来我在流程上强制解析后立即下载,中间不停留。

十七、监控

没有监控的反爬适配等于闭眼开车。我在下载链路埋了指标:

metrics.counter('downloader.douyin', 
    'result', 'success').increment()
metrics.counter('downloader.douyin', 
    'result', 'fail', 'reason', 'rate_limit').increment()
metrics.counter('downloader.douyin', 
    'result', 'fail', 'reason', 'need_cookie').increment()

Grafana 看板盯成功率、Cookie 使用率。

我重点盯两个数字:无 Cookie 成功率和 need_cookie 占比。前者掉了 10 个点就启动预案,后者持续升高说明方案在失效,该让用户多提供 Cookie 或升级策略了。

十八、抖音 vs 其他平台

最后把各平台的反爬难度放在一起看,方便做资源投入的决策:

平台 反爬严度 无 Cookie 可下率
YouTube 中 90%+
B 站 中 85%+
抖音 高 70%
小红书 高 60%
Twitter 中 80%

这套数据来自我们生产环境近一个月的统计。抖音和小红书反爬严、成功率低,运营上要给用户说清楚预期;YouTube 和 B 站则适合作为主推体验的平台。

常见问题(FAQ)

Q1:为什么抖音反爬最严?

抖音商业化重,反爬是商业策略。

Q2:无 Cookie 失败怎么办?

用户自行提供 Cookie,平台加密存储。

Q3:Cookie 会被封吗?

合理使用不会。但同一 IP 大量切换账号会被风控。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
其AI的头像其AI普通用户

相关推荐

返回顶部