
1. 为什么我要写一个QQ空间说说导出工具QQ空间这玩意儿说起来挺有意思。2005年上线到2018年前后月活还能破6亿很多人的青春记忆、中二发言、非主流签名、暗恋对象的留言互动全都沉淀在那个叫“说说”的功能里。我从2010年开始用QQ空间到2016年基本停更六年时间攒了将近两千条说说。有段时间想把这些内容导出来做个纪念册结果发现官方压根没有导出功能手动复制粘贴两千条每条还要点开、全选、复制、切到Excel、粘贴这活儿干到天亮也干不完。于是我就琢磨着写个爬虫。网上搜了一圈发现现成的工具要么年久失修跑不起来要么需要各种复杂配置要么就是收费的。干脆自己动手用Python写了一个轻量级的QQ空间说说导出工具取名GetQzonehistory。核心思路很简单模拟登录拿到Cookie然后调用QQ空间说说的分页接口把数据抓下来清洗之后写入Excel。整个项目不到500行代码依赖也就requests、pandas、openpyxl这几个常用库跑起来很轻快。这篇文章我会把这个项目的完整实现思路、关键代码、踩过的坑、以及怎么根据自己的需求做二次开发全都掰开揉碎讲清楚。适合有Python基础、想学爬虫实战的朋友也适合单纯想把QQ空间说说导出来留个纪念的普通用户。哪怕你之前没写过爬虫跟着我的步骤走也能跑通。2. 项目整体设计与技术选型2.1 为什么选Python而不是其他语言做爬虫这件事Python几乎是默认选项。原因不复杂requests库处理HTTP请求简单到令人发指pandas处理数据清洗和导出Excel就是几行代码的事再加上BeautifulSoup或者直接解析JSON整个链路非常顺畅。如果用Java写光是配Maven依赖和写一堆Getter/Setter就够烦的用Node.js也行但数据处理这块pandas的生态优势太明显了。具体到这个项目我的技术栈是这样的组件选型理由HTTP请求requests简洁、稳定、文档全处理Cookie和Session非常方便数据解析json reQQ空间接口返回的是JSONP格式用正则剥壳后直接json解析数据清洗pandas处理时间格式、去重、字段重命名一气呵成Excel导出openpyxlpandas的Excel写入引擎支持xlsx格式兼容性好进度显示tqdm抓取几千条数据时有个进度条心里踏实有人可能会问为什么不用Scrapy说实话Scrapy确实强大但对于这种单站点、数据量不大、逻辑简单的任务来说Scrapy的复杂度反而是一种负担。你需要定义Item、Pipeline、Middleware调试起来也麻烦。requests pandas的组合代码量少可读性强出了问题容易定位更适合这种轻量级场景。2.2 核心思路拆解整个项目的逻辑链条可以概括为四步登录获取CookieQQ空间的说说接口需要登录态才能访问所以第一步是拿到有效的Cookie。分页抓取数据接口返回的是分页数据每页固定条数通过循环翻页把所有说说抓下来。数据清洗与结构化原始数据里有很多冗余字段和HTML标签需要清洗成干净的表格结构。写入Excel用pandas把清洗后的数据写入xlsx文件方便查看和二次处理。这四步里第一步是最容易出问题的因为QQ空间的登录机制这些年一直在变。第二步的难点在于接口参数的理解和翻页逻辑的处理。第三步和第四步相对简单但也有一些细节需要注意比如时间戳转换、内容中的特殊字符处理等。2.3 项目目录结构我习惯把项目结构保持简洁不搞太多花里胡哨的模块拆分。GetQzonehistory的目录大概长这样GetQzonehistory/ ├── main.py # 入口文件负责调度整个流程 ├── login.py # 登录模块获取Cookie ├── fetch.py # 数据抓取模块负责分页请求 ├── clean.py # 数据清洗模块 ├── export.py # Excel导出模块 ├── config.py # 配置文件存放请求头、接口地址等 └── output/ # 导出文件存放目录这种拆分方式的好处是每个模块职责单一调试的时候可以单独跑某个模块不用每次都从头开始。比如Cookie过期了只需要重新跑login.py就行不用重新抓数据。3. 登录模块拿到那把钥匙3.1 QQ空间登录机制简析QQ空间的登录体系经历过多次迭代早期可以用账号密码直接模拟登录后来加了各种验证码、设备校验、滑块验证纯代码模拟登录的难度越来越大。目前比较稳妥的方案是手动登录 自动提取Cookie。具体操作是用Selenium或者Playwright打开QQ空间登录页面手动扫码或者输入账号密码登录登录成功后从浏览器中提取Cookie保存到本地文件。后续抓取数据时直接加载这个Cookie文件只要Cookie没过期就不用重复登录。这种方案的好处是绕开了复杂的登录验证逻辑稳定性高。缺点是Cookie有有效期一般几天到几周不等过期后需要重新登录一次。但对于导出说说这种一次性任务来说完全够用了。3.2 用Selenium获取Cookie的实操步骤先安装依赖pip install selenium webdriver-manager然后写一个简单的登录脚本from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager import time import json def login_and_save_cookie(): # 初始化浏览器 service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice) # 打开QQ空间登录页 driver.get(https://qzone.qq.com/) # 等待手动登录这里给60秒时间 print(请在浏览器中完成登录登录成功后脚本会自动继续...) time.sleep(60) # 提取Cookie cookies driver.get_cookies() # 保存到本地文件 with open(cookies.json, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2) print(f已保存 {len(cookies)} 条Cookie到cookies.json) driver.quit() if __name__ __main__: login_and_save_cookie()这段代码的逻辑很直白打开浏览器等你手动登录然后自动把Cookie抓下来存成JSON文件。60秒的等待时间可以根据实际情况调整如果你手速快30秒也够如果网络慢可以设成120秒。注意webdriver-manager会自动下载匹配的ChromeDriver省去了手动配置驱动的麻烦。但前提是你电脑上已经装了Chrome浏览器。如果用的是Edge或者其他浏览器需要换成对应的驱动。3.3 Cookie的加载与验证拿到Cookie之后下一步是在请求中加载它。这里有个细节从Selenium提取的Cookie是列表格式每个元素是一个字典包含name、value、domain等字段。requests库需要的Cookie格式是字典或者RequestsCookieJar所以需要做一次转换import json import requests def load_cookie(): with open(cookies.json, r, encodingutf-8) as f: cookies_list json.load(f) # 转换成requests需要的格式 cookies {} for item in cookies_list: cookies[item[name]] item[value] return cookies def verify_cookie(cookies): 验证Cookie是否有效 session requests.Session() session.cookies.update(cookies) # 用一个简单的接口测试 test_url https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6 params { uin: 你的QQ号, ftype: 0, sort: 0, pos: 0, num: 1, replynum: 100, g_tk: 0, callback: _preloadCallback, code_version: 1, format: jsonp, need_private_comment: 1 } resp session.get(test_url, paramsparams) if msglist in resp.text: print(Cookie有效) return True else: print(Cookie已失效请重新登录) return False验证Cookie是否有效这一步很重要。我踩过的坑是Cookie过期后接口不会返回401或者403而是返回一个空的msglist或者跳转到登录页的HTML。如果不做验证直接跑抓取脚本会得到一堆空数据还以为是代码写错了。3.4 关于g_tk参数的说明QQ空间的接口有一个叫g_tk的参数它是根据Cookie中的skey计算出来的一个校验值。早期版本的接口对g_tk校验不严传0也能用。但后来部分接口开始严格校验g_tk传错了会返回错误码。g_tk的计算逻辑是这样的def calculate_g_tk(skey): 根据skey计算g_tk hash_val 5381 for char in skey: hash_val (hash_val 5) ord(char) return hash_val 0x7fffffff这个算法的原理是经典的DJ Bernstein哈希QQ空间用它来做简单的请求校验。在实际使用中如果接口返回“请先登录”之类的提示大概率是g_tk没算对。不过根据我的实测说说列表接口目前对g_tk的校验比较宽松传0也能正常返回数据。但为了保险起见建议还是算一下。4. 数据抓取翻页逻辑与接口参数4.1 说说接口的请求结构QQ空间说说的列表接口地址是https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6这个接口的关键参数有以下几个参数名含义示例值uin目标QQ号123456789ftype类型过滤0全部sort排序方式0按时间倒序pos起始位置0, 20, 40...num每页条数20replynum评论抓取数100format返回格式jsonpg_tk校验值计算得出或传0其中pos和num是控制翻页的核心参数。pos表示从第几条开始取num表示取多少条。比如第一页是pos0num20第二页就是pos20num20以此类推。4.2 翻页抓取的完整实现import requests import json import re import time from tqdm import tqdm def fetch_all_feeds(cookies, qq_number, total_countNone): 抓取所有说说 session requests.Session() session.cookies.update(cookies) base_url https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: fhttps://user.qzone.qq.com/{qq_number}, } all_feeds [] pos 0 page_size 20 # 先请求一次获取总数 if total_count is None: total_count get_total_count(session, base_url, headers, qq_number) # 计算总页数 total_pages (total_count page_size - 1) // page_size # 用tqdm显示进度 with tqdm(totaltotal_pages, desc抓取说说) as pbar: while pos total_count: params { uin: qq_number, ftype: 0, sort: 0, pos: str(pos), num: str(page_size), replynum: 100, g_tk: 0, callback: _preloadCallback, code_version: 1, format: jsonp, need_private_comment: 1 } try: resp session.get(base_url, paramsparams, headersheaders, timeout10) resp.raise_for_status() # 剥掉JSONP外壳 json_str resp.text json_str re.sub(r^_preloadCallback\(, , json_str) json_str re.sub(r\);?$, , json_str) data json.loads(json_str) if msglist not in data or not data[msglist]: break all_feeds.extend(data[msglist]) pos page_size pbar.update(1) # 控制请求频率避免触发风控 time.sleep(1.5) except Exception as e: print(f\n抓取第{pos//page_size 1}页时出错: {e}) time.sleep(5) continue return all_feeds def get_total_count(session, url, headers, qq_number): 获取说说总数 params { uin: qq_number, ftype: 0, sort: 0, pos: 0, num: 1, replynum: 0, g_tk: 0, callback: _preloadCallback, code_version: 1, format: jsonp, need_private_comment: 0 } resp session.get(url, paramsparams, headersheaders) json_str re.sub(r^_preloadCallback\(, , resp.text) json_str re.sub(r\);?$, , json_str) data json.loads(json_str) return data.get(total, 0)这段代码有几个关键点需要展开说JSONP剥壳QQ空间的接口返回的不是纯JSON而是包了一层_preloadCallback(...)的JSONP格式。所以需要先用正则把外壳剥掉再解析JSON。这个callback参数是可以自定义的但为了简单我固定用了_preloadCallback。请求频率控制time.sleep(1.5)这行代码很重要。QQ空间有风控机制如果请求太频繁会返回错误码或者直接封IP。1.5秒的间隔是我实测下来比较安全的数值抓2000条说说大概需要2-3分钟完全可以接受。如果你追求速度可以降到1秒但再低就有风险了。异常处理网络请求难免出错所以用try-except包起来出错后等5秒重试。这里没有做无限重试因为如果是Cookie失效导致的错误重试再多次也没用。实际使用中如果连续多页都失败建议检查Cookie是否过期。4.3 返回数据的字段结构接口返回的每条说说数据包含很多字段我挑几个核心的列一下字段名含义备注tid说说ID唯一标识content说说正文可能包含HTML标签created_time发布时间Unix时间戳commentlist评论列表嵌套结构pic图片列表包含图片URLrt_con转发内容如果是转发说说cmtnum评论数数字fwdnum转发数数字like点赞信息包含点赞人数这些字段里content和created_time是最核心的也是导出Excel时必须要有的。commentlist和pic属于可选字段如果不需要可以跳过能减少不少数据量。5. 数据清洗从原始JSON到干净表格5.1 时间戳转换与格式化接口返回的created_time是Unix时间戳比如1698765432直接看根本不知道是哪天。需要转换成可读的日期格式from datetime import datetime def timestamp_to_str(ts): Unix时间戳转日期字符串 try: return datetime.fromtimestamp(int(ts)).strftime(%Y-%m-%d %H:%M:%S) except (ValueError, TypeError, OSError): return 这里用fromtimestamp而不是utcfromtimestamp因为QQ空间返回的时间戳是基于本地时区的。如果你发现转换出来的时间差了8小时检查一下是不是用了UTC转换。5.2 正文内容的清洗说说正文里经常包含HTML标签、表情代码、提及等需要做一轮清洗import re def clean_content(content): 清洗说说正文 if not content: return # 去掉HTML标签 content re.sub(r[^], , content) # 去掉表情代码比如[em]e100[/em] content re.sub(r\[em\].*?\[/em\], , content) # 去掉多余的空白字符 content re.sub(r\s, , content).strip() # 处理HTML实体 content content.replace(nbsp;, ) content content.replace(amp;, ) content content.replace(lt;, ) content content.replace(gt;, ) content content.replace(quot;, ) return content清洗的时候有个取舍表情代码要不要保留我的做法是直接去掉因为导出到Excel之后[em]e100[/em]这种代码看起来很奇怪而且Excel也没法渲染成表情图片。如果你想把表情保留下来可以替换成对应的文字描述比如[微笑]、[大哭]之类的但这需要一个映射表工作量不小。5.3 评论数据的提取评论是嵌套在说说数据里的结构大概是这样{ commentlist: [ { content: 评论内容, createTime: 2023-10-01 12:00, name: 评论者昵称, uin: 123456 } ] }提取评论的时候我选择把评论拼成一个字符串用分号隔开放在一个单元格里。这样做的原因是Excel不适合展示一对多的嵌套结构如果每条评论单独一行说说正文就要重复很多次看起来很乱。def extract_comments(commentlist): 提取评论内容 if not commentlist: return comments [] for cmt in commentlist: name cmt.get(name, 匿名) content clean_content(cmt.get(content, )) if content: comments.append(f{name}: {content}) return | .join(comments)5.4 构建DataFrame清洗完数据之后用pandas构建DataFrameimport pandas as pd def build_dataframe(feeds): 把原始数据转成DataFrame rows [] for feed in feeds: row { 说说ID: feed.get(tid, ), 发布时间: timestamp_to_str(feed.get(created_time, 0)), 正文: clean_content(feed.get(content, )), 评论: extract_comments(feed.get(commentlist, [])), 评论数: feed.get(cmtnum, 0), 转发数: feed.get(fwdnum, 0), 点赞数: len(feed.get(like, {}).get(likeList, [])) if feed.get(like) else 0, 图片来源: 原创 if not feed.get(rt_con) else 转发, } rows.append(row) df pd.DataFrame(rows) # 按时间排序 df df.sort_values(发布时间, ascendingFalse).reset_index(dropTrue) return df这里我加了几个衍生字段评论数、转发数、点赞数、图片来源。这些字段在原始数据里要么是嵌套结构要么需要计算单独拎出来放在表格里更直观。6. 导出Excel让数据好看又好用6.1 用pandas写入xlsx导出这一步用pandas的to_excel方法就行def export_to_excel(df, output_path): 导出DataFrame到Excel with pd.ExcelWriter(output_path, engineopenpyxl) as writer: df.to_excel(writer, sheet_name说说列表, indexFalse) # 获取worksheet对象做格式调整 worksheet writer.sheets[说说列表] # 设置列宽 column_widths { A: 15, # 说说ID B: 20, # 发布时间 C: 60, # 正文 D: 50, # 评论 E: 10, # 评论数 F: 10, # 转发数 G: 10, # 点赞数 H: 10, # 图片来源 } for col, width in column_widths.items(): worksheet.column_dimensions[col].width width print(f已导出到 {output_path})列宽的设置很关键。默认情况下Excel的列宽是固定的正文那一列如果不加宽内容会被截断看起来很难受。我一般把正文列设成60评论列设成50这样大部分内容都能完整显示。6.2 处理Excel的字符限制Excel单元格有字符数限制最大是32767个字符。一般来说说说正文不会超过这个数但如果你把几百条评论拼在一起就有可能超限。处理方法是截断def truncate_cell(text, max_len32000): 截断过长的单元格内容 if len(text) max_len: return text[:max_len] ...(内容过长已截断) return text在构建DataFrame的时候对正文和评论字段都做一次截断处理避免写入Excel时报错。6.3 多Sheet导出方案如果你想把说说、评论、图片分开导出可以用多个Sheetdef export_multi_sheet(df, feeds, output_path): 多Sheet导出 with pd.ExcelWriter(output_path, engineopenpyxl) as writer: # Sheet1: 说说列表 df.to_excel(writer, sheet_name说说列表, indexFalse) # Sheet2: 评论详情 comment_rows [] for feed in feeds: for cmt in feed.get(commentlist, []): comment_rows.append({ 说说ID: feed.get(tid, ), 评论者: cmt.get(name, ), 评论内容: clean_content(cmt.get(content, )), 评论时间: cmt.get(createTime, ), }) if comment_rows: comment_df pd.DataFrame(comment_rows) comment_df.to_excel(writer, sheet_name评论详情, indexFalse) # Sheet3: 图片链接 pic_rows [] for feed in feeds: for pic in feed.get(pic, []): pic_rows.append({ 说说ID: feed.get(tid, ), 图片URL: pic.get(url1, ) or pic.get(url2, ), }) if pic_rows: pic_df pd.DataFrame(pic_rows) pic_df.to_excel(writer, sheet_name图片链接, indexFalse)多Sheet的好处是数据分类清晰想看评论就去评论Sheet想下载图片就去图片Sheet。缺点是文件会大一些但现在的硬盘容量这点大小根本不算什么。7. 常见问题与排查技巧实录7.1 Cookie失效的几种表现Cookie失效是最常见的问题表现有以下几种表现原因解决方法返回空msglistCookie过期重新登录获取新Cookie返回登录页HTMLCookie完全失效重新登录返回错误码-3000g_tk校验失败重新计算g_tk请求超时网络问题或IP被限换网络或等一段时间排查的时候先把返回的原始文本打印出来看看。如果里面包含“登录”字样基本可以确定是Cookie问题。7.2 抓取到一半中断怎么办如果抓取过程中程序崩溃或者网络中断已经抓到的数据会丢失。解决方法是加一个断点续传机制import os import pickle def save_progress(feeds, pos, filenameprogress.pkl): 保存抓取进度 with open(filename, wb) as f: pickle.dump({feeds: feeds, pos: pos}, f) def load_progress(filenameprogress.pkl): 加载抓取进度 if os.path.exists(filename): with open(filename, rb) as f: return pickle.load(f) return {feeds: [], pos: 0}每抓完一页就保存一次进度下次启动时先检查有没有进度文件有的话从上次的位置继续抓。这个机制在抓取大量数据时非常实用我抓一个有一万多条说说的账号时中途断了三次全靠断点续传才抓完。7.3 正文中的特殊字符导致Excel报错有些说说正文里包含控制字符比如\x00、\x01这些字符在Excel里是非法的写入时会报错。处理方法是过滤掉def remove_control_chars(text): 移除控制字符 import unicodedata return .join( ch for ch in text if unicodedata.category(ch)[0] ! C or ch in \n\r\t )在清洗正文的时候调用这个函数把非法字符过滤掉就能避免写入Excel时报错。7.4 抓取速度与风控的平衡QQ空间的风控策略是动态的有时候1.5秒的间隔没事有时候1秒就被限。我的经验是抓取前100条时可以用1秒间隔试探如果连续成功保持1.5秒如果出现失败立刻降到3秒如果还是失败停止抓取等半小时再试另外请求头里的User-Agent和Referer也要设置正确。Referer要指向目标QQ号的空间首页否则容易被识别为异常请求。7.5 常见问题速查表问题可能原因排查方法解决方案抓不到数据Cookie失效打印返回文本重新登录数据不完整翻页逻辑错误检查pos参数修正pos递增逻辑Excel打不开文件被占用关闭已打开的Excel关闭后重试中文乱码编码问题检查文件编码统一用utf-8程序卡住网络超时加timeout参数设置10秒超时评论为空replynum设太小检查replynum参数改成100或更大8. 二次开发与功能扩展8.1 增加图片下载功能如果你想把说说里的图片也保存到本地可以加一个下载模块import os import requests def download_images(feeds, save_dirimages): 下载说说中的图片 if not os.path.exists(save_dir): os.makedirs(save_dir) for feed in feeds: tid feed.get(tid, ) pics feed.get(pic, []) for i, pic in enumerate(pics): url pic.get(url1) or pic.get(url2) if not url: continue try: resp requests.get(url, timeout15) if resp.status_code 200: filename f{tid}_{i}.jpg filepath os.path.join(save_dir, filename) with open(filepath, wb) as f: f.write(resp.content) except Exception as e: print(f下载图片失败: {url}, 错误: {e})下载图片的时候要注意QQ空间的图片URL有时效性过期后会返回403。所以最好在抓取说说的同时就下载图片不要等几天后再下载。8.2 导出为Markdown格式除了Excel还可以导出为Markdown方便导入到笔记软件def export_to_markdown(df, output_path): 导出为Markdown格式 with open(output_path, w, encodingutf-8) as f: f.write(# QQ空间说说备份\n\n) for _, row in df.iterrows(): f.write(f## {row[发布时间]}\n\n) f.write(f{row[正文]}\n\n) if row[评论]: f.write(f 评论: {row[评论]}\n\n) f.write(---\n\n) print(f已导出Markdown到 {output_path})Markdown格式的好处是可读性强可以直接用文本编辑器打开也方便导入到Obsidian、Notion等笔记工具里。8.3 定时自动备份如果你想定期自动备份说说可以用schedule库做一个定时任务import schedule import time def auto_backup(): 自动备份任务 cookies load_cookie() if not verify_cookie(cookies): print(Cookie已失效请重新登录) return feeds fetch_all_feeds(cookies, 你的QQ号) df build_dataframe(feeds) # 按日期命名文件 from datetime import datetime date_str datetime.now().strftime(%Y%m%d) output_path foutput/qzone_{date_str}.xlsx export_to_excel(df, output_path) print(f自动备份完成: {output_path}) # 每周日凌晨2点执行 schedule.every().sunday.at(02:00).do(auto_backup) while True: schedule.run_pending() time.sleep(60)这个方案适合把脚本部署在服务器上定期自动备份。但要注意Cookie过期的问题如果Cookie失效了自动备份会失败需要手动重新登录。8.4 数据可视化分析导出数据之后还可以用matplotlib或者pyecharts做可视化分析比如每年发说说的数量趋势发说说的时段分布高频词汇统计互动最多的好友排名import matplotlib.pyplot as plt def plot_yearly_trend(df): 绘制每年说说数量趋势 df[年份] pd.to_datetime(df[发布时间]).dt.year yearly_count df.groupby(年份).size() plt.figure(figsize(10, 6)) yearly_count.plot(kindbar) plt.title(每年说说数量) plt.xlabel(年份) plt.ylabel(数量) plt.tight_layout() plt.savefig(output/yearly_trend.png) plt.show()这种分析做出来挺有意思的能直观看到自己这些年的表达欲变化。我自己导出来一看2013年发了500多条2016年之后断崖式下跌一年不到50条挺感慨的。9. 一些实操心得与避坑建议9.1 关于Cookie的保存安全Cookie文件里包含你的登录凭证相当于一把钥匙。保存的时候要注意不要把Cookie文件上传到GitHub或者网盘不要分享给他人定期清理过期的Cookie文件如果怀疑泄露立刻在QQ安全中心退出所有设备我在代码里把Cookie保存在本地JSON文件并且加了.gitignore规则防止误提交到代码仓库。9.2 抓取频率的控制经验我前后抓过五个QQ号的说说总计大概两万多条数据。总结下来的频率控制经验是新号或者活跃号风控更严建议2秒间隔老号或者不活跃号1.5秒基本没问题凌晨时段2点-6点风控相对宽松可以适当提速如果连续抓取超过5000条建议中途休息10分钟这些经验不一定适用于所有人但作为一个参考基准是没问题的。核心原则是宁可慢一点也不要触发风控导致IP被封。9.3 Excel导出的性能优化当数据量超过一万条时pandas写入Excel会变得很慢。优化方法有使用xlsxwriter引擎代替openpyxl写入速度更快关闭自动计算和格式检查分批写入每5000条保存一次# 使用xlsxwriter引擎 with pd.ExcelWriter(output_path, enginexlsxwriter) as writer: df.to_excel(writer, sheet_name说说列表, indexFalse)实测下来xlsxwriter比openpyxl快大概30%-40%但xlsxwriter不支持读取Excel只能写入。如果你只需要导出用xlsxwriter更合适。9.4 关于数据备份的长期策略说说数据导出来之后建议做多重备份本地硬盘存一份云盘存一份注意加密打印一份纸质版如果特别重要我自己的做法是每年导出一次存成Excel和Markdown两种格式分别放在不同的地方。Excel方便查看和筛选Markdown方便阅读和导入笔记软件。9.5 法律与道德层面的提醒最后说一个容易被忽略的点抓取自己的QQ空间数据是完全合法的但不要用这个工具去抓别人的说说。即使技术上能做到未经授权抓取他人数据可能涉及隐私侵权。这个工具的设计初衷是帮助用户备份自己的数据请务必在合法合规的范围内使用。另外导出的数据里可能包含他人的评论和互动信息分享或公开这些数据时要注意保护他人隐私必要时做匿名化处理。10. 项目源码与使用说明10.1 完整依赖列表requests2.28.0 pandas1.5.0 openpyxl3.0.0 selenium4.0.0 webdriver-manager3.8.0 tqdm4.64.0安装命令pip install -r requirements.txt10.2 快速开始# 第一步登录并保存Cookie python login.py # 第二步抓取并导出 python main.py --qq 你的QQ号 --output output/qzone.xlsxmain.py的入口逻辑import argparse from login import load_cookie, verify_cookie from fetch import fetch_all_feeds from clean import build_dataframe from export import export_to_excel def main(): parser argparse.ArgumentParser(descriptionQQ空间说说导出工具) parser.add_argument(--qq, requiredTrue, helpQQ号) parser.add_argument(--output, defaultoutput/qzone.xlsx, help输出文件路径) args parser.parse_args() # 加载Cookie cookies load_cookie() if not verify_cookie(cookies): print(Cookie无效请先运行 login.py 重新登录) return # 抓取数据 print(f开始抓取 {args.qq} 的说说...) feeds fetch_all_feeds(cookies, args.qq) print(f共抓取 {len(feeds)} 条说说) # 清洗数据 df build_dataframe(feeds) # 导出Excel export_to_excel(df, args.output) if __name__ __main__: main()10.3 配置文件说明config.py里存放一些可调整的参数# 请求间隔秒 REQUEST_INTERVAL 1.5 # 每页条数 PAGE_SIZE 20 # 请求超时秒 REQUEST_TIMEOUT 10 # 最大重试次数 MAX_RETRIES 3 # 输出目录 OUTPUT_DIR output # Cookie文件路径 COOKIE_FILE cookies.json这些参数可以根据自己的网络环境和抓取量做调整。如果网络稳定可以把REQUEST_INTERVAL降到1.0如果经常超时把REQUEST_TIMEOUT加到20。10.4 运行环境要求Python 3.8及以上Chrome浏览器用于Selenium登录操作系统Windows/macOS/Linux均可内存建议4GB以上硬盘至少500MB可用空间如果下载图片这个工具对硬件要求很低我用一台2015年的老笔记本跑过抓2000条说说大概3分钟完全没问题。10.5 后续可以扩展的方向这个项目目前的完成度已经够用了但如果想继续折腾还有几个方向可以扩展支持导出为PDF格式方便打印增加GUI界面让不懂命令行的用户也能用支持增量备份只抓取上次备份之后的新说说增加数据统计面板展示发说说频率、互动排行等支持多账号批量备份我个人最想加的是增量备份功能因为每次全量抓取都要花几分钟如果只抓新增的几秒钟就搞定了。实现思路是记录上次抓取的最新说说ID下次抓取时遇到这个ID就停止。这个工具我从2022年写到现在前后改了十几版踩了不少坑也积累了一些经验。最开始用Scrapy写发现太重了后来改用requests代码量直接砍了一半。Cookie获取从最初的模拟登录改成Selenium手动登录稳定性提升了很多。数据清洗那块也反复调整过主要是处理各种奇怪的HTML标签和特殊字符。如果你也在用这个工具或者基于它做了二次开发欢迎交流。数据备份这件事早做早安心别等到哪天平台关停了才后悔。