抖音视频解析器成功返回 URL,但下载全部 404——iesdouyin 分享页 play_addr 死链排查
自建了一套抖音视频无水印下载服务。架构很经典:抓 iesdouyin.com/share/video/{id}/ 的 HTML,从 window._ROUTER_DATA 里提取视频元数据,再从 item_list[0].video.play_addr.url_list[0] 拿播放地址,把 playwm 改成 play、720p 改成 1080p,丢给 /download 代理端点去实际拉流。
上线初期运行正常。直到某天用户开始反馈「点下载没反应」,日志里满屏:
{"message":"404 Client Error: Not Found for url: https://aweme.snssdk.com/aweme/v1/play/?video_id=...&ratio=1080p&line=0","status":"error"}
诡异的是 /api/douyin/info(只返回元数据,不实际拉流)还是 status: "ok",看起来一切正常。问题只出在 /download 这条真正去取视频的路径上。
现象
/info接口正常,返回标题、封面、video_id、video_720p/video_1080pURL- 所有 URL 形如
https://aweme.snssdk.com/aweme/v1/play/?video_id=v03033g10000xxxx&ratio=1080p&line=0 - curl 任意一条 URL,不管 720p 还是 1080p、有没有登录 cookie、换 UA——全部
HTTP 404,空 body,无Location跳转 - 用 Python
requests.get(...).raise_for_status()捕获到异常,冒泡到用户界面
一句话确诊脚本:
import requests
s = requests.Session()
s.headers.update({
"User-Agent": "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
})
s.get("https://www.douyin.com/", timeout=10)
VID = "v03033g10000d86o1jvog65jir8ssb50" # 换成你手上的 video_id
for url in [
f"https://aweme.snssdk.com/aweme/v1/play/?video_id={VID}&ratio=1080p&line=0",
f"https://aweme.snssdk.com/aweme/v1/play/?video_id={VID}&ratio=720p&line=0",
f"https://aweme.snssdk.com/aweme/v1/playwm/?video_id={VID}&ratio=720p&line=0",
]:
r = s.get(url, allow_redirects=False, timeout=10)
print(url[:80], "->", r.status_code, "Location=", r.headers.get("Location", ""))三条全是 404、Location 为空——诊断成立。
根因
aweme.snssdk.com/aweme/v1/play(wm)/ 是字节跳动的老 App CDN 路径,已于 2024 年起逐步下线,2025 年后基本全面失效。
问题是 iesdouyin.com/share/video/{id}/ 的 _ROUTER_DATA 还在往 play_addr.url_list 里塞这些旧地址。解析流程完全正常——HTML 能抓、JSON 能解,标题/封面/hashtag 全都对——但 play_addr 字段已经是一个蜜罐:对未签名爬虫只返回永久 404 的死链。
抖音的真实播放地址已迁移到:
v3-default.douyinvod.com/...v9-dy.toutiaocdn.com/...v95-zjbdcdn.douyincdn.com/...
这些新 CDN URL 需要配套 a_bogus(X-Bogus)请求签名才能访问,而签名算法在 JS 里做了混淆,且定期更换。
/info 接口成功返回、URL 格式完全合法——这会让你以为是 URL 拼写 bug,反复折腾 play/playwm、720p/1080p、加减 line=0、换 UA、加 cookie,全部无效。
真正的问题不在你的 URL 改写逻辑,而是上游数据源已经把死链当活链分发了。任何在 aweme.snssdk.com 域名上的调参都是白费。
不要浪费时间在这些上
以下操作一条都不会让下载恢复:
playwm↔play互换ratio=720p↔ratio=1080p互换- 去掉
line=0 - 换 desktop/mobile/TikTok app UA
- 带上 douyin.com session cookie(2025 起 cookie-issuing step 是 JS 门控,
requests.Session拿不到有效 cookie) - 换
api.amemv.com/api5-normal-c-lf.amemv.com(同一退役家族,也是 404) - 加重试 / 延长超时 / 换代理 IP
正解
三档应对方案,按耐久度排序:
方案一(推荐):换维护中的开源解析库
接入 Evil0ctal/Douyin_TikTok_Download_API。它直接打 https://www.douyin.com/aweme/v1/web/aweme/detail/?aweme_id={id},携带正确的 a_bogus 签名,返回的 play_addr 是新 CDN 上真正可用的地址。
方案二(自研、高维护成本)
逆向当前 a_bogus JS 签名,起一个 Node sidecar 负责签名、主服务负责调 detail API。预计每隔几个月签名算法就会轮换一次,维护负担不低。
方案三(止血用,不治本)
在 /info 返回 URL 之前,先对每条 URL 做一次 HEAD 请求。如果 404,直接返回结构化错误而不是让客户端拿到死链:
import requests
def probe_url(url: str) -> bool:
try:
r = requests.head(url, timeout=5, allow_redirects=False)
return r.status_code == 200
except Exception:
return False
# 在 /info 响应里加 liveness 检测
if not probe_url(video_1080p):
return {
"status": "error",
"code": "DOUYIN_CDN_DEAD",
"message": "抖音 CDN 端点已下线,请等待解析器升级"
}放弃 iesdouyin 分享页作为播放 URL 的数据源。元数据(标题、封面、video_id)可以继续从这里取,但 play_addr 字段要通过能正确签名的 detail API 另行获取。短期止血可在 /info 加 HEAD 探活,明确告知客户端失效原因,避免 raw requests 异常字符串直接暴露给用户。
验证切换成功的命令:
# 新解析出的 URL 不应再出现 aweme.snssdk.com
curl -sI '<新 video_1080p URL>' | head -1
# 期望:HTTP/2 200一句话外卖
元数据 API 返回 success、URL 格式合法——这不代表 URL 背后的资源还活着;当所有参数变体对所有客户端身份都 404 时,问题一定在数据源,而不在你的拼参逻辑。