1. 抖音网页端视频下载为什么会卡在 X-Bogus
抖音网页端的视频列表接口aweme/v1/web/aweme/post/不是拿到 URL 就能直接请求的。你在浏览器里打开一个博主主页,翻两页,Network 面板里能看到请求参数里带了一个X-Bogus,长度固定 28 位,每次刷新都不一样。少了它,服务端直接返回空数据或者风控页;随便填一个,同样拿不到aweme_list。
这个参数就是抖音前端 JS 在发请求前现算出来的签名。它跟 URL 里的 query 字符串、User-Agent、时间戳等都有关系,所以你不能写死一个值复用,必须把生成逻辑还原出来,在 Python 里每次请求前动态算。
这篇要解决的就是这条链路:怎么在浏览器里定位到 X-Bogus 的生成入口,怎么把混淆过的 JS 抠出来在 Node 环境跑通,怎么用 Python 的execjs调用它,最后拼出完整请求把视频真实地址拿到并下载。适合已经会写基础 requests 爬虫、想搞明白 JS 逆向签名这一环的人。全程只讲技术链路,请求频率自己控制,别拿去批量刷。
我试过直接抄网上现成的 X-Bogus 算法,版本一变就废,所以更推荐你把「定位入口 → 抠 JS → 本地复现」这套方法学会,换参数也能自己跟。
2. 抓包定位:从 XHR 筛选到加密函数入口
2.1 筛选目标接口
打开目标博主主页,F12 进 Network,勾选 XHR,然后在过滤框输入post。翻页时会出现形如:
https://www.douyin.com/aweme/v1/web/aweme/post/?device_platform=webapp&aid=6383&...点开这个请求,看 Payload/Query String Parameters,里面有一堆device_platform、aid、sec_user_id、max_cursor、msToken,最后一个就是X-Bogus。对比两次翻页请求,你会发现只有X-Bogus、max_cursor、msToken在变,其余基本固定。max_cursor是翻页游标,msToken是另一个风控参数,本篇先聚焦 X-Bogus。
2.2 用启动器(Initiator)找调用栈
直接在 Sources 里全局搜X-Bogus往往搜不到,因为代码被混淆了,字符串是拼接出来的。更靠谱的是用 Initiator:
在 Network 里点开那个 post 请求,切到 Initiator 标签,能看到一条调用栈。点最上面一层,会跳到发起 ajax 的位置。这里通常是一个被混淆的XMLHttpRequest封装,X-Bogus在发送前已经被塞进 URL 了。
如果调用栈太深,用 XHR 断点更直接:Sources → XHR/fetch Breakpoints → 添加,URL 填aweme/v1/web/aweme/post。刷新页面,请求发出前会断下,此时在 Call Stack 里逐层往上翻,找到计算签名的那一层。
2.3 日志断点确认生成结果
断下来之后,在可疑的那行右键 → Add logpoint,输入:
console.log(_0x2458f0['apply'](_0xc26b5e, _0x1f1790))继续执行,Console 里会打印出一串 28 位字符串,形如DFSzswVOljJANtOstup5PBt/pL3I,跟请求里的 X-Bogus 一致,说明找对了函数。
输出太多时用条件断点,因为 X-Bogus 长度固定 28:
_0x2458f0['apply'](_0xc26b5e, _0x1f1790).length == 28这样只在真正生成签名时停下。顺着_0x2458f0往上找它的定义,最终会定位到一个加密函数,入参是拼好的 query 字符串,出参就是 X-Bogus。
3. 把混淆 JS 抠出来在本地跑通
3.1 复制核心 JS 文件
定位到加密函数后,看它来自哪个文件,通常是webmssdk.es5.js这类。把这个文件的全部代码复制到一个新建的douyin.js里。直接node douyin.js会报一堆错,因为它是给浏览器环境写的,缺window、document、navigator。
3.2 补齐浏览器环境
在douyin.js顶部补上这些:
window = global; document = {}; document.addEventListener = function () {}; navigator = { "userAgent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' };再执行,报错会少很多。差什么补什么,常见还有location、screen,按报错逐个补空对象或简单实现即可。
3.3 暴露生成函数
找到之前那个加密函数的位置,在文件末尾把它挂到全局,方便 Python 调用:
window.xiaoc = function (params) { return _0x5a8f25(params); // 换成你实际定位到的函数名 };注意这里的userAgent必须和后面 Python 请求头里的 UA 完全一致,否则算出来的签名服务端不认。
4. Python 调用 JS 并拼出完整请求
4.1 用 execjs 调用
先装依赖:
pip install PyExecJS requests调用代码:
import execjs with open("douyin.js", encoding="utf-8") as f: js_data = f.read() js_compile = execjs.compile(js_data) params = "device_platform=webapp&aid=6383&channel=channel_pc_web&..." xb_data = js_compile.call("window.xiaoc", params) print(xb_data) # 应输出 28 位字符串如果这里报ReferenceError,回到第 3 步继续补环境;如果输出长度不是 28,检查传入的params是否和浏览器里完全一致(顺序、编码都不能变)。
4.2 请求头模板
headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", "Referer": "https://www.douyin.com/user/MS4wLjABAAAAF0zlK2_5qCr5Lqy6zLNMH8W146aOetdfKSX95jYXwi8", "Cookie": "你的Cookie" }UA 必须和 JS 里写的一致。Cookie 从浏览器复制,包含msToken、ttwid等,缺失会触发风控。
4.3 拼接 URL 并发请求
import requests url = "https://www.douyin.com/aweme/v1/web/aweme/post/?" params = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAAF0zlK2_5qCr5Lqy6zLNMH8W146aOetdfKSX95jYXwi8&max_cursor=0&count=18&..." xb_data = js_compile.call("window.xiaoc", params) full_url = url + params + "&X-Bogus=" + xb_data response = requests.get(url=full_url, headers=headers) print(response.json())params里的字段顺序要和浏览器抓到的保持一致,X-Bogus是对整个 query 串算的,顺序变了签名就错。
5. 验证签名有效性并下载视频
5.1 判断签名是否生效
请求返回后,看 JSON 里有没有aweme_list。有且非空,说明 X-Bogus 有效;返回{"status_code": 8}或空列表,多半是签名错、Cookie 过期或 UA 不匹配。先用一个真实视频链接跑通单次请求,再考虑翻页。
5.2 提取视频地址
douyin_videos = response.json().get("aweme_list", []) for v in douyin_videos: print(v["video"]["play_addr"]["url_list"][0])play_addr.url_list里通常有多个备用地址,取第一个即可。
5.3 下载到本地
import os os.makedirs("./DouYin", exist_ok=True) for v in douyin_videos: title = v["desc"].replace("/", "_") video_url = v["video"]["play_addr"]["url_list"][0] res = requests.get(video_url, headers=headers) try: with open(f"./DouYin/{title}.mp4", "wb") as f: f.write(res.content) print(f"视频 {title} 下载完成") except Exception as e: print(f"视频 {title} 下载出错 {e}")标题里可能有/等非法字符,先替换掉再当文件名。
6. 本篇常见报错排查
execjs 报ReferenceError: window is not defined:douyin.js顶部没补window = global,或者补的位置在引用之后。放到文件最前面。
签名长度不是 28:传入的params和浏览器不一致,重点检查字段顺序、是否漏了msToken、URL 编码是否被二次处理。
返回空aweme_list:Cookie 过期、UA 与 JS 不一致、X-Bogus算错三者之一。先用浏览器复制的最新 Cookie 重试。
max_cursor翻页拿不到新数据:把上一次响应里的max_cursor传给下一次请求,同时重新算 X-Bogus,不能复用。
Node 执行报document is not defined:补document = {}和document.addEventListener = function(){},按报错继续补location、screen。
请求被重定向到验证页:请求频率过高触发风控,降低频率、加随机间隔,别用多线程猛刷。
排障和接入细节可以对照官方接入文档:https://taotoken.net/api ,API Keys 在 https://taotoken.net/api-keys 管理。想先验证模型返回结构,用模型对话页面 https://taotoken.net/chat 试跑;长期做编码或 Agent 任务,看 Coding Plan https://taotoken.net/coding-plan 。
翻页批量下载时,把max_cursor循环更新、每次重算签名,再配合time.sleep控制节奏,基本就能稳定拿到一个博主的全部视频列表。评论接口是另一套参数,思路一样,先抓包定位签名再复现。