ARTICLE DETAIL

资讯详情

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

Python采集抖音视频全链路:从短链解析到下载的实战指南

Python采集抖音视频全链路:从短链解析到下载的实战指南 做抖音视频采集这件事网上能搜到一堆“几行代码搞定”的帖子但真正自己去跑一遍就会发现要么代码里的接口早就失效要么刚跑通几条就被拦截。这篇是我自己从零开始把 Python 采集抖音视频这条链路完整打通的经验记录覆盖分享链接解析、视频信息获取、签名处理、文件下载的完整过程也把 Cookie、风控拦截、批量采集节奏这些绕不开的坑都讲清楚。适合刚接触爬虫、想用 Python 处理抖音视频素材的开发者参考看完能直接照着改出自己需要的采集脚本。1. 一条抖音视频采集链路要打通哪几个环节1.1 采集链路三段式从短链到视频文件很多人上来就找现成代码但抖音的接口和签名一直在变直接抄一段代码基本活不过一个月。这篇文章不会只给一段能跑的代码而是把整条链路拆开讲让你知道每一步在做什么、为什么这么做、出了问题怎么定位。一条完整的采集链路分三段输入源头拿到一个抖音分享链接类似https://v.douyin.com/xxxx/这种短链或者一段带有分享口令的文字。中间环节从分享链接解析出视频 ID再携带合适的请求头请求视频详情接口拿到视频的播放地址。输出环节向播放地址发起请求把视频流保存到本地文件。听起来不复杂但每一段都有各自的坑。短链要经历一次 302 跳转详情接口要带 Cookie、User-Agent可能还要算签名参数播放地址本身往往又是一个会过期的临时链接需要尽快下载。这三段里任何一段出错采集出来的就是一堆 403 页面或者乱码文件。我建议在动手之前先把三个环节的职责分清解析环节干的事情是“从一长串文本里找出视频 ID”信息环节干的事情是“问服务器要这个视频的地址”下载环节干的事情是“把 mp4 二进制流写到磁盘”。1.2 为什么建议把三个环节拆开写把链路拆开最大的好处是便于维护。抖音的风控升级很频繁今天改的是签名参数明天可能改的是播放地址格式。如果你把三个环节写在一个函数里一旦中间某步变动整段逻辑都要返工排查问题的时候也分不清到底是哪一步出的错。更实际的原因是三个环节各自适合独立测试。解析环节的输出就是视频 ID信息环节的输入输出都依赖网络下载环节又要处理文件系统。拆开后你可以先把解析环节跑通打印出视频 ID 确认无误再去调试信息环节最后处理下载。这样每一步都有明确的验证点出错了也能快速定位到具体模块。下面我按这三个环节挨个展开讲代码也是按这个结构组织的。2. 从分享链接到视频ID解析环节的两种做法2.1 短链跳转法让服务器告诉你视频ID抖音的分享短链是v.douyin.com域名下的随机字符串真正包含视频 ID 的完整链接藏在 302 跳转之后的地址里。用 requests 的allow_redirects参数就能跟踪过去import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.douyin.com/, } def resolve_share_url(share_url: str) - str: resp requests.get(share_url, headersHEADERS, allow_redirectsTrue, timeout10) return resp.url share_url https://v.douyin.com/xxxx/ final_url resolve_share_url(share_url) print(final_url) # 期望输出类似: https://www.douyin.com/video/7123456789012345678跳转后的最终 URL 里会有一个很长的纯数字 ID那是这个视频在抖音里的唯一标识后面请求详情接口全靠它。注意这个数字串长度通常在 19 位左右用正则\d{15,20}匹配比较稳妥不要用\d一把梭否则可能把 URL 里其他参数的数字也匹配进来。实际测试中我发现有的短链会先跳到一个中间页再跳到最终页requests 默认会一路跟随所以你不需要自己处理中间过程直接拿resp.url就是最终地址。有的分享文案里还带有“复制打开抖音”一类的口令口令本身也会包含一段 hash 字符串但那段不是视频 ID。如果你想直接从口令文本解析需要额外处理解码步骤不如短链跳转干净。实际操作中我一般让用户提供一个干净的短链解析成功率最高。如果你拿到的是一整段分享文案可以用正则把里面的短链 URL 先提取出来再走上面的跳转逻辑。2.2 正则提取的细节与容错拿到最终 URL 之后提取视频 ID 的代码很简单import re def extract_video_id(final_url: str) - str | None: match re.search(r/video/(\d{15,20}), final_url) if match: return match.group(1) match re.search(rmodal_id(\d{15,20}), final_url) if match: return match.group(1) return None这里用了两种模式去匹配一是/video/路径二是modal_id参数。因为抖音有时候短链跳过去会落在带弹窗参数的页面上视频 ID 不在路径里而在查询参数里。多写一个分支能提高不少命中率。早年还有一种情况是分享链接跳转到v.douyin.com下的短链后停留在页面需要从页面源代码里提取itemId之类的字段这种方案现在已经很少用但如果你遇到解析不到 ID 的情况可以打开最终页面源码搜一下 video id 关键字通常会有一个_ROUTER_DATA字段里藏着完整信息。还有一个细节值得提醒requests 默认的 User-Agent 是python-requests/2.x用这个去请求抖音短链大概率直接返回验证页面。所以 HEADERS 里一定要带上一个浏览器的 User-Agent最好连 Referer 和 Accept-Language 一起带上。别小看这些请求头很多风控第一道就是看 UAUA 不对后面的代码写得再漂亮也白搭。3. 视频详情接口抓包定位与请求构造3.1 先在浏览器里把接口长什么样摸清楚解析出视频 ID 之后下一步是拿到视频的播放地址。这个地址不会直接写在 HTML 里而是由页面里的 JavaScript 异步请求一个详情接口拿到的。想知道接口长什么样最直接的办法是打开浏览器的开发者工具在无痕窗口里打开抖音网页版随便点开一个视频。按 F12 打开开发者工具切换到 Network网络面板。刷新页面在筛选框里输入aweme请求列表里会出现aweme/detail之类的接口。点开这个请求查看 Payload 和 Response 的内容。请求 URL 大致是https://www.douyin.com/aweme/v1/web/aweme/detail/?aweme_idxxxa_bogusxxx其中aweme_id就是我们刚解析出来的视频 IDa_bogus是一段由前端 JS 动态生成的签名参数。响应体里有个video字段里面又套着play_addr和play_addr_lowbr那才是真正能下载的视频流地址。download_addr有时候也有但经常会因为鉴权原因没法直接访问我实际测试下来play_addr最稳。响应里还有desc视频描述和author.nickname作者昵称后面写文件名要用。3.2 用 requests 请求详情接口能通但有条件在不处理a_bogus的前提下直接用 requests 请求这个接口返回结果通常分三种情况返回 200 且带有完整 JSON说明接口还没强制校验签名这种情况现在很少见但某些低版本接口还能碰到。返回 200 但 Response 是一段 HTML请求被风控兜住了被重定向到验证页面。返回带错误码的 JSON缺少签名参数被拒。也就是说纯 requests 方案近两年基本不可靠。为了演示完整流程我介绍一种“人工补签”的方式先从浏览器复制一份完整 Cookie 和请求头再把 Cookie 带进 requests 请求里。个人小批量采集、几十个视频以内这种方式依旧是成本最低的DETAIL_API https://www.douyin.com/aweme/v1/web/aweme/detail/ def fetch_video_info(video_id: str, cookie: str) - dict: params { aweme_id: video_id, device_platform: webapp, aid6383: , } headers { User-Agent: HEADERS[User-Agent], Referer: https://www.douyin.com/, Cookie: cookie, Accept: application/json, text/plain, */*, } resp requests.get(DETAIL_API, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json()Cookie 从哪里来打开 Chrome进入抖音网页版随便点开一个视频按 F12 切到 Network找到任意一个请求右键 Copy as cURL然后从 cURL 命令里把 Cookie 字段粘出来。整个过程不需要任何额外工具。注意 Cookie 有效期不长通常几天到几周过期后重新复制一份就行。这里必须强调这个方案适合个人学习测试一批采集别超过几十个频率控制在每秒一个以内不要并发轰炸。如果要做商业化的大规模采集正当路径是去接入抖音开放平台的正规接口而不是跟风控死磕。3.3 异常情况下如何判断问题出在哪请求详情接口常见的返回异常按出现频率排一下返回 HTML几乎 100% 是缺 Cookie 或者 Cookie 失效。返回签名错误码签名校验失败要么a_bogus没带要么带了但算错了。纯 requests 方案里会遇到。超时或者连接重置本机 IP 被临时风控需要停一会儿再继续。返回aweme_detail为 null视频 ID 不对或者视频被删掉、设为私密。定位思路也很简单先用浏览器手动访问一次详情接口 URL看浏览器里能不能返回数据。浏览器能、Python 不能问题一定在请求头或者签名浏览器也不能那接口或者视频本身有问题跟你的代码无关。这个排查顺序能帮你省掉大量无效调试时间。4. 想要稳定采集绕不开的 a_bogus 签名处理4.1 a_bogus 是什么为什么不能绕过a_bogus是抖音 web 端用来校验请求合法性的一段签名参数由页面里的 JavaScript 根据 URL、请求参数、Cookie 等信息动态算出。它的作用就是防止别人用 requests 这种静态请求伪造接口调用。只要缺少它或者算错接口就会拒绝返回数据。这个名字容易让人误解成“假参数”其实它是全家桶里的一个早些年流行的是X-Bogus后来升级成a_bogus。网上有很多讲 X-Bogus 算法的文章但那是老接口时代的东西照着抄基本跑不通。真正靠谱的做法不是逆向这套算法而是“让浏览器帮你算”。逆向算法的技术确实值得研究但如果只是想把采集工具跑起来浏览器自动化是投入产出比最高的路线。4.2 让浏览器代劳Playwright 接管页面与请求我的方案是用 Playwright 打开真实浏览器访问抖音页面等页面里的 JS 跑完接口请求自动带上a_bogus发出去然后我们在浏览器层面拦截响应直接拿到 JSONfrom playwright.sync_api import sync_playwright def fetch_video_info_with_browser(video_id: str) - dict: url fhttps://www.douyin.com/video/{video_id} with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page( user_agent( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) ) result {} def on_response(response): if aweme/v1/web/aweme/detail/ in response.url: try: result[data] response.json() except Exception: pass page.on(response, on_response) page.goto(url, wait_untilnetworkidle, timeout30000) page.wait_for_timeout(3000) browser.close() if data not in result: raise RuntimeError(未能捕获详情接口响应可能页面加载失败或需要登录) return result[data]这段代码的思路是打开视频详情页让页面自己触发详情接口请求用on_response钩子把包含aweme/detail的响应捕获下来。因为浏览器自己会计算a_bogus所以不需要在 Python 里实现签名算法。实测下来Playwright 方案的稳定性比纯 requests 方案高很多这也是目前个人开发者做抖音采集的主流姿势。需要注意几个参数headlessTrue能省资源但如果发现容易触发验证可以改成headlessFalse用有头模式跑兼容性更好wait_untilnetworkidle和page.wait_for_timeout(3000)是为了等接口请求完成网络慢的环境可以把超时时间调大首次启动 Playwright 需要先执行playwright install chromium装浏览器内核不然会直接报异常。4.3 从返回的 JSON 里提取真正的下载地址不管用哪种方式拿到 JSON提取下载地址的逻辑是一样的。响应结构大致是aweme_detail.video.play_addr.url_list[0]里面是带签名的完整地址直接用就行。我习惯把播放地址、视频描述、作者昵称一起取出来后面写文件命名要用def extract_download_url(detail: dict) - tuple[str, str, str]: video_info detail[aweme_detail][video] play_url video_info[play_addr][url_list][0] desc detail[aweme_detail].get(desc, ) author_info detail[aweme_detail].get(author, {}) author author_info.get(nickname, ) if author_info else return play_url, desc, author播放地址的格式在不同视频上可能不一样有的是v3-web.douyinvod.com开头有的是www.douyin.com开头的临时地址。不管哪种这个地址都带有效期一般在几十分钟到几小时之间拿到之后要尽快下载别存到数据库里隔天再下到时候多半已经 403 了。这一点我后面还会再提因为它是实际使用中最容易踩的坑之一。5. 视频流下载重定向处理与文件写入5.1 播放地址为什么要处理重定向播放地址指向的是 CDN 的调度入口真正保存视频文件的节点地址在请求过程中会经过一次或多次重定向。requests 默认会跟随重定向但要注意一个问题如果你在请求播放地址时带了详情接口的 RefererCDN 那边有可能会拒绝。所以我单独抽一个下载函数只保留必要的 UAReferer 和 Cookie 都不带def download_video(play_url: str, save_path: str) - None: headers { User-Agent: HEADERS[User-Agent], } with requests.get(play_url, headersheaders, streamTrue, timeout30) as resp: resp.raise_for_status() with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size64 * 1024): if chunk: f.write(chunk)streamTrue一定要开否则大视频会把内容全部加载进内存几个几十兆的视频就能把内存吃满。分块写盘的好处是边下边存万一网络断了前面下载的部分也不会丢。另外CDN 响应头里通常有Content-Length可以在下载前先判断一下文件大小跳过大小为 0 或小到可疑的响应避免把错误页面存成 .mp4。我第一次跑通的时候犯过这个错误几兆的 HTML 错误页被存成了视频文件播放器打不开排查了半小时才发现是响应内容的问题。5.2 文件名与目录的工程化处理视频描述里经常带有特殊字符比如\、/、?、*、|、、、这些在 Windows 下不能出现在文件名里。老老实实做一层过滤import re def safe_filename(name: str, max_len: int 50) - str: name re.sub(r[\\/:*?|\r\n\t], _, name) name name.strip().strip(.) return name[:max_len] or untitled我习惯的文件名格式是“作者_描述_视频ID.mp4”既方便批量整理又保留了唯一标识后面回查的时候能直接对应到原视频。如果描述太长就截断但视频 ID 一定要完整保留。目录方面建议按日期分文件夹比如./douyin/2026-02/避免几千个文件堆在一个目录里资源管理器打开都卡。如果你采集的内容类型比较多可以再加一层分类目录比如按话题或者按作者分方便后续使用。6. 批量采集与风控实战报错、限频、Cookie 轮换6.1 单视频跑通之后批量怎么组织单个视频的采集链路理清楚之后批量就是套一层循环的事。但这里要泼一盆冷水很多初学者上来就写 for 循环一个接一个请求跑到第几十个就被风控踢下线然后跑来问为什么代码会被封。批量采集的核心不是代码而是节奏。我的建议是采用“低并发 随机延迟 失败退避”的策略import random import time def batch_collect(video_ids: list[str], cookie: str): for idx, vid in enumerate(video_ids): try: play_url, desc, author collect_single(vid, cookie) save_path f./douyin/{safe_filename(author)}_{safe_filename(desc)}_{vid}.mp4 download_video(play_url, save_path) print(f[{idx 1}/{len(video_ids)}] {vid} - {save_path}) except Exception as exc: print(f[{idx 1}/{len(video_ids)}] {vid} 失败: {exc}) time.sleep(random.uniform(3, 8))这个随机延迟不是装样子。风控会分析请求频率固定间隔比随机间隔更容易被识别。睡眠时间我一般控制在 3 到 8 秒之间单号采集几十条这个节奏基本不会触发风控。如果确实有更多量要采就得准备多个账号的 Cookie 做轮换而不是让一个 Cookie 拼命跑。另外批量脚本里一定要加异常处理单条失败不要让整个脚本中断退出尽量记录日志后继续跑剩下的最后统一看哪些失败了再补采。6.2 高频报错的真实含义与处置方式把实际踩过的报错和处理方式整理成一张表方便遇到的时候快速对照现象大概率原因处理方式HTTP 200 但返回 HTMLCookie 失效或缺失重新从浏览器复制 Cookie返回签名错误码签名缺失或错误改用 Playwright 方案返回未登录错误码登录态过期重新登录并更新 CookieHTTP 403下载地址过期或 Referer 被拒重新获取播放地址去掉 Referer连接超时/重置IP 被临时风控停止请求 10~30 分钟再继续视频内容为 0 字节响应被劫持或地址错误检查响应头 Content-Type 是否为 video/mp4批量场景里最常见的其实是第一种前几十个好好的突然开始返回 HTML。这不是代码问题是 Cookie 被风控标记了。可以尝试换一个浏览器无痕窗口重新复制 Cookie也可以直接停 15 分钟再接。千万不要死循环重试同一个失效 Cookie那样只会让风控判定更加严重。我之前见过有同事在脚本里写了一个 while True 的重试逻辑跑到后面直接把账号搞进了风控名单这个教训很深刻。6.3 合规这件事说几句实在的抖音视频采集这个需求本身是个中性工具用来做内容备份、个人素材整理、学习数据分析都可以但有几个底线问题值得提前想清楚。第一采集到的视频如果包含人物肖像二次发布一定涉及肖像权问题除非你有授权第二视频内容本身受版权保护不能拿来做成素材库对外售卖第三如果你是在公司里做这类项目务必先确认平台的用户协议和公司合规流程别拿个人账号去跑业务级的需求。我的建议是这几点只采集你自己有权限处理的视频比如自己账号里的作品、得到授权的博主内容。个人学习用的小批量采集注意频率不要影响平台正常服务。有商业化需求优先查是否接入过抖音开放平台正规授权接口省心得多。采集完的本地文件做好隐私管理特别是涉及个人信息的评论区、私信内容一律不要碰。7. 我自己跑下来的一些心得7.1 排查顺序先人工后自动化第一个经验是“先确认目标页面能不能正常打开”。有时候代码怎么调都失败回头一查是浏览器本身都打不开网页那问题根本不在你的代码。在做任何排查之前先人工打开一个视频页确认页面能正常加载、接口能正常返回再谈写代码。这一步能帮你省掉一半的无效排查时间。这个习惯对任何网页采集任务都适用不光是抖音。7.2 签名算法与自动化路线的选择第二个是关于签名。网站的风控是动态升级的a_bogus这套签名今天能用明年可能就换了新参数到时候你花半个月逆向出来的算法直接作废。与其追着算法跑不如一开始就站在 Playwright 这条路线上让网站自己的 JS 去解密、去签名、去拿数据。逆向算法的技术确实值得研究但一个采集工具用浏览器自动化的方式维护成本最低这是我在几次风控升级之后总结出的结论。记住你的目标是把数据拿到手不是跟风控较劲。7.3 播放地址时效与下载顺序第三个是下载地址的时效性。我踩过最大的坑就是白天把一批视频的播放地址存进了数据库晚上下班回来跑下载脚本结果全部 403。播放地址从获取到失效可能只有几十分钟所以“获取地址”和“下载文件”这两个动作一定要紧接着完成中间不要隔太久。如果确实需要分批下载就重新请求一次详情接口重新拿地址。这一点无论用 requests 还是 Playwright 都成立。7.4 想清楚采集目的再动手第四个是想清楚采集的目的。绝大多数人做抖音采集想要的不是视频文件本身而是视频背后的信息某个博主的内容规律、某个话题的传播趋势、某些素材在特定时间窗口的热度变化。如果只是为了分析其实只需要采集文本信息就够了不要下载一堆几百 MB 的视频把磁盘塞满。把这些维度理清楚你的 Python 代码会简单很多对平台的负担也小很多。就我自己的经验来说先想清楚“我要解决什么问题”再决定“我要采什么数据”比先写代码再调参高效得多。整个链路从短链解析、详情接口、签名处理到视频下载每一步都有它存在的理由。照着上面的顺序把每一段跑通再根据自己的需求改改参数和存储逻辑一个可用的采集工具就搭起来了。后续如果接口有变动优先排查请求头和签名相关的部分大部分问题都出在那里。
返回列表