Python爬虫实战:导出QQ空间说说并保存为Excel
1. 为什么我要自己导出QQ空间说说QQ空间这玩意儿说实话现在还在用的人不多了但里面躺着的东西是真不少。我从2010年前后开始断断续续发说说十几年下来攒了上千条有吐槽考试的、有半夜emo的、有给朋友庆生的还有一堆现在看了想钻地缝的中二发言。这些内容对我来说就是一份数字日记但问题是——它锁在腾讯的服务器里我想翻看只能一条条往下滑想搜索某个关键词基本没戏想做个时间线回顾更是天方夜谭。市面上确实有一些现成的导出工具但要么是收费的要么需要你把账号密码交给第三方要么就是导出格式乱七八糟。作为一个写了几年Python的人我第一反应就是这事儿我自己能不能干答案是可以而且比想象中简单。GetQzonehistory这个项目就是干这个的——用Python写一个轻量爬虫把你QQ空间里的说说抓下来整理成结构化的数据最后导出成Excel表格。整个过程不需要安装什么重型框架核心依赖就是requests加pandas代码量控制在几百行以内非常适合作为Python爬虫入门的实战项目。这篇文章我会把整个项目的设计思路、关键代码、踩过的坑全部摊开讲。不管你是刚学Python想找个练手项目还是跟我一样想把自己多年的说说存档都能直接照着做。我会重点讲清楚三件事怎么拿到数据、怎么处理数据、怎么存成Excel。中间涉及的反爬应对、登录态维持、数据清洗这些细节我都会给出具体的解决方案。提示本文涉及的所有操作仅用于导出你自己账号的个人数据请勿用于抓取他人信息或商业用途。爬虫技术本身是中性的关键在于使用者的边界意识。2. 动手之前先把这几件事想明白2.1 数据从哪来QQ空间说说的接口长什么样QQ空间的说说数据并不是直接嵌在HTML页面里的而是通过AJAX异步加载的。也就是说你打开user.qzone.qq.com看到的那一页只是壳子真正的数据是浏览器在后台发请求拿到的JSON。我们要做的就是模拟这个请求。说说的列表接口大致是这个形式https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6这个接口需要携带几个关键参数参数名含义说明uin目标用户的QQ号导出自己的就填自己的pos起始位置从0开始每次递增num每页数量一般设20太大容易被限制g_tk令牌从Cookie中计算得出核心难点format返回格式填json其中g_tk是最关键的一环。它不是随便填的而是需要从你登录后的Cookie里提取p_skey字段然后经过一个特定的哈希算法计算出来。这个算法网上有公开的实现核心逻辑是对p_skey做一次简单的字符串处理再取模。def get_g_tk(p_skey): h 5381 for c in p_skey: h (h 5) ord(c) return h 0x7fffffff这段代码看着简单但它是整个爬虫能不能跑通的前提。g_tk算错了接口直接返回403你连一条数据都拿不到。2.2 登录态怎么维持Cookie是绕不过去的坎QQ空间的接口全部需要登录态也就是说你必须带上有效的Cookie才能访问。这里有两种思路第一种是手动获取Cookie。你在浏览器里登录QQ空间按F12打开开发者工具在Network面板里找到任意一个请求把Cookie复制出来。这种方式最简单适合一次性导出缺点是Cookie有有效期过期了就得重新复制。第二种是模拟登录。用requests构造登录请求自动获取Cookie。但QQ的登录流程涉及加密参数、验证码、滑块验证等多重防护实现起来复杂度陡增对于这个轻量项目来说性价比不高。我的建议是走第一条路。你只需要在代码里留一个cookie变量把浏览器里复制的内容粘贴进去就行。虽然手动了一点但胜在稳定可靠不用跟腾讯的登录风控斗智斗勇。headers { Cookie: 你复制出来的Cookie字符串, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., Referer: https://user.qzone.qq.com/ }注意Cookie里包含了你的登录凭证千万不要把它提交到GitHub或者发给别人。建议用.env文件或者本地配置文件来存放代码里通过环境变量读取。2.3 为什么选Excel而不是数据库有人可能会问为什么不存到MySQL或者SQLite里答案很简单Excel的通用性最强。你导出的数据最终是要给人看的Excel几乎每个人都会打开排序、筛选、做透视表都方便。而数据库需要额外的环境配置对非技术用户不友好。当然如果你数据量特别大比如几万条Excel确实会有点吃力。但对于大多数人的QQ空间说说来说几千条顶天了Excel完全扛得住。而且pandas的to_excel()方法一行代码就能搞定导出省事。如果你确实想存数据库pandas也支持to_sql()配合SQLAlchemy可以无缝切换。这个项目里我会以Excel为主但会留一个数据库导出的扩展口子。3. 核心代码拆解从请求到落盘的完整链路3.1 分页抓取怎么把上千条说说全部拿下来QQ空间的说说接口是分页返回的每次最多返回20条num参数可以调大但超过一定值会被服务端截断。所以我们需要一个循环不断地递增pos参数直到接口返回的数据为空为止。import requests import time import pandas as pd def fetch_messages(uin, g_tk, cookie, total_limit2000): base_url https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6 headers { Cookie: cookie, User-Agent: Mozilla/5.0 ..., Referer: fhttps://user.qzone.qq.com/{uin} } all_msgs [] pos 0 num 20 while pos total_limit: params { uin: uin, pos: pos, num: num, g_tk: g_tk, format: json } resp requests.get(base_url, paramsparams, headersheaders, timeout10) data resp.json() msglist data.get(msglist) if not msglist: break for msg in msglist: all_msgs.append({ 发布时间: msg.get(created_time), 内容: msg.get(content, ), 点赞数: msg.get(like_count, 0), 评论数: msg.get(comment_count, 0), 转发数: msg.get(forward_count, 0), 图片数: len(msg.get(pic, [])), }) pos num time.sleep(1.5) # 关键控制请求频率 return all_msgs这段代码有几个细节值得展开说。第一time.sleep(1.5)不是可有可无的。腾讯的服务端有频率检测如果你不加延时连续快速请求几十次轻则返回空数据重则直接封禁你的IP一段时间。1.5秒是我实测下来比较安全的间隔既能保证速度又不会触发风控。如果你要抓的数据量很大建议把间隔调到2秒以上。第二total_limit是一个保护机制。万一接口出了问题导致pos一直递增但返回的数据始终不为空这个限制可以防止死循环。设成2000对于绝大多数用户来说足够了。第三msg.get(content, )用了默认值。有些说说只有图片没有文字content字段可能是空的甚至不存在用get方法可以避免KeyError。3.2 时间戳转换把一串数字变成人能看懂的时间接口返回的created_time是一个Unix时间戳比如1698765432这样的数字。直接存到Excel里没人看得懂需要转换成可读的日期格式。from datetime import datetime def convert_timestamp(ts): try: return datetime.fromtimestamp(int(ts)).strftime(%Y-%m-%d %H:%M:%S) except (ValueError, TypeError): return 这个转换看起来简单但有一个坑时区问题。datetime.fromtimestamp()默认使用你本机的时区如果你在国内得到的就是北京时间没问题。但如果你在服务器上跑服务器通常是UTC时区转换出来的时间会差8小时。解决办法是显式指定时区from datetime import datetime, timezone, timedelta def convert_timestamp(ts): try: tz timezone(timedelta(hours8)) return datetime.fromtimestamp(int(ts), tztz).strftime(%Y-%m-%d %H:%M:%S) except (ValueError, TypeError): return 这样不管你在哪里跑代码输出的都是北京时间跟你在QQ空间网页上看到的一致。3.3 数据清洗说说内容里的那些脏东西原始数据里有很多需要处理的东西。比如说说内容里可能包含HTML标签、表情符号的转义字符、多余的换行符等等。如果不处理导出的Excel会很难看。import re def clean_content(text): if not text: return # 去掉HTML标签 text re.sub(r[^], , text) # 把转义字符还原 text text.replace(nbsp;, ).replace(amp;, ) text text.replace(lt;, ).replace(gt;, ) # 去掉多余的空白 text re.sub(r\s, , text).strip() return text这里有个经验之谈不要过度清洗。有些人的说说里会带一些特殊的符号或者格式如果你全部删掉可能会丢失原本的语义。比如有人喜欢用【】来标注心情这种就应该保留。清洗的原则是只去掉影响阅读的噪音保留用户原本的表达。另外图片的处理也值得说一下。接口返回的pic字段是一个列表里面包含了图片的URL。如果你想把图片也存下来可以额外写一个下载函数def download_images(msg_list, save_dirimages): import os os.makedirs(save_dir, exist_okTrue) for i, msg in enumerate(msg_list): for j, pic_url in enumerate(msg.get(pic, [])): try: img_resp requests.get(pic_url, timeout10) with open(f{save_dir}/{i}_{j}.jpg, wb) as f: f.write(img_resp.content) except Exception as e: print(f图片下载失败: {e})不过图片下载会比较耗时而且占空间建议作为可选功能。3.4 导出Excelpandas的一行代码与背后的讲究数据清洗完之后导出就是一行代码的事df pd.DataFrame(all_msgs) df.to_excel(qzone_history.xlsx, indexFalse, engineopenpyxl)但这一行代码背后有几个值得注意的点。引擎选择pandas导出Excel需要依赖openpyxl或xlsxwriter。openpyxl更适合读写已有的Excel文件xlsxwriter更适合从零创建并做格式化。如果你只是简单导出用openpyxl就行记得pip install openpyxl。列宽调整默认导出的Excel列宽是固定的内容长了会显示不全。可以用xlsxwriter来设置列宽with pd.ExcelWriter(qzone_history.xlsx, enginexlsxwriter) as writer: df.to_excel(writer, indexFalse, sheet_name说说记录) worksheet writer.sheets[说说记录] worksheet.set_column(A:A, 20) # 时间列 worksheet.set_column(B:B, 80) # 内容列 worksheet.set_column(C:F, 10) # 互动数据列中文编码pandas导出Excel时默认使用UTF-8一般不会出现乱码。但如果你在Windows上遇到了乱码问题检查一下openpyxl的版本升级到最新版通常能解决。4. 实测中遇到的那些坑和应对方案4.1 接口返回403g_tk计算错误的排查过程我第一次跑代码的时候接口直接返回了403页面上写着权限不足。当时我以为是Cookie过期了重新复制了一遍还是不行。后来把请求URL打印出来仔细看发现g_tk的值是0。问题出在哪我检查了p_skey的提取逻辑发现Cookie字符串里p_skey的值后面跟了一个分号我没有去掉就直接传进哈希函数了。修正之后g_tk算出来是一个正常的整数接口立刻就能通了。这个坑的教训是从Cookie里提取字段时一定要把首尾的空白和分隔符去掉。正确的提取方式def extract_p_skey(cookie_str): for item in cookie_str.split(;): item item.strip() if item.startswith(p_skey): return item.split(, 1)[1] return 4.2 抓了几页之后返回空数据频率控制的边界在哪解决了403之后前几页抓得很顺利但抓到第5页左右的时候接口开始返回空的msglist。我一开始以为是数据抓完了但去网页上看明明还有好几百条。这就是典型的频率限制。腾讯的服务端在检测到短时间内大量相似请求后会暂时返回空数据但不会直接封禁。解决办法就是加大延时。我把time.sleep从0.5秒调到了1.5秒重新跑顺利抓完了全部数据。后来我又做了几次测试总结出一个经验值每请求一页延时1.5到2秒是比较安全的区间。如果你抓的数据量超过1000条建议把延时设到2.5秒虽然慢一点但胜在稳定。提示如果你在抓取过程中发现返回空数据不要立刻重试先等几分钟再继续。频繁重试只会让限制时间延长。4.3 说说内容里的表情和特殊字符导致Excel打不开有一次导出的Excel文件用WPS打开正常但用Microsoft Excel打开就报错提示文件格式无效。排查了半天发现是某条说说里包含了一个特殊的Unicode字符openpyxl在写入时没有正确处理。解决办法是在清洗阶段过滤掉不可打印的字符def remove_control_chars(text): return .join(c for c in text if unicodedata.category(c)[0] ! C)unicodedata.category(c)返回字符的类别以C开头的都是控制字符或未分配字符这些字符在Excel里会导致问题。过滤掉它们之后文件就能正常打开了。4.4 大量数据导出时的内存问题如果你要导出的说说有上万条一次性全部加载到内存里再导出可能会导致内存占用过高。解决办法是分批处理def export_in_batches(msg_list, batch_size500): for i in range(0, len(msg_list), batch_size): batch msg_list[i:ibatch_size] df pd.DataFrame(batch) mode w if i 0 else a header (i 0) with pd.ExcelWriter(qzone_history.xlsx, engineopenpyxl, modemode) as writer: df.to_excel(writer, indexFalse, headerheader, sheet_name说说记录)不过说实话对于QQ空间说说这种场景上万条的情况很少见。这个优化更多是出于代码健壮性的考虑。5. 让这个项目更好用的几个扩展方向5.1 加一个简单的命令行界面现在的代码需要手动改参数对非技术用户不太友好。可以用argparse加一个简单的命令行入口import argparse parser argparse.ArgumentParser(description导出QQ空间说说) parser.add_argument(--uin, requiredTrue, help你的QQ号) parser.add_argument(--cookie, requiredTrue, help浏览器Cookie) parser.add_argument(--output, defaultqzone_history.xlsx, help输出文件名) parser.add_argument(--limit, typeint, default2000, help最大抓取条数) args parser.parse_args()这样用起来就是python export_qzone.py --uin 123456789 --cookie 你的Cookie --output my_qzone.xlsx如果你想要图形界面可以用tkinter做一个简单的窗口输入框填QQ号和Cookie按钮点击开始导出。tkinter是Python自带的不需要额外安装适合做这种小工具。5.2 数据分析从说说里挖出点有意思的东西数据导出来之后除了存档还可以做点简单的分析。比如发说说的时段分布看看你习惯在什么时间发说说是深夜emo型还是早晨打卡型。年度发帖量趋势哪一年你发得最多哪一年几乎没发。高频词统计用jieba分词之后统计词频看看你最常提到什么。import jieba from collections import Counter all_text .join(df[内容].dropna()) words jieba.lcut(all_text) words [w for w in words if len(w) 1] counter Counter(words) print(counter.most_common(20))这些分析不需要很复杂的代码但能让你的数据变得更有趣。5.3 定时备份用schedule库实现自动导出如果你想让这个工具定期自动跑可以用schedule库import schedule import time def job(): msgs fetch_messages(uin, g_tk, cookie) df pd.DataFrame(msgs) df.to_excel(fbackup_{datetime.now().strftime(%Y%m%d)}.xlsx, indexFalse) schedule.every().week.do(job) while True: schedule.run_pending() time.sleep(60)这样每周自动导出一份相当于给你的QQ空间做了一个增量备份。不过要注意Cookie的有效期过期了需要手动更新。6. 关于爬虫这件事的一些个人体会写爬虫这些年我最大的感受是技术本身不难难的是把握分寸。QQ空间的接口就在那里你只要知道怎么构造请求就能拿到数据但拿到数据之后怎么用、用多少、多久用一次这些才是真正需要思考的问题。我在这个项目里加了很多限制延时、总量上限、只抓自己的数据。这些限制看起来让代码变慢了但它们让整个工具变得可持续。如果你不加限制地猛抓轻则IP被封重则账号被限制得不偿失。另外一点是数据的所有权。你导出的说说内容是你自己创造的你有权保存和备份。但如果你用同样的技术去抓别人的数据性质就变了。这个边界一定要清楚。最后说一个实际的小技巧导出之后立刻检查数据完整性。我遇到过好几次因为网络波动导致某几页数据丢失的情况如果不检查你可能过了很久才发现备份不完整。检查的方法很简单对比一下Excel里的条数和QQ空间网页上显示的说说总数差太多就重新跑一次。这个项目我前后迭代了三个版本从最开始的硬编码到后来的命令行工具代码量始终控制在500行以内。它不是什么高大上的东西但确实解决了我自己的一个真实需求。如果你也有类似的需求直接拿去改改就能用。