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),它的核心策略就三步:
- 用移动端 UA 伪装;
- 调移动端 API 拿视频信息;
- 用特定的
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 天自动清理,既是给用户留取回时间,也是控制存储成本。
十六、踩过的坑
抖音这块的坑数不清,我按”先查什么”的顺序整理了一个清单:
- 先看 yt-dlp 是不是旧版本,抖音改接口后老版本必挂;
- 再看 UA 池里有没有过时的 UA,太老的会被特征识别;
- 再看 Cookie 格式,浏览器导出的是 JSON,yt-dlp 只认 Netscape;
- 最后看并发和频次,是不是把自己给封了。
- 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% |
| 中 | 80% |
这套数据来自我们生产环境近一个月的统计。抖音和小红书反爬严、成功率低,运营上要给用户说清楚预期;YouTube 和 B 站则适合作为主推体验的平台。
常见问题(FAQ)
Q1:为什么抖音反爬最严?
抖音商业化重,反爬是商业策略。
Q2:无 Cookie 失败怎么办?
用户自行提供 Cookie,平台加密存储。
Q3:Cookie 会被封吗?
合理使用不会。但同一 IP 大量切换账号会被风控。