十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

抖音Web端视频采集实战:接口分析与签名参数全解析

抖音Web端视频采集实战:接口分析与签名参数全解析 这两年搞抖音数据采集的越来越多但网上大多数教程都在讲页面解析、模拟点击这类土办法实际一跑就废。我个人的结论是抖音Web端的视频采集核心就三件事——找到对口的数据接口、搞清楚签名参数的生成思路、处理好下载链路的时效与风控。这篇文章我直接按自己的实操路径来写从接口分析到代码落地再到常见坑位完整过一遍Python爬取抖音视频的流程给想少走弯路的同学一份能直接抄作业的参考。先交代一下背景我的场景是给某个内容分析项目做素材收集目标是从抖音网页版根据关键词或单个视频链接采集视频文件量级不大单次几十到几百条不涉及批量爆破、不涉及用户隐私数据。如果你想采集的数据量很大或者想绕过登录机制批量下载那下面的方案不一定适合而且我得说一句任何采集都必须在平台规则和法律法规允许的范围内进行本文只讨论接口调用层面的技术实现用于学习和小规模合法素材整理请勿用于商业侵权或恶意抓取。1. 整体设计与思路拆解1.1 从哪条路径下手抖音的客户端形态非常多有移动端App、小程序、Web网页版、桌面客户端等等但做爬虫选型的逻辑其实很简单哪个端口的接口最容易调用就用哪个。移动端App的接口虽然数据最全但签名体系极度复杂而且设备风控等级极高普通爬虫根本扛不住。小程序端接口比较轻量但同样存在签名校验。相比之下Web端的接口相对友好参数可控性也高一直是个人开发者的首选。我最终选定的是**抖音Web端www.douyin.com**的接口链路。原因有几条一是网页端数据结构相对规整很多信息直接挂在window.__INITIAL_STATE__里二是Web端的签名校验虽然存在但网上已有成熟的降级方案不需要你去硬刚最底层的加密逻辑三是Web端天然支持Cookie会话模拟真实用户浏览行为的成本比较低。不过这里要明确一点抖音的接口版本更新非常频繁我今天写的接口路径和参数可能过几个月就变了。所以这篇文章的重心不是给你一套永远能跑的代码而是教你分析方法让你将来遇到接口变动时自己能快速定位到新的接口和参数。1.2 为什么选接口方案而不是页面解析很多新手一上来就用Selenium或Playwright模拟浏览器然后从渲染后的DOM里提取视频链接。这个方法确实能跑但问题很明显浏览器自动化启动慢一个会话光启动浏览器就要几秒批量采集时效率低到怀疑人生。抖音页面是典型的流式加载视频列表会随滚动动态加载你需要频繁处理滚动事件、等待元素出现代码复杂度成倍增长。页面里的视频地址往往不是最终播放地址而是经过处理的短链或带签名的地址直接用很可能下载失败。浏览器自动化很容易被检测WebDriver特征、自动化痕迹都是现成的风控指标。而直接调用接口的方案数据返回的是结构化JSON视频链接就在里面拿到就能下载效率和稳定性完全不是一个量级。1.3 整体工作流程我理一下这套采集方案的整体链路构造请求 → 获取接口数据 → 解析JSON提取视频信息 → 处理视频直链 → 下载视频文件其中每一步都有坑构造请求时要带全Referer、User-Agent等请求头否则容易被风控拦截获取接口数据后要注意数据是JSON还是JSONP格式需要做相应处理提取视频信息时要同时拿视频标题、作者、封面图、播放链接等多个字段处理视频直链时要注意链接中可能带签名参数签名有有效期需要尽快下载下载视频时要设置合理的超时和重试机制防止因网络波动导致下载失败。2. 核心接口与数据解析2.1 入口数据的两种获取方式抖音Web端采集视频常见的有两种入口单个视频链接和关键词搜索列表。单个视频链接的思路最简单。你在抖音APP或网页版上复制一个视频分享链接形如https://v.douyin.com/xxxxx/这个链接打开后会302跳转到视频详情页页面里就包含了完整的视频数据。采集时你只需要请求这个详情页再从HTML或内嵌的JSON数据里提取信息即可。关键词搜索列表则稍微复杂。抖音网页版搜索后会走一个独立的搜索接口返回的是包含多个视频数据的列表。这个方式的难点在于搜索接口的分页不是简单的页码每页条数而是依赖一个cursor游标参数你需要解析响应里的has_more字段来判断是否有下一页。我个人的建议是如果只是零散采集几个视频就用单个链接如果是批量采集优先用搜索接口。因为搜索接口一次能拿回几十条数据减少请求次数对风控更友好。2.2 从详情页提取视频数据先说说单个视频链接的采集。你复制到的链接是短链直接访问会得到一个302跳转。用requests请求时默认不会自动跟随跳转所以你需要手动处理import requests share_url https://v.douyin.com/xxxxx/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://www.douyin.com/, } resp requests.get(share_url, headersheaders, allow_redirectsFalse) redirect_url resp.headers.get(Location)这个redirect_url就是视频详情页的真实链接。接下来请求这个详情页页面HTML里会有一段script idRENDER_DATA typeapplication/json里面的内容就是URL编码后的JSON数据解码后就能拿到视频信息。import urllib.parse import json from bs4 import BeautifulSoup detail_resp requests.get(redirect_url, headersheaders) detail_resp.encoding utf-8 soup BeautifulSoup(detail_resp.text, html.parser) render_data soup.find(script, idRENDER_DATA) if render_data: decoded_data urllib.parse.unquote(render_data.string) data json.loads(decoded_data)解析出来的data是一个嵌套很深的字典视频信息通常在data[app][videoInfoRes][item_list][0]这个路径下。里面包含video.play_addr_list视频播放地址列表通常有多个清晰度video.play_addr旧版本接口的播放地址字段desc视频文案author.nickname作者昵称statistics点赞、评论、转发等数据这里有个很关键的点**play_addr里拿到的地址是带签名参数的有效期通常只有几分钟到几小时所以解析出来之后要立刻下载**。如果你拿去存库过段时间再用地址大概率已经失效了。2.3 搜索接口的参数结构搜索接口的地址一般形如https://www.douyin.com/aweme/v1/web/search/item/请求方式为GET关键参数包括keyword搜索关键词search_channel搜索类型视频搜索用aweme_general或aweme_videosort_type排序方式0表示综合排序1表示最多点赞publish_time发布时间过滤0表示不限search_source固定值通常为normal_searchquery_correct_type固定值通常为1offset或cursor游标位置第一页为0count每页数量通常为10到20device_platform固定值webappaid固定值6383channel固定值channel_pc_web实际请求时这些参数和Cookie里的会话信息必须保持一致性否则容易被判定为异常访问。我建议你在浏览器里登录抖音网页版然后从开发者工具里复制完整的Cookie字段手动设置到请求头里。注意搜索接口返回的数据里也有has_more字段值为1表示还有下一页。每次翻页时需要把上一次响应里的cursor值传给下一次请求。这个机制和传统分页完全不同很容易踩坑。2.4 签名参数的处理思路抖音的Web接口大部分请求都需要带一个X-Bogus头或a_bogus参数这个参数本质上是把请求URL、User-Agent、Cookie等关键信息通过一套自定义算法加密后生成的校验字符串。如果你直接裸请求返回结果大概率是{status_code: 200101}这类风控提示。关于签名算法我不建议你自己去逆向JS成本太高。更实际的做法是在浏览器开发者工具里搜索a_bogus或X-Bogus定位到生成代码的位置。复制这段JS代码借用Python的execjs库调用执行得到签名参数。把签名参数拼接到请求URL或请求头里。import execjs import re # 从抖音的JS文件中提取签名算法代码这里简化为从本地文件读取 with open(msign.js, r, encodingutf-8) as f: js_code f.read() ctx execjs.compile(js_code) def get_bogus(url: str, user_agent: str) - str: result ctx.call(sign, url, user_agent) return resultmsign.js你可以理解为一份从抖音Web JS里提取出来的签名函数网上有不少开源项目维护了这类代码。使用时注意定期更新因为平台调整JS逻辑的频率挺高的。实操心得签名算法代码更新频繁我建议把它单独放在一个JS文件里Python这边只管调用不要试图去维护JS内部的逻辑。另外签名函数可能依赖固定的User-Agent你设置请求头时一定要和生成签名时用的User-Agent保持一致否则签名校验过不了。3. 实操过程与代码实现3.1 环境准备建议使用Python 3.9依赖库越少越好方便后续部署。我这边用到的库有requestsHTTP请求execjs执行JS签名函数json、re数据解析os文件保存安装命令pip install requests PyExecJS这里提一下PyExecJS的坑它默认依赖系统的JavaScript运行时Windows下需要安装Node.jsLinux下需要安装Node.js或QuickJS。如果没有运行时环境调用脚本时会直接报错。装好Node.js后可以在Python里验证一下import execjs print(execjs.get().name)输出是Node.js (V8)就说明环境正常。3.2 单个视频链接采集的完整代码下面给一套可以直接改来用的例子目标是根据一个分享短链下载视频文件import requests import json import urllib.parse import os import re from bs4 import BeautifulSoup UA Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 HEADERS { User-Agent: UA, Referer: https://www.douyin.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } def get_redirect_url(share_url: str) - str: resp requests.get(share_url, headersHEADERS, allow_redirectsFalse, timeout10) location resp.headers.get(Location) if not location: raise Exception(无法获取跳转链接) return location def parse_video_info(detail_url: str) - dict: resp requests.get(detail_url, headersHEADERS, timeout15) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) render_tag soup.find(script, idRENDER_DATA) if not render_tag: raise Exception(未找到RENDER_DATA) decoded urllib.parse.unquote(render_tag.string) data json.loads(decoded) # 提取视频信息这个路径以实际返回为准 item data.get(app, {}).get(videoInfoRes, {}).get(item_list, [{}])[0] video_info { desc: item.get(desc, ), author: item.get(author, {}).get(nickname, ), video_url: item.get(video, {}).get(play_addr, {}).get(url_list, [])[0], } return video_info def download_video(video_url: str, save_path: str): if not video_url.startswith(http): video_url https: video_url # 播放地址里的 需要转义 video_url video_url.replace(amp;, ) resp requests.get(video_url, headersHEADERS, streamTrue, timeout30) if resp.status_code ! 200: raise Exception(f下载失败HTTP {resp.status_code}) with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) if __name__ __main__: share_url https://v.douyin.com/xxxxx/ detail_url get_redirect_url(share_url) info parse_video_info(detail_url) print(f视频文案: {info[desc]}) print(f作者: {info[author]}) os.makedirs(downloads, exist_okTrue) save_path os.path.join(downloads, f{info[author]}_{int(time.time())}.mp4) download_video(info[video_url], save_path) print(f已保存: {save_path})这里面有几个细节说明一下短链跳转时allow_redirectsFalse是关键否则你拿不到中间跳转的Location头。RENDER_DATA里的数据是URL编码的必须unquote之后才能json.loads。视频地址里的amp;是HTML转义符直接请求会报错要替换成。下载时用streamTrue边读边写避免一次性把大文件载入内存。3.3 搜索列表批量采集批量采集的核心是搜索接口的翻页逻辑。下面是简化版的代码骨架import requests import time def search_videos(keyword: str, cookie: str, max_pages: int 3): base_url https://www.douyin.com/aweme/v1/web/search/item/ headers { User-Agent: UA, Referer: fhttps://www.douyin.com/search/{keyword}, Cookie: cookie, } cursor 0 page 0 all_items [] while page max_pages: params { keyword: keyword, search_channel: aweme_general, sort_type: 0, publish_time: 0, search_source: normal_search, query_correct_type: 1, offset: cursor, count: 10, device_platform: webapp, aid: 6383, channel: channel_pc_web, } resp requests.get(base_url, headersheaders, paramsparams, timeout15) data resp.json() items data.get(data, []) if not items: break all_items.extend(items) has_more data.get(has_more, 0) if not has_more: break cursor data.get(cursor, cursor) page 1 time.sleep(2) # 控制请求频率 return all_items这里的offset和cursor通常是同一个值第一页传0翻页时把响应里的cursor原样传回去。sort_type0是综合排序sort_type1是最高点赞你可以按需求切换。注意批量采集时请求频率非常重要。我实测下来每次请求间隔2秒比较稳如果间隔太短很容易触发验证码或返回空数据。如果采集量上百条建议把间隔拉到3到5秒并且启用IP轮换或代理池。3.4 数据存储与断点续传采集到的视频信息建议先存成JSON或存数据库再异步下载视频。我常用的做法是搜索阶段只解析并保存元数据标题、作者、链接、点赞数等等所有元数据采集完毕再逐个下载视频。这样即使中间某个视频下载失败也不会浪费时间重跑搜索接口。import json def save_meta(items, filenamevideos.json): with open(filename, w, encodingutf-8) as f: json.dump(items, f, ensure_asciiFalse, indent2)下载阶段可以维护一个已完成列表每次启动时读取列表跳过已下载的视频实现断点续传。import os finished_file downloaded.txt finished set() if os.path.exists(finished_file): with open(finished_file, r, encodingutf-8) as f: finished set(line.strip() for line in f) # 下载完成后 with open(finished_file, a, encodingutf-8) as f: f.write(video_id \n)4. 常见问题与排查技巧4.1 采集返回状态码200101这是抖音风控最常见的提示之一字面意思是“校验失败”。引发的原因可能是一次请求频率太高、请求头缺失、Cookie异常也可能是指纹参数未正确传入。我的排查顺序是先检查请求头是否携带完整的Referer和User-Agent再确认Cookie是否有效且未过期最后查看签名参数X-Bogus或a_bogus是否拼接正确。有时候同一个接口用浏览器打开是正常的但爬虫请求就报200101这时候十有八九是签名参数缺失或错误。优先检查签名算法是不是已经失效重新从网页JS里提取一次。原因表现处理方式请求头不全直接200101或403补齐Referer、UA、Accept等Cookie过期返回登录跳转或空数据手动刷新Cookie重新设置签名参数错误返回200101更新签名JS代码确认UA一致请求频率过高先正常后突然200101调大请求间隔切换IP4.2 拿到视频地址却下载失败之前说过视频播放地址通常带签名play_addr里的地址可能不是最终直链而是需要二次302跳转。用requests下载时默认会自动跟随跳转所以大部分情况下能直接下载。但有个场景要注意片段式视频地址。抖音部分视频的url_list里有多个地址有的指向v3-web.douyinvod.com这种CDN域名有的指向一个文本片段列表HLS分片如果你拿到的URL后面带m3u8那你下载的是一个流媒体列表文件不是直接可播放的MP4。处理方式有两种一种是换url_list里的其他地址找MP4直链另一种是用ffmpeg把m3u8合成为完整视频ffmpeg -i https://xxx.m3u8 -c copy output.mp44.3 页面数据结构变更抖音前端经常改版RENDER_DATA的路径、字段名都可能变。如果解析不到视频数据最快的办法是在浏览器中打开详情页右键检查script idRENDER_DATA的完整内容用Python交互环境一层层剥洋葱一样试路径。不要相信网上的旧文章里写的固定路径一定要自己验证。我给一个快速验证的技巧import json import urllib.parse def find_key(data, target_key): results [] if isinstance(data, dict): for k, v in data.items(): if k target_key: results.append(v) results.extend(find_key(v, target_key)) elif isinstance(data, list): for item in data: results.extend(find_key(item, target_key)) return results # 从decoded data里找所有包含play_addr的值 videos find_key(data, play_addr)这样找字段比逐层写死路径更稳妥能适配不同版本的返回结构。4.4 关于无水印方案的一个提醒聊到抖音采集总绕不开“无水印下载”这个话题。市面上很多方法的原理其实都类似视频播放地址里有水印标志参数某些情况下可以通过替换域名或去掉指定参数来获得无水印版本。我不在这里展开具体的破解操作原因是这类方案变动太频繁而且涉及平台权益。我的实际使用建议是如果你只是做个人素材收集直接从接口拿到的带水印视频通常够用。如果确实需要无水印素材请走抖音官方的下载入口或授权渠道避免侵权风险。另外抖音的接口返回数据里带有wm、watermark字样时通常就是水印控制参数但这部分内容法律风险较高我强烈不建议去逆向处理。5. 一些值得说的经验最后分享几个实操中总结出来的经验都是踩坑踩出来的。关于签名参数尽量不要自己维护完整的JS解析逻辑。抖音Web端的JS代码是改版重灾区今天调试通了明天可能就变了。更稳妥的方式是定期从网页包里提取最新版签名算法并且做好异常兜底——签名失败时主动降级为浏览器自动化方案虽然慢但能跑通。关于Cookie建议保存一份浏览器Cookie快照每次采集前先验证一下是否失效。验证方法很简单请求一次搜索接口看返回里的status_code和data字段。我遇到过明明浏览器能正常访问但Python请求返回数据为空的情况最后发现是Cookie复制时漏了一段折腾了很久。关于视频链接的有效期这一点很多人容易忽略。play_addr里的URL短则几分钟长则几小时就会过期。所以解析和下载之间的间隔一定要短不要在中间去做太多耗时操作。如果采集量很大建议解析完立即下载不要等全部解析完再统一下载否则前面的地址早就失效了。关于采集的合法边界这个我不多展开但我要再说一次个人学习、素材整理、舆情分析等正当用途没有问题但批量下载他人作品用于二次传播、去水印后声称原创、或者用于商业项目都会涉及侵权。做技术归做技术红线还是要守住。如果你只是想快速采集几个视频我建议直接复制视频分享链接用3.2节的代码就够了。如果要做关键词批量采集那就把3.3节的搜索循环和下载逻辑拼起来记得控制好速度和频率。抖音的接口变化快今天能跑的代码明天可能就要调参保持对网页结构的敏感度比背代码更重要。
返回列表