)
1. 快手视频批量下载的真实痛点与场景拆解很多人第一次写快手视频下载脚本卡住的地方往往不是代码本身而是三件事直链拿不到、翻页翻不动、文件重名覆盖。我试过用最朴素的方式——打开博主主页右键复制视频地址结果发现拿到的是带时效签名的 CDN 链接过几分钟就 403。后来才明白快手 PC 端主页的视频列表是通过 GraphQL 接口分页返回的photoUrl字段里才是当前可用的直链而这个直链同样有时效必须在拿到响应的当下就发起下载。这篇内容面向的是有 Python 基础、想跑通「批量抓取快手视频并保存到本地」这条链路的同学。核心检索词就是 Python 爬虫、快手视频下载、批量保存本地。我会把请求头构造、pcursor 翻页、去重命名、断点续传、MD5 校验这几块拆开讲每一步都给可复制的代码。同时因为脚本里会涉及调用凭证的管理我会用 TaoToken 的统一 Key 通道来集中管理 API 凭证避免把敏感信息散落在代码里。先说清楚边界本文只讨论公开主页视频列表的采集与本地保存用于个人学习与技术研究。实际抓取时请控制频率加time.sleep不要对目标站点造成压力。另外快手前端结构会变接口字段和参数名可能调整遇到报错要会看响应体而不是死磕旧代码。整个流程我拆成六个环节环境准备、接口分析与请求头构造、pcursor 分页遍历、直链下载与去重命名、断点续传与 MD5 校验、常见报错排查。每个环节都有独立的验证动作跑通一个再进下一个不要一口气全写完再调试那样出错很难定位。环境方面Python 3.8 以上即可依赖只有requests。如果你本地已经装过 requests直接跳过安装。没装的话pip install requests目录结构建议提前建好后面脚本直接往里写kuaishou_downloader/ ├── main.py ├── download/ ├── done.txt └── config.jsondownload/放视频文件done.txt记录已完成的视频 ID 用于断点续传config.json放凭证和博主 userId。这个结构不复杂但能让你在中断后不用从头再来。2. TaoToken 统一 Key 通道的前置准备与凭证管理脚本里需要用到调用凭证。传统做法是把 Cookie、Key 直接硬编码在main.py里一旦代码分享出去就泄露了。更稳妥的方式是用 TaoToken 的统一 Key 通道来集中管理把凭证从业务代码里剥离出来。TaoToken 官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基础地址是 https://taotoken.net/api 。它的作用是给你一个统一的 Key 来管理调用凭证你不需要在多个脚本里重复粘贴同一串密钥。对于爬虫项目来说这意味着你可以把 Key 写进环境变量或配置文件代码里只读不写。具体操作路径先到控制台创建 Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建完成后在 API Keys 页面复制你的 Key页面地址 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。如果你对模型对话能力也感兴趣可以看 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 长期做编码和 Agent 任务的话Coding Plan 页面在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。拿到 Key 之后不要写死在代码里。我建议用config.json加环境变量两层兜底{ taotoken_api_key: 你的_TAOTOKEN_KEY, taotoken_base_url: https://taotoken.net/api, kuaishou_user_id: 3xkw562btehf7ga, cookie: 你的快手登录Cookie, max_pages: 5, sleep_seconds: 2 }然后在main.py里这样读取import json import os def load_config(pathconfig.json): with open(path, r, encodingutf-8) as f: cfg json.load(f) cfg[taotoken_api_key] os.getenv(TAOTOKEN_API_KEY, cfg.get(taotoken_api_key, )) return cfg这样即使config.json被误传到公开仓库只要环境变量里没设Key 也不会生效。如果你用的是 Cline MCP 或 Claude Code 这类工具做辅助开发配置里同样要写全三件套Base URL 填https://taotoken.net/apiKey 填你创建的那串Model ID 按你实际使用的模型填。三者缺一请求就会失败。需要提醒的是TaoToken 在这里的角色是凭证管理通道不是用来替代你的爬虫逻辑。快手接口的请求头、Cookie、pcursor 这些还是得你自己构造。把凭证管理和业务逻辑分开是让项目可维护的关键一步。3. 可复制的请求会话配置与 pcursor 分页脚本这一节是核心。先构造一个带重试的 requests 会话再写 GraphQL 请求体最后处理 pcursor 翻页。会话配置我加了HTTPAdapter和Retry这样遇到偶发的连接重置会自动重试不用手动重跑import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(): session requests.Session() retry Retry( total3, backoff_factor1.5, status_forcelist[429, 500, 502, 503, 504], allowed_methods[POST, GET] ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.mount(http://, adapter) return session请求头里最关键的是Cookie和content-type。content-type必须是application/json因为快手 GraphQL 接口收的是 JSON body不是表单def build_headers(cookie): return { 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, accept: */*, Accept-Language: zh-CN,zh;q0.9, content-type: application/json, origin: https://www.kuaishou.com, referer: https://www.kuaishou.com/, Cookie: cookie }GraphQL 的 query 字符串很长我把它单独抽成一个常量避免和业务逻辑混在一起。operationName固定为visionProfilePhotoListpage固定为profileuserId从配置读pcursor第一页传空字符串GRAPHQL_QUERY fragment photoContent on PhotoEntity { id duration caption originCaption likeCount viewCount commentCount realLikeCount coverUrl photoUrl photoH265Url manifest manifestH265 videoResource coverUrls { url __typename } timestamp expTag animatedCoverUrl distance videoRatio liked stereoType profileUserTopPhoto musicBlocked __typename } fragment feedContent on Feed { type author { id name headerUrl following headerUrls { url __typename } __typename } photo { ...photoContent __typename } canAddComment llsid status currentPcursor tags { type name __typename } __typename } query visionProfilePhotoList($pcursor: String, $userId: String, $page: String, $webPageArea: String) { visionProfilePhotoList(pcursor: $pcursor, userId: $userId, page: $page, webPageArea: $webPageArea) { result llsid webPageArea feeds { ...feedContent __typename } hostName pcursor __typename } } 请求函数这样写注意用json而不是datadef fetch_page(session, headers, user_id, pcursor): url https://www.kuaishou.com/graphql payload { operationName: visionProfilePhotoList, variables: { page: profile, pcursor: pcursor, userId: user_id }, query: GRAPHQL_QUERY } resp session.post(url, headersheaders, jsonpayload, timeout15) resp.raise_for_status() return resp.json()翻页逻辑的关键点第一页pcursor传空响应里data.visionProfilePhotoList.pcursor就是下一页的游标。当返回的pcursor为no_more或者feeds为空时停止翻页def iter_all_feeds(session, headers, user_id, max_pages, sleep_seconds): import time pcursor for page in range(max_pages): data fetch_page(session, headers, user_id, pcursor) node data.get(data, {}).get(visionProfilePhotoList) if not node: print(f第 {page1} 页无数据停止) break feeds node.get(feeds, []) if not feeds: print(f第 {page1} 页 feeds 为空停止) break yield page, feeds pcursor node.get(pcursor, ) if not pcursor or pcursor no_more: print(已到最后一页) break time.sleep(sleep_seconds)这段代码跑通后你会看到每页返回的 feeds 列表每个 feed 的photo.photoUrl就是视频直链。先别急着下载打印几个确认字段结构对得上再进下一步。4. 直链下载、去重命名与 MD5 完整性校验拿到photoUrl之后下载本身不难难的是去重和校验。快手同一个视频可能在不同页重复出现如果直接按序号命名会覆盖或者产生重复文件。我的做法是用photo.id作为唯一标识文件名用{id}_{caption前20字}.mp4同时把已完成的 id 写进done.txt。下载函数带流式写入避免大文件占内存import os import hashlib def download_video(session, video_url, save_path, chunk_size1024*256): with session.get(video_url, streamTrue, timeout30) as r: r.raise_for_status() with open(save_path, wb) as f: for chunk in r.iter_content(chunk_sizechunk_size): if chunk: f.write(chunk) return save_path去重和断点续传靠done.txtdef load_done(done_filedone.txt): if not os.path.exists(done_file): return set() with open(done_file, r, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) def mark_done(done_file, video_id): with open(done_file, a, encodingutf-8) as f: f.write(video_id \n)MD5 校验用来确认下载完整。快手直链下载完成后本地文件的 MD5 应该和远端一致。如果远端没给 MD5至少校验文件大小和Content-Lengthdef md5_of_file(path): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(1024*1024), b): h.update(chunk) return h.hexdigest()主循环把上面几块串起来def run(): cfg load_config() session build_session() headers build_headers(cfg[cookie]) done load_done() os.makedirs(download, exist_okTrue) for page, feeds in iter_all_feeds( session, headers, cfg[kuaishou_user_id], cfg[max_pages], cfg[sleep_seconds] ): for feed in feeds: photo feed.get(photo, {}) vid photo.get(id) vurl photo.get(photoUrl) if not vid or not vurl: continue if vid in done: print(f跳过已下载 {vid}) continue caption (photo.get(caption) or no_caption)[:20] safe_name .join(c for c in caption if c.isalnum() or c in _-) save_path os.path.join(download, f{vid}_{safe_name}.mp4) try: download_video(session, vurl, save_path) size os.path.getsize(save_path) print(f完成 {save_path} 大小 {size} 字节 MD5 {md5_of_file(save_path)}) mark_done(done.txt, vid) except Exception as e: print(f下载失败 {vid}: {e})跑完之后download/里的文件数量应该等于done.txt的行数。你可以用ls download | wc -l和wc -l done.txt对比数字一致说明没有漏记。MD5 值建议单独存一份checksums.txt方便后续复查。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth这一节列几个我实际踩过的报错以及对应的排查方向。401 Unauthorized最常见的原因是 Cookie 过期或者没带 Cookie。快手登录态失效后GraphQL 接口会返回 401 或者返回体里result为错误码。排查动作把headers里的 Cookie 换成浏览器里最新登录的重新跑一次fetch_page打印resp.status_code和resp.text前 500 字。如果返回体里有result: 2之类的基本就是登录态问题。local proxy failed这个报错通常出现在你本地网络环境有代理设置但 requests 没走对通道。排查动作检查环境变量HTTP_PROXY、HTTPS_PROXY是否被设置如果有要么在session里显式配置proxies要么清掉环境变量。另外Retry配置里如果status_forcelist包含了不该重试的状态码也可能触发这个提示。把total调小先确认单次请求能通。reading choices 相关报错这类报错一般出现在解析响应体时代码假设了某个字段一定存在但实际返回结构变了。比如data[data][visionProfilePhotoList][feeds]里某个 feed 没有photo字段。排查动作在解析前先做防御性判断用.get()逐层取打印原始 JSON 的 keys。我习惯在fetch_page返回后先print(json.dumps(data, ensure_asciiFalse)[:800])确认结构再写解析逻辑。OAuth 相关报错如果你在脚本里集成了 TaoToken 的调用出现 OAuth 报错通常是 Key 没带对或者 Base URL 写错。检查三件套Base URL 是否为https://taotoken.net/apiKey 是否从 API Keys 页面正确复制Model ID 是否和实际调用的一致。三者任一不对都会报鉴权失败。另外Key 如果写在config.json里但环境变量也设了同名变量环境变量会覆盖配置文件确认你改的是生效的那一份。还有一个容易忽略的点快手 GraphQL 接口对请求频率敏感连续快速请求可能返回空 feeds 或者 429。加time.sleep(2)以上并且把max_pages设小一点先测试。如果返回 429把Retry的backoff_factor调大让重试间隔拉长。6. 凭证与调用通道的长期管理建议脚本跑通之后真正要花心思的是长期维护。快手前端接口会变Cookie 会过期直链有时效这些都不是一次配置就能一劳永逸的。我的做法是把易变的部分全部外置到config.json代码里只留逻辑。Cookie 过期了就更新配置文件接口字段变了就改GRAPHQL_QUERY不用动主流程。凭证管理这块TaoToken 的统一 Key 通道解决的是「多处调用、一处管理」的问题。你可以在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里查看 Key 的使用情况在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里轮换 Key。如果后续你要把爬虫和模型对话、编码辅助结合起来模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 长期编码任务可以看 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 遇到配置问题先翻文档比到处问快。最后给一个实用技巧把done.txt和checksums.txt一起纳入版本管理如果视频本身不敏感这样换机器或者重装环境后直接跑脚本就能跳过已下载的只补新增的。断点续传不是靠复杂的断点记录而是靠「已完成清单 唯一 ID」这个简单组合。文件数量对得上、MD5 对得上这条链路就算真正跑通了。