Python批量下载抖音视频:接口解析与本地备份实现
做自媒体最怕什么不是选题不爆而是哪天账号出问题或者自己手滑辛辛苦苦拍的几百条视频一夜清零。我自己就栽过这个跟头早期拍的几十条素材因为换手机没备份直接没了那种心疼劲儿到现在还记得。从那以后我养成一个习惯定期把账号下的视频全部备份到本地硬盘。这个需求听着简单但真做起来才发现抖音根本没有“一键导出全部视频”的入口网页版只能一页页翻手动另存为能累死人。这篇文章我就把整套批量下载的实现思路和可复现代码整理出来分享给同样有内容备份需求的创作者、剪辑师和数据管理强迫症们。核心思路一句话就能说清拿到账号主页的视频列表接口解析出每个视频的真实播放地址然后按顺序批量下载到本地。全文基于 Python 实现会拆解从参数获取到并发下载的完整链路也会把我在实际使用中踩过的坑、翻过的车都写出来。如果你只是想找个人帮你把视频存下来没关系技术原理看得懂最好看不懂直接照着代码用也行。但有一条底线必须先说清楚这套东西只建议用来备份你自己的账号内容或者已经获得授权的内容不要拿它去下载别人的视频二次发布或商用这是版权红线也是平台规则的红线。1. 批量下载的整体思路与方案选型1.1 先理解视频下载的本质在动手写代码之前必须先弄清楚一件事你在抖音上看到的每一个视频本质上都是一个存在 CDN 服务器上的 mp4 文件。播放器拿到文件地址然后拉流渲染你才能看到画面。所谓下载视频翻译成技术语言就是拿到这个 mp4 的真实地址然后用 HTTP 请求把数据流保存到本地文件里。这个真实地址平时藏在网页的某个角落肉眼是看不到的。如果你手动操作可以在浏览器按 F12 打开开发者工具切到 Network 面板刷新页面播放一个视频然后在类型里筛出 Media就能看到一段 .mp4 结尾的 URL。右键复制用下载工具就能拉下来。单条视频这么操作没问题但批量下载时就完全不现实了——总不可能一条条去翻网络面板吧。所以批量下载的核心难点并不在于“下载”这个动作而在于“怎么自动拿到这一堆视频的直链”。这就像你去仓库取货下载只是把货搬上车真正的难点是搞到货物清单和仓库钥匙。自动遍历某个账号的全部视频列表再从每条记录里把直链提取出来这才是整套自动化方案的地基。1.2 三类主流方案对比围绕“拿到视频直链”这个目标市面上常见的思路可以分成三类页面解析、接口调用、成品工具。页面解析的思路是直接用 BeautifulSoup 或正则表达式去抠网页源码里的标签把 src 属性拿出来。这个方案对单条视频确实好用代码简单不需要太多前置知识。但问题也很明显网页源码里包含大量混淆和加密逻辑直接解析很容易扑空而且抖音的页面是动态加载的滚动到哪才加载到哪想拿到全部视频必须模拟滚动效率和稳定性都很差。接口调用的思路是直接和抖音网页版内部使用的 JSON 接口通信。你在浏览器里看到的所有数据都是前端通过 XHR 请求拿到的我们只要照着浏览器的请求格式用代码发同样的请求就能拿到结构化的数据。这个方案的优点是数据干净、分页清晰、效率极高缺点是需要花点时间理解参数含义和数据结构。成品工具是很多零基础用户的第一选择市面上的“抖音下载神器”一搜一大把。但我必须泼一盆冷水这类工具大多来路不明很多都需要你登录账号把 Cookie 拱手交给第三方服务器。Cookie 等于你账号的临时身份证落到别人手里人家随时能用你的身份做任何事。为了一次下载把账号安全和隐私搭进去这笔账怎么算都不划算。我自己从不推荐使用这类工具宁可多花半小时看明白脚本原理。1.3 为什么不建议用模拟点击有人可能会问既然接口调用要理解参数为什么不直接用 Selenium 这种浏览器自动化工具模拟人手动操作我一开始也是这么想的还专门写过一个自动化脚本让浏览器自动打开主页、自动下滑、自动点击播放按钮。实测之后果断放弃原因有两个。第一是效率太低。抖音页面是滚动懒加载的每滚一次只能加载一二十个视频两百个视频要滚半天中间还可能因为网速、资源加载、页面渲染问题中断。等你终于把页面滚到底部还要再逐条点击视频、等待弹窗、提取地址整个流程跑完四十分钟起步。而接口方案拉取全部视频的列表信息只要几秒钟差距是数量级的。第二是极易失效。页面自动化依赖大量 UI 选择器比如按钮的 class、弹窗的 id这些在前端工程师手里是随时能改的东西。今天能用的选择器明天页面一改版就全部失灵维护成本极高。接口方案虽然也会变但网页版的内部接口相对稳定而且接口返回的是结构化数据改版时只需要调一下参数名比改一整套 UI 操作逻辑省心太多。2. 动手前的环境准备与关键参数2.1 运行环境与依赖库整个方案基于 Python 3只需要安装 requests 这一个第三方库。为什么只用一个库因为项目越小越容易维护出问题时排查也快。有些教程会推荐你用 scrapy、playwright 这种重型框架说实话没必要杀鸡不用牛刀。环境搭建我用一个例子来说明。在终端里执行下面几行命令创建虚拟环境并安装依赖python -m venv douyin_backup source douyin_backup/bin/activate # Windows 下改为 douyin_backup\Scripts\activate pip install requests虚拟环境的作用是给这个脚本单独开一个隔离的 Python 空间不会污染你系统的全局 Python 环境。Windows 和 macOS 的命令略有区别但逻辑完全一样。如果你之前没建过虚拟环境建议先把这个基础打牢后面写别的脚本也用得上。2.2 拿到登录凭证Cookie 与 User-Agent抖音网页版几乎所有的数据接口都要求登录态不登录直接请求返回的数据要么是空的要么直接报错。登录后浏览器会生成一段叫 Cookie 的字符串里面包含了你的身份标识、登录凭证等信息。另外还需要一个 User-Agent它相当于浏览器向服务器自报家门的一段话服务器靠它判断你是一个真实用户在访问。获取步骤非常简单我一步步说用 Chrome 或 Edge 打开 https://www.douyin.com登录你自己的账号。按 F12 打开开发者工具切到 Network网络面板。刷新页面在请求列表里随便点一个 XHR 类型的请求。在右侧 Request Headers请求标头里找到 Cookie整段复制。同一块区域往上翻把 User-Agent 也一起复制下来。用这两个值替换到脚本开头的 HEADERS 字典里。有一点要特别注意Cookie 是有时效的短则几小时长则几天过期后脚本就会失效。另外不要把 Cookie 粘贴到任何来路不明的第三方网站或工具里这等于把账号密码交给别人保管风险极大。2.3 关键参数拆解sec_uid、max_cursor 与数据分页打开任意一个抖音用户主页网址长这样https://www.douyin.com/user/MS4wLjABAAAA...。那一串以 MS4wLjAB 开头的乱码就是 sec_uid也就是用户唯一标识。它决定你要拉取哪个账号下的视频是整个脚本最核心的输入参数。账号下的视频并不是一次性全部返回的接口采用游标分页的方式。第一次请求时max_cursor 填 0接口返回本页数据和一个新的 max_cursor 值下次请求把新的值填进去就能拿到下一页直到返回的 has_more 变成 false。这套机制很好理解就像读一本厚书每次只看一页页码由 max_cursor 告诉你怎么翻。count 参数代表每页期望返回多少条网页版默认是 18实际测试中设置超过 18 会被忽略保持默认即可。读懂这三个参数批量下载的骨架就搭起来了。剩下的就是循环请求、解析数据、攒够列表然后进入下载环节。3. 批量下载的完整实现过程3.1 获取用户主页的视频列表我们请求的接口是 /aweme/v1/web/aweme/post/对应网页版“作品”列表。这个接口是网页版前端自己用的按浏览器的格式构造请求就能正常访问。构造请求时除了带上第 2 节准备好的 Cookie 和 User-Agent还需要填 sec_user_id、max_cursor、count 等参数。下面是一段可直接运行的请求函数import requests import time COOKIE 替换成你自己登录后的完整Cookie USER_AGENT 替换成你的浏览器User-Agent HEADERS { User-Agent: USER_AGENT, Cookie: COOKIE, Referer: https://www.douyin.com/, Accept: application/json, text/plain, */*, } def fetch_video_list(sec_uid, max_cursor0): url https://www.douyin.com/aweme/v1/web/aweme/post/ params { device_platform: webapp, aid: 6383, channel: channel_pc_web, sec_user_id: sec_uid, max_cursor: max_cursor, count: 18, cookie_enabled: true, } resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() return resp.json()这段代码的核心是 params 字典。我在参数表里加了一个 cookie_enabled: true实测带上这个参数能提高请求成功率。如果你在跑的时候发现接口返回异常可以打开浏览器开发者工具找一个真实的请求对比一下参数把缺失的字段补上。拿到返回的 JSON 后先别急着遍历视频。验证一下接口是否正常重点看返回的 status_code 是否为 0以及 aweme_list 是否存在。如果 aweme_list 是空数组大概率是 Cookie 失效或触发风控了这时候去查代码是没用的先解决登录态问题。3.2 从列表数据中提取视频直链与文案接口返回的数据量很大每条视频除了播放地址还包括文案、点赞数、评论数、发布时间、封面图等等。我们真正需要的信息其实只有几样视频 ID、文案、播放地址。下面是一个解析函数把嵌套 JSON 里的关键字段抠出来import re def sanitize_filename(text): return re.sub(r[\\/:*?|], _, text) def parse_video_info(item): aweme_id item.get(aweme_id, ) desc sanitize_filename(item.get(desc, ) or )[:30] video item.get(video, {}) play_addr video.get(play_addr, {}) url_list play_addr.get(url_list, []) return { aweme_id: aweme_id, desc: desc, url: url_list[0] if url_list else , duration: video.get(duration, 0), create_time: item.get(create_time, 0), }aweme_id 是整个视频的唯一 ID全平台不会重复用它做文件名能保证不冲突。desc 是发布时的文案我截取前 30 个字符塞进文件名方便日后检索。文案里经常混着各种特殊字符比如斜杠、双引号、表情必须做一次清洗替换否则保存文件时会直接报“系统找不到指定的路径”这类错误。play_addr 的 url_list 是一个数组里面可能有多个地址通常第 0 个就是可直接播放的流媒体地址。按我经验直接取第 0 个就够了没有遇到需要轮询的情况。duration 和 create_time 暂时用不上但保留下来对后面做归档很有帮助我会在第 5 节详细说明。3.3 多线程批量下载与本地保存把全部视频信息攒到一个列表后就到了最朴素的下载环节。我采用线程池的方式并发下载把网络 I/O 的等待时间压下来。原理很简单网络请求大部分时间都花在等待服务器响应上单线程只能一条一条等多线程则可以让多条请求同时等待整体耗时大幅缩短。import os from concurrent.futures import ThreadPoolExecutor, as_completed def download_video(video_url, save_path, retries3): for attempt in range(retries): try: resp requests.get( video_url, headers{User-Agent: USER_AGENT, Referer: https://www.douyin.com/}, streamTrue, timeout60 ) resp.raise_for_status() with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size1024 * 1024): if chunk: f.write(chunk) return True except Exception: if attempt retries - 1: return False time.sleep(2) return False def batch_download(video_list, save_dirdownloads): os.makedirs(save_dir, exist_okTrue) tasks [] with ThreadPoolExecutor(max_workers5) as executor: for info in video_list: if not info[url]: continue filename f{info[aweme_id]}_{info[desc]}.mp4 save_path os.path.join(save_dir, filename) if os.path.exists(save_path) and os.path.getsize(save_path) 1024: continue tasks.append(executor.submit(download_video, info[url], save_path)) for future in as_completed(tasks): pass这里有两个细节希望你能注意到。第一线程数我控制在 5实测并发太高容易被服务端限流反而拖慢整体速度第二下载前检查文件是否已存在且大于 1KB如果是就直接跳过。这两个细节合在一起解决了两个大问题线程数控制是稳定性的关键文件跳过是断点续跑的关键。下载时用 streamTrue 配合 iter_content 按块写入可以避免一次性把整个大文件读进内存。抖音的视频动辄几十兆内存小的机器直接读会崩溃按 1MB 分块写入就安全得多。3.4 断点续传与完整性校验第一次写批量脚本时我踩过一个特别蠢的坑跑完一遍后发现好多视频文件只有几百字节打开完全无法播放全是被风控返回的空壳文件。从那以后我把完整性校验写进了流程。这里的断点续传不是指 TCP 层面的 Range 请求而是指任务级续跑。设计思路就三条一是下载前检查本地文件。文件存在且大小超过 1KB认定已下载成功直接跳过。二是下载完成后检查文件大小。低于 1KB 的判定为失败删除并计入失败日志。三是失败重试。每条链接最多重试 3 次每次间隔 2 秒重试仍失败的写入 fail.log等待第二轮专门处理。这套逻辑有一个前提抖音的视频直链有时效性过期后会返回错误或空内容。所以我的建议是边抓取边下载不要把所有视频信息抓完再去下载。宁可多跑几个循环也别让链接在内存里过期。4. 实测中的常见问题与避坑指南4.1 请求频率过快触发风控刚开始跑批量脚本时我一股脑把 18 页接口在几十秒内全部请求完结果第二页就返回“操作频繁请稍后再试”。抖音对接口请求频率的监控很严格同一个账号短时间内高频访问基本百分百触发风控。正确的节奏是每页请求之间至少间隔 1 到 2 秒。别小看这几秒200 个视频用分页拉取多等个三十秒完全不影响体验但能换来整晚稳定运行。如果返回提示需要验证不要硬刚立刻停止脚本等 10 到 30 分钟再继续。我试过最严重的一次被限制到第二天早上才恢复所以控制频率真的是第一要务。4.2 登录态过期导致返回空数据Cookie 的时效性很难提前预测有时登录后第二天就失效了。判断方法其实很简单请求列表接口返回的 aweme_list 是空数组而且没有报错提示那大概率就是 Cookie 过期而不是账号没发过视频。解决方式就是重新打开浏览器登录一次复制最新的 Cookie 替换掉代码里的旧值。我个人的习惯是先把新 Cookie 放到一个临时请求里测一次确认能返回数据再跑完整脚本避免批量任务跑到一半才发现全部白跑。4.3 视频直链有效期与下载失败视频直链是带签名的动态地址有效期一般不会太长。如果你把几百条视频信息全部抓出来放着隔半小时再去下载前面的链接很可能已经失效了。解决思路前面提过边抓边下不要全抓完再下。另一个症状是下载到的文件特别小。出现这种情况基本可以断定链接已过期或者被风控拦截。针对这部分文件做重试重试超过 3 次仍失败就记入失败名单等 Cookie 刷新或限流解除后单独处理。别指望一条链接能反复用每次请求接口重新获取直链才是正道。4.4 特殊字符与文件名冲突文案里有表情符号、斜杠、引号在 Windows 文件系统里都是非法字符直接拼进路径会保存失败。我用 sanitize_filename 函数统一清洗把非法字符替换成下划线。另外同一个账号下可能有多条视频的文案完全相同如果只用文案做文件名肯定会出现覆盖。所以文件名必须带上 aweme_id这个 ID 全局唯一能彻底避免冲突。4.5 水印、版权与合规边界这一条必须放在避坑指南里说清楚因为比技术问题更容易让人翻车。任何下载工具都只是技术手段不改变内容归属。批量下载他人视频再二次发布或商用属于侵权这是明确的底线。我这里只建议备份两类内容一是自己账号发布的视频这是对创作成果的合理保存相当于给自己上保险二是已获得作者明确授权的账号内容比如帮朋友、帮团队做素材归档。平台的服务协议通常也不允许自动化采集所以这个脚本只限于个人备份场景请不要拿它去抓别人的数据。5. 从“脚本”到“可靠工具”的进阶策略5.1 按时间与主题自动归档下载完一堆文件后最怕的就是不知道哪个是哪个。我一开始是全部平铺在一个文件夹里文件名虽然有文案和 ID但找起素材来还是头疼。后来加了个按年月日归档的功能从接口返回的 create_time 时间戳解析出日期自动创建“年份/月份”这样的目录结构。from datetime import datetime def make_archive_dir(root, item): ts item.get(create_time, 0) if ts: dt datetime.fromtimestamp(ts) return os.path.join(root, str(dt.year), f{dt.month:02d}) return root这个函数放到下载循环里下载前先把目标目录建好再把文件写进去。实测下来归档版比平铺版好用的地方是一年后想找“去年夏天拍的探店素材”几秒钟就能定位不用在几百个文件名里大海捞针。5.2 增量备份与失败任务重跑备份类工具的核心使用场景是“隔一段时间跑一次”所以增量逻辑真的能省下大量流量和磁盘空间。做法很简单每次脚本跑完把成功写入的 aweme_id 记录到一个本地清单文件里下次运行时先读取清单把已经备份过的视频 ID 过滤掉只下载新发布的视频。失败重跑也一样用一个 fail.log 记录失败的文件名。下载成功就删掉对应行失败就追加。这样跑第二轮时只处理上次失败的文件不重复劳动。日志文件的编码务必用 UTF-8这个坑我在 Windows 下踩过中文文件名全部乱码索引直接废掉。5.3 后续可以扩展的方向脚本跑通之后想升级无非是三个方向一是加图形界面用 PyQt 或 Web 页面做个本地小工具让完全不懂代码的同事也能双击使用二是做定时任务用系统自带的计划任务每天凌晨自动跑一次增量备份三是做多平台支持把这套思路复制到其他短视频平台。我个人最推荐先做增量备份因为这才最贴近真实需求。定时任务则要注意自动跑的时候不要打扰到你正常工作网页版登录态也可能在你没注意时过期所以定一个合理的运行频率和失败提醒机制很重要。做到这一步你手里的就不再是一个一次性脚本而是一个能长期陪伴你的可靠备份小工具。技术带来的安全感是很实在的至少我现在的每一部作品硬盘里都有一份稳稳当当的备份再也不用担心平台规则变动或者账号出问题导致内容丢失了。