尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

QQ空间动态爬虫实战:Cookie登录、并发控制与zip归档全攻略

QQ空间动态爬虫实战:Cookie登录、并发控制与zip归档全攻略 简介这是一套基于 cookie 登录的 QQ 空间动态爬虫项目面向想学习 Python 爬虫、数据采集与自动化处理的中级开发者。项目中实现了好友列表获取、说说动态抓取、详情页解析等完整流程可通过 cookie 绕过登录限制抓取所有可访问好友空间的动态并保存到本地同时用 JS 与 HTML 生成可视化报告适合社交数据分析和个人数据归档场景。压缩包共 16 个文件以 10 个 Python 脚本为主干涵盖请求发送、页面解析、数据存储等模块另有 2 个 JS 和 1 个 HTML 用于结果展示1 个 MD 说明文档及 cookie 会话文件辅助运行包体仅 222KB结构紧凑便于阅读改造。目前已有 244 人学习下载。入手后可获得一个可运行的完整爬虫方案包括 cookie 复用、请求伪装、数据清洗入库、异常处理等关键实现按目录拆解还能学到模块化编程与反爬应对思路。1. QQ 空间动态爬虫的可行边界与 cookie 登录的真正约束先把已登录账号“所有可访问好友”的空间动态抓下来存成 zip代码量其实不大真正的难点有三个暗坑cookie 失效常常不表现为登录退出而表现为接口返回空列表或签名参数失效好友可见性不是静态权限而是风控实时算出来的结果同一个好友在不同端看到的内容都不一样单线程翻页很慢并发一上去就触发验证码。这个标题要做的事就是拿 cookie 建立可靠会话、拉全好友列表、解析动态 JSON、控制节奏落到本地 zip。适合写过头条爬虫、但对 QQ 空间这类强风控站点还陌生的开发者也适合想给过去动态做空间归档备份的普通用户。2. cookie 登录的获取、存储与失效判定从浏览器到 requests 的无缝切换2.1 cookie 和 session 的区别决定你该怎么注入登录态QQ 空间的登录态由 Session服务端缓存和 Cookie客户端凭证共同维护。Cookie 只是钥匙Session 才是门锁背后的房主。很多初学者把两者混为一谈以为把浏览器里整段 Cookie 塞给 requests 就万事大吉其实服务端在 Session 里还存着会话元数据、风控状态和临时授权一旦 Session 被清理即便浏览器那份 Cookie 还没过期接口也会拒绝响应。这也是为什么经常能看到 HTTP 200、data 为空装得跟成功一样实际上会话早就断了。对爬虫而言最省事的路径是在浏览器里手动登录一次把登录后的 Cookie 导出给脚本用而不是自己去实现 QQ 号密码加密和验证码流程。自己实现登录协议等于把腾讯的登录风控、滑块验证和加密算法全部重新踩一遍投入产出比很低。手动登录配合导出的 Cookie既能带上 p_skey又能避免密码登录引发的二次验证是这类强风控站点上更常见的做法。2.1.1 chrome 98 之后Cookie 复制要换一种姿势从 chrome 98 开始开发者工具的 Application 面板里默认看不到 HttpOnly 标记的 Cookie很多关键的登录字段藏在那里面控制台里 document.cookie 也拿不到。所以更可靠的办法是去 Network 面板找在浏览器里登录 qzone.qq.com确认动态列表能正常显示。按 F12 打开开发者工具切到 Network 面板刷新页面。找到任意以 cgi-bin 开头的接口请求点开 Headers。在 Request Headers 里找到 Cookie 这一行右键复制整段值。复制出来是一整段以分号分隔的键值对包含 uin、skey、p_skey 等字段。p_skey 带 HttpOnly只靠 JS 取不到从 Network 面板复制是最快的办法。复制后先保存成 txt避免后面反复去浏览器里找。2.2 requests 注入 cookie 的三种方式以及会话持久化import requests from http.cookiejar import CookieJar import pickle raw_cookie_str uin123456; skeytestskey; p_skeytestpskey # 方式一字符串直接塞请求头适合快速调试单个接口 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Cookie: raw_cookie_str, Referer: https://user.qzone.qq.com/, } r1 requests.get(https://user.qzone.qq.com/, headersheaders) # 方式二解析成 CookieJar 绑定到 Session推荐用于完整采集 jar CookieJar() for item in raw_cookie_str.split(; ): key, value item.split(, 1) jar.set(key, value, domain.qq.com, path/) session requests.Session() session.cookies jar r2 session.get(https://user.qzone.qq.com/, headers{ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://user.qzone.qq.com/, }) # 方式三先让 requests 访问登录页由库自动吸收 Set-Cookie s3 requests.Session() s3.headers.update({User-Agent: Mozilla/5.0}) s3.get(https://user.qzone.qq.com/)方式一适合只调试一个接口复制粘贴最快方式二把 Cookie 按 .qq.com 域绑定到 Session 上会话会带着它们走重定向并且后续响应里的新 Set-Cookie 也会合并进来是实际采集最常用的形态方式三要求浏览器会话仍然有效本质上是让 requests 自己续上会话如果服务端要求扫码或短信验证它会拿不到新 Cookie。建议正式脚本选方式二并且在请求完后把 session.cookies 对象保存到本地跨天采集时直接复用。def save_cookies(session, pathqzone_cookies.pkl): with open(path, wb) as f: pickle.dump(session.cookies, f) def load_cookies(pathqzone_cookies.pkl): s requests.Session() with open(path, rb) as f: s.cookies pickle.load(f) return s保存 CookieJar 而不是保存字符串原因在于 pickle 会把每个 cookie 的 domain、path、expires 元数据一起带走重新加载后 requests 能按域名自动匹配不用手工拼请求头。加载完建议立刻探活一次用一个轻量接口做校验确认会话还在服务端有效避免后面请求全部白跑。2.3 登录失败的三档识别与重登判断QQ 空间的接口很喜欢制造“假成功”HTTP 200 加一个空 data 只会让你一头雾水。根据响应特征可以把失效分成三档响应特征判定处理HTTP 200data 是正常数组登录态有效继续解析HTTP 200JSON 里 code 为负数或出现 login_verify 字段登录态失效或风控停止请求回到浏览器重登HTTP 302 跳转到 xui.ptlogin2.qq.comsession 已断丢弃旧 Cookie重新导出判断逻辑可以在代码里做成一个函数返回状态枚举。def check_login_state(resp): if resp.status_code 302 and ptlogin2 in resp.headers.get(Location, ): return session_expired if resp.status_code ! 200: return network_error try: data resp.json() except ValueError: return unknown if data.get(code) 0: return ok if login_verify in str(data) or api in str(data).lower(): return need_relogin return unknown这里的参数解释一下status_code 先判断网络层Location 里的 ptlogin2 是腾讯统一登录中心的域名特征只要出现就说明会话已经在服务端被清了data.get(code) 0 是正常返回的约定负值则基本是登录态问题。注意不要只靠返回码判断因为 302 在 requests 里默认会跟随跳转最后拿到的可能是一段 HTML所以要在会话里把 allow_redirects 关掉或者直接检查最终 URL 是否变成了 login 相关域名。提示cookie 失效后不建议在脚本里强行重试登录接口。QQ 空间的登录流程涉及加密参数和时间戳脚本模拟失败只会加重风控回浏览器手动一次再导出成本最低。3. 好友列表与空间动态接口的解析从 skey 到完整 zip3.1 gtk接口请求里的隐藏签名参数QQ 空间接口不直接收 skey 作为参数而是要传一个由 skey 派生出的 gtkg_tk。这是腾讯前端公开的哈希算法把 skey 拆成字符后逐位累乘。Python 实现如下def get_gtk(skey: str) - str: hash_val 5381 for ch in skey: hash_val (hash_val 5) ord(ch) return str(hash_val 0x7fffffff)函数每次读到字符 ch就让 hash_val 左移 5 位相当于乘 32再加上原来的 hash_val 和字符码点最终与 0x7fffffff 做按位与把结果限制在 31 位有符号整数范围内。这个值会作为每个 cgi 请求的 g_tk 参数出现。还有一个细节计算时用的是哪个 skey 决定你能访问谁的空间。用自己账号的 skey 算出来的 gtk 能访问自己的空间要访问好友空间用的是以 p_skey 参与计算。很多爬虫对好友请求全部带自己空间的 gtk结果只能拿到公开内容漏掉“仅好友可见”的动态原因就在这里。参数含义用于skey账号基本登录凭证访问自己的空间、好友列表p_skey好友空间的授权凭证访问好友可见动态gtkg_tk由 skey 派生的签名参数携带在每个 cgi 请求 query 里3.2 好友列表接口先把能访问的人筛出来“所有可访问好友”不来自某个离线配置文件而是来自好友列表接口的一次实时读取。常见做法是请求好友列表相关的 cgi 接口把返回的 JSON 里每个 uid 收集起来再去逐一请求他们的说说列表。接口路径形如/cgi-bin/..._get_friend_list域名和参数会随版本调整所以调试时先打印一次原始 JSON 确认结构再写解析逻辑。def fetch_friend_list(session, uin, gtk): params { uin: uin, g_tk: gtk, fupdate: 1, } r session.get( https://user.qzone.qq.com/proxy/domain/robert.qzone.qq.com/cgi-bin/mqzone_cgi_get_friend_list, paramsparams, timeout10, ) data r.json() friends [] for group in data.get(data, {}).get(groupList, []): for entry in group.get(hostList, []): friends.append(entry[uin]) return friends注意这里把数据解析成“groupList 里取 hostList”只是当前版本的常见结构如果返回的 code 是 0 但 friends 为空先检查原始 JSON 里字段名是不是变了。好友列表的过滤逻辑不要在脚本里猜腾讯返回哪些 uid 就以哪些为准。如果好友数量很大接口会按分组、按 uin 区间限流但最终都要做一次真实请求验证——拿 cookie 请求对方空间的动态列表接口能返回 200 且带动态才算“可访问”。3.3 说说动态解析与 zip 落地结构说说列表接口返回的 JSON 里核心字段是 createTime、content、pics。把每条动态转成统一 dict 后写到 zip代码import json import zipfile from datetime import datetime def save_feeds(friend_uin: str, feeds: list, archive: str): with zipfile.ZipFile(archive, a, zipfile.ZIP_DEFLATED) as zf: for idx, feed in enumerate(feeds): record { uin: friend_uin, create_time: feed.get(createTime), content: (feed.get(content) or ).strip(), pic_count: len(feed.get(pics) or []), pic_urls: [p.get(url) for p in (feed.get(pics) or [])], } date_str datetime.fromtimestamp(record[create_time]).strftime(%Y%m%d) path f{friend_uin}/{date_str}_{idx:04d}.json zf.writestr(path, json.dumps(record, ensure_asciiFalse, indent2))说明ZipFile 以追加模式打开边抓边写压缩包避免所有内容攒在内存里把进程拖垮。内容摘要把图片 URL 也记进去方便以后回看原图。为了兼容老动态的格式解析时要对取不到字段的情况兜底比如 content 可能缺失但图片存在此时最好把原始 JSON 一并放进 record方便事后回过头来补解析。zipfile 的压缩级别选 ZIP_DEFLATED实测下来体积大约是原始 JSON 的一半。4. 并发采集与风控避让requests 爬虫的节奏控制4.1 爬虫并发设计到底选哪个线程池、协程还是分布式说到“爬虫并发设计到底哪个好”需要先看清场景。QQ 空间动态抓取是典型的 IO 密集型任务请求发出去后大部分时间在等网络返回这时候引入并发能明显提速。可选方案有三个requests 多线程、aiohttp 协程、分布式抓取。对单人归档场景好友数量最多几十上百Python 爬虫里协程的异步调度省下的那点时间并不值当分布式爬虫更是过度设计——风控会先拦住高频请求。线程池的优势是代码直接、异常处理自然、限速容易控制和 requests 配合最顺手。from concurrent.futures import ThreadPoolExecutor, as_completed import random import time def crawl_friend(uin, session_factory, gtk): session session_factory() # 每个线程独立 Session feeds fetch_feeds(session, uin, gtk) time.sleep(random.uniform(0.3, 1.0)) # 随机间隔降低风控概率 return uin, len(feeds) with ThreadPoolExecutor(max_workers3) as pool: futures [pool.submit(crawl_friend, uin, create_session, gtk) for uin in friend_uins] for future in as_completed(futures): uin, count future.result() print(f{uin} 完成共 {count} 条)注意不要全局共用一个 Session 对象往里面写 cookierequests 的 Session 在并发写场景下可能出现 headers 覆盖。常见做法是外层有一个模板 sessioncrawl_friend 内部用工厂函数生成干净 Session只把同一份 Cookie 塞进去。线程数不要超过 5QQ 空间接口对单账号的并发限制比较严格多了只会换来验证码。4.2 必配的参数请求延迟、重试和随机窗口把请求节奏做成参数表方便直接抄参数推荐区间作用线程数3~5总并发上限请求间隔0.3~1.0 秒单线程内两次请求的停顿好友间随机延迟1~2 秒避开固定频率特征单接口重试次数3配合退避因子退避基数值1.0 秒重试等待时间按次方增长这里的随机窗口非常关键因为固定间隔本身就是一种容易被识别出的行为特征随机化之后的请求模式更像是真人操作。重试时也要随机退避而不是立刻重试否则刚好在风控窗口里反复撞同一堵墙。4.3 失败降级遇到验证码和 302 就该停def request_with_retry(session, url, params, max_retries3): for attempt in range(max_retries): try: resp session.get(url, paramsparams, timeout10) state check_login_state(resp) if state need_relogin: raise SystemExit(cookie 失效请重新导出) if state ok: return resp.json() except requests.Timeout: pass time.sleep(1.5 * (2 ** attempt) random.random()) raise RuntimeError(重试耗尽)timeout 设为 10 秒超过直接进入下一轮SystemExit 用于把登录态失效这种致命问题直接顶出去而不是当普通请求失败继续刷接口退避等待使用 1.5 秒乘 2 的 n 次方再叠加随机数避免退避时点整齐。重试只能解决临时性的网络抖动如果响应的 code 是负值且字段里出现 login_verify说明已经不是网络问题而是会话问题再重试只会加重风控果断停止让浏览器重新登录更划算。5. 增量抓取与 zip 热更新断点续传的实操技巧5.1 用一个游标文件记住抓到了哪里不要每次启动都从最新一条往前抓完而是维护一个本地游标 JSON记录每个好友已经抓到的最新时间戳。这样再次运行时只抓游标之后新增的内容配合定时任务就能长期做空间动态归档。with open(cursor.json, r) as f: cursor json.load(f) for uin in friend_uins: since cursor.get(uin, 0) feeds fetch_feeds_since(session, uin, since) save_feeds(uin, feeds, qzone_archive.zip) if feeds: cursor[uin] max(f[createTime] for f in feeds) with open(cursor.json, w) as f: json.dump(cursor, f, ensure_asciiFalse, indent2)createTime 的时间戳做游标比页码可靠因为分页可能因好友删除动态而错位。注意把游标更新的时机放在保存成功之后避免数据没写进 zip 就把游标往前推了那样一旦进程崩溃会漏掉中间一段。5.2 压缩包完整性校验zip 文件以追加模式写入存在格式不完整的风险特别是在脚本中途被强杀的场景。每次退出前用 testzip() 转一圈能及时发现损坏条目import zipfile if zipfile.ZipFile(qzone_archive.zip).testzip() is None: print(压缩包完整) else: print(存在损坏条目建议重新生成分卷归档)采集周期拉长之后建议按好友或按月分卷压缩文件名带日期区间避免单个 zip 过大也降低损坏后的恢复代价。每完成一个好友就把该好友的 JSON 落盘一次既能及时释放内存也为增量去重留下可靠的本地依据。完成这一步才真正达到“保存到本地.zip”且随时可查的状态。本文还有配套的精品资源点击获取
返回列表