python快手批量采集(二):用 pcursor 接口分页拉取与断点续采实战
1. 从一次翻页翻车说起pcursor 分页游标到底是什么做快手批量采集第一页往往最顺利请求发出去二十条视频数据整整齐齐躺在visionProfilePhotoList里。真正让人抓头的是第二页——你把同样的参数再发一次返回的还是那二十条。问题不在请求头也不在 Cookie而在一个叫pcursor的字段上。pcursor是快手接口里的分页游标cursor你可以把它理解成图书馆管理员手里那枚书签第一次借书时书签是空的管理员从第一排开始给你拿你拿走二十本后他会在第二十本的位置夹一枚书签下次你再来他直接翻到书签处继续。这个「书签」不是页码而是一串看起来像科学计数法的长数字比如1.660360144345E12。它由上一组响应体返回再原样塞进下一组请求的表单里如此循环直到响应体里出现no_more才代表到底了。这篇要解决的就是这条链路上的工程化问题怎么解析返回结构、怎么推进游标、什么时候停、断了之后怎么接着采。适合已经能发出第一页请求、但卡在「第二页拿不到数据」或者「采到一半程序崩了要重头再来」的 Python 开发者。核心检索词就三个python 快手批量采集、pcursor 分页、断点续采。下面所有代码都可以直接复制改参数运行我会把每一步的中间结果也打出来方便你对照自己的返回体。先说清楚一个前提采集行为要遵守目标平台的服务条款和 robots 协议控制请求频率只采公开数据别给服务器添堵。本文聚焦的是分页游标这套机制本身把它吃透你在别的分页接口上也能复用同样的思路。2. 前置准备TaoToken 接入与请求环境搭建在写分页逻辑之前得先把「能稳定发请求」这件事解决掉。很多同学卡在第一步不是代码问题而是请求链路本身不稳要么直连超时要么返回一堆风控页面。我的做法是把模型调用和采集脚本的调试分开——采集脚本负责抓数据遇到返回结构看不懂、报错信息读不明白的时候用 TaoToken 的模型对话能力帮我快速解析 JSON 结构和定位字段路径效率比对着几百行响应体肉眼找高得多。TaoToken 是一个聚合多家大模型能力的 API 平台官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。它的价值在于你写采集脚本时经常需要「让模型帮我看看这段返回体里 pcursor 到底在哪一层」或者「这段正则为什么匹配不到」直接调模型比开浏览器搜半天快。对于长期做数据采集和 Agent 的同学Coding Plan 这类套餐能把调用成本压下来适合把模型能力嵌进日常脚本调试流程。接入本身不复杂关键是三件套要配全Base URL、API Key、Model ID。少任何一个都会报 401 或者 model not found。我建议你先把 Key 拿到手后面验证分页逻辑时如果 JSON 解析报错可以直接把响应体丢给模型问字段路径。拿 Key 的路径是登录后进控制台在 API Keys 页面创建。地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。创建完复制那串sk-开头的字符串只显示一次记得存好。如果你用的是 Claude Code 这类编码工具它需要单独配置 Anthropic 兼容的接入方式文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL 和鉴权头的完整写法。配置类工具最怕的就是 Base URL 少写一段路径或者 Key 前面多了空格这两个坑我后面排障章节会专门讲。环境层面Python 侧只需要requests和json标准库够用。建议建一个独立虚拟环境避免和你机器上其他项目的依赖打架python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install requests请求头这块快手接口对User-Agent、Referer、Cookie比较敏感。我的经验是把浏览器里真实请求的这几个头完整复制过来尤其是Cookie它决定了你能不能拿到数据。别用默认的 python-requests UA那基本第一页就给你返回空。3. 可复制的分页请求模板与游标持久化配置这一节是全文的核心我把分页请求拆成「请求参数模板」和「游标状态文件」两块。先看请求参数模板。快手这个接口的表单里pcursor初始值是空字符串其余参数比如userId、count保持固定。下面是一个可以直接跑的模板import requests import json import time import os BASE_URL https://www.kuaishou.com/graphql # 以实际抓包到的接口为准 HEADERS { User-Agent: 你的浏览器UA, Referer: https://www.kuaishou.com/, Cookie: 你的Cookie, Content-Type: application/json, } def build_payload(user_id, pcursor): return { operationName: visionProfilePhotoList, variables: { userId: user_id, pcursor: pcursor, count: 20, }, query: 你的GraphQL查询语句, }注意pcursor默认给空字符串这就是第一页的起点。请求发出去后从响应体里取下一枚游标def fetch_page(user_id, pcursor): payload build_payload(user_id, pcursor) resp requests.post(BASE_URL, headersHEADERS, jsonpayload, timeout15) resp.raise_for_status() data resp.json() photo_list data[data][visionProfilePhotoList] next_cursor photo_list.get(pcursor, ) videos photo_list.get(feeds, []) return videos, next_cursor这里有个细节pcursor在响应体里的位置是data.visionProfilePhotoList.pcursor不是顶层。字段路径写错是新手最常见的翻页失败原因返回的next_cursor会是空循环直接退出你还以为是采完了。接下来是断点续采的关键——游标持久化。思路很简单每采完一页把当前游标和已采数量写进本地 JSON 文件程序重启时先读这个文件从上次的游标继续。配置文件长这样{ user_id: 3x1234567890, last_pcursor: 1.660360144345E12, fetched_count: 40, seen_ids: [video_id_1, video_id_2], updated_at: 2025-01-01T12:00:00 }对应的读写函数STATE_FILE cursor_state.json def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, r, encodingutf-8) as f: return json.load(f) return {user_id: , last_pcursor: , fetched_count: 0, seen_ids: []} def save_state(state): state[updated_at] time.strftime(%Y-%m-%dT%H:%M:%S) with open(STATE_FILE, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2)seen_ids这个字段是去重用的。快手在翻页边界偶尔会返回重复视频尤其是你请求间隔太短的时候。把已采的视频 id 存下来写入前先判断能避免数据里出现重复行。这个列表会越滚越大采几千条之后建议换成 SQLite 或者布隆过滤器但小规模采集用 JSON 完全够。把分页和状态串起来的主循环def crawl(user_id, max_pages50): state load_state() pcursor state[last_pcursor] if state[user_id] user_id else seen set(state[seen_ids]) page 0 while page max_pages: videos, next_cursor fetch_page(user_id, pcursor) if not videos: print(本页无数据可能已到底或触发风控) break new_count 0 for v in videos: vid v.get(id) if vid and vid not in seen: seen.add(vid) new_count 1 # 这里写你的入库逻辑 state.update({ user_id: user_id, last_pcursor: next_cursor, fetched_count: state[fetched_count] new_count, seen_ids: list(seen), }) save_state(state) print(f第 {page1} 页新增 {new_count} 条游标 {next_cursor}) if next_cursor no_more or not next_cursor: print(已到最后一页) break pcursor next_cursor page 1 time.sleep(2) # 控制频率别把服务器打爆 return state这段代码里有两个终止条件next_cursor no_more和not next_cursor。前者是接口明确告诉你到底了后者是防御性判断防止字段缺失导致死循环。time.sleep(2)是必须的我试过把间隔调到 0.5 秒第三页就开始返回空数据等几分钟再跑又正常典型的频率限制。4. 验证请求用最小样本检查翻页完整性与去重效果代码写完不能直接上大批量先用一个粉丝量小的账号跑三页验证三件事游标是否真的在推进、翻页有没有漏、去重有没有生效。第一步打印每页的游标变化。正常情况应该是第一页请求pcursor返回一个长数字第二页请求这个长数字返回另一个不同的长数字直到某页返回no_more。如果你发现第二页返回的游标和第一页一样说明请求参数没生效大概率是pcursor没塞进variables里或者塞错了层级。v1, c1 fetch_page(user_id, ) print(第一页游标:, c1) v2, c2 fetch_page(user_id, c1) print(第二页游标:, c2) print(游标是否推进:, c1 ! c2)第二步检查翻页完整性。把三页的视频 id 收集起来看总数是不是接近 60每页 20 条。如果第二页只有 5 条可能是账号本身视频就少也可能是被截断。这时候把响应体完整打印出来看feeds数组长度和pcursor字段resp requests.post(BASE_URL, headersHEADERS, jsonbuild_payload(user_id, c1)) body resp.json() feeds body[data][visionProfilePhotoList][feeds] print(本页条数:, len(feeds)) print(本页游标:, body[data][visionProfilePhotoList][pcursor])第三步验证去重。故意把同一页请求两次看seen_ids有没有拦住重复state crawl(user_id, max_pages2) print(累计采集:, state[fetched_count]) print(去重集合大小:, len(state[seen_ids]))如果fetched_count小于len(seen_ids)说明有重复被拦下了去重逻辑生效。正常情况下两者应该相等。第四步模拟断点续采。跑到第二页时按 CtrlC 中断然后重新运行crawl观察它是不是从last_pcursor继续而不是从第一页重来。这一步能验证状态文件读写是否正确。我踩过的坑是状态文件写在了相对路径换了个工作目录运行就读不到了结果又从第一页开始。建议用绝对路径或者把状态文件和脚本放同一目录并用os.path.dirname(__file__)拼路径。验证通过后你会看到类似这样的输出第 1 页新增 20 条游标 1.660360144345E12 第 2 页新增 20 条游标 1.660360144346E12 第 3 页新增 18 条游标 no_more 已到最后一页 累计采集: 58 去重集合大小: 5858 而不是 60是因为账号本身只有 58 条视频这是正常的。如果每页都满 20 但总数对不上才需要怀疑漏采。5. 常见报错排查401、游标不推进与 JSON 解析失败采集过程中最常撞见的几个报错我按出现频率排一下每个都给定位方法。401 Unauthorized / 鉴权失败。这个在采集脚本里通常不是 Key 的问题而是 Cookie 过期。快手接口靠 Cookie 里的登录态鉴权Cookie 一般几小时到几天就失效。表现是请求返回 401 或者返回一个空的feeds。解决办法是重新从浏览器复制 Cookie。如果你同时用 TaoToken 调模型辅助调试注意区分两套鉴权TaoToken 用Authorization: Bearer sk-xxx快手用 Cookie别混。TaoToken 侧如果报 401检查 Key 有没有多余空格、Base URL 是不是写成了https://taotoken.net/api注意结尾不要多加斜杠导致路径拼接错误。local proxy failed / 连接超时。这个报错说明请求根本没发出去卡在本地网络层。常见原因是系统代理设置和脚本里的代理配置冲突。检查requests有没有走系统代理可以显式设置proxies{http: None, https: None}排除干扰。另外超时时间别设太短快手接口偶尔响应慢timeout15比较稳妥。reading choices of undefined / JSON 解析失败。这个报错在采集脚本里对应的是resp.json()抛异常说明返回的不是 JSON可能是 HTML 风控页。先打印resp.text[:500]看内容。如果是 HTML说明触发了风控降低频率、换 Cookie、加请求间隔。如果你是在用模型辅助解析响应体时看到reading choices那是模型 API 返回结构和你预期的不一致检查请求体里model字段填的 Model ID 是否正确以及messages数组格式对不对。游标不推进第二页和第一页数据一样。这是分页逻辑最典型的 bug。排查顺序先确认pcursor有没有从响应体正确取出打印body[data][visionProfilePhotoList][pcursor]再确认取出的值有没有传进下一次请求的variables.pcursor最后确认请求体是json而不是data用data发 GraphQL 会导致参数不被识别。这三步走完基本能定位。OAuth 相关报错。如果你用 Claude Code 或类似工具接入 TaoToken报 OAuth 错误通常是鉴权头格式不对。Anthropic 兼容接口需要x-api-key头而不是Authorization具体看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。配置类工具的三件套Base URL、Key、Model ID缺一不可Model ID 填错会报 model not foundBase URL 填错会报 404 或连接失败。no_more 判断失效导致死循环。有些账号的最后一页返回的pcursor是空字符串而不是no_more如果你的终止条件只判断no_more就会一直请求空游标拿到重复数据。所以终止条件要写成if next_cursor in (no_more, , None)。这个坑我在两个不同账号上遇到过返回格式不一致防御性写法能省很多事。6. 把分页能力沉淀成可复用的采集骨架走到这里你已经有了一个能翻页、能续采、能去重的采集脚本。我想再补一个实用技巧把fetch_page和状态管理抽成一个类这样换账号、换接口时只改参数不改逻辑。class CursorCrawler: def __init__(self, fetch_fn, state_file): self.fetch_fn fetch_fn self.state_file state_file def run(self, key, max_pages50): state load_state() pcursor state[last_pcursor] if state[user_id] key else # ... 同上逻辑这样你采完视频列表想接着采评论或者别的分页接口只要换一个fetch_fn就行游标持久化和去重逻辑完全复用。另外提醒一句seen_ids用列表存采到几万条之后in判断会变慢因为列表查找是 O(n)。换成set或者写进 SQLite 加唯一索引性能会好很多。我采一个五万粉的账号时列表方案跑到两万条明显卡顿换 set 之后流畅了。最后采集频率这件事再怎么强调都不为过。time.sleep(2)是底线账号视频多的话建议加到 3 到 5 秒并且每采几百条停一会儿。数据是采不完的账号被封了就什么都没了。把游标状态存好今天采一半明天接着采比一口气冲到底稳妥得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →