返回博客

抖音视频解析器成功返回 URL,但下载全部 404——iesdouyin 分享页 play_addr 死链排查

自建了一套抖音视频无水印下载服务。架构很经典:抓 iesdouyin.com/share/video/{id}/ 的 HTML,从 window._ROUTER_DATA 里提取视频元数据,再从 item_list[0].video.play_addr.url_list[0] 拿播放地址,把 playwm 改成 play720p 改成 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_1080p URL
  • 所有 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", ""))

三条全是 404Location 为空——诊断成立。

根因

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_bogusX-Bogus)请求签名才能访问,而签名算法在 JS 里做了混淆,且定期更换。

坑:iesdouyin 分享页的 play_addr 是一个只对爬虫亮出来的死链

/info 接口成功返回、URL 格式完全合法——这会让你以为是 URL 拼写 bug,反复折腾 play/playwm720p/1080p、加减 line=0、换 UA、加 cookie,全部无效。

真正的问题不在你的 URL 改写逻辑,而是上游数据源已经把死链当活链分发了。任何在 aweme.snssdk.com 域名上的调参都是白费。

不要浪费时间在这些上

以下操作一条都不会让下载恢复

  • playwmplay 互换
  • ratio=720pratio=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 端点已下线,请等待解析器升级"
    }
正解:换接入新 CDN 且维护 a_bogus 签名的解析库

放弃 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 时,问题一定在数据源,而不在你的拼参逻辑。