资讯动态

QQ空间备份实战:Python脚本实现增量归档与本地搜索

发布时间:2026/10/15 16:03:49 来源:尧图企业网站定制
简介QZoneExport是一款面向QQ空间用户的开源备份工具基于项目主分支打包而成主要解决个人社交数据难以本地留存的问题。备份内容覆盖说说、日志、相册、留言等常见类型可帮助将宝贵记忆导出到本地妥善保存同时兼顾开发者学习QQ空间API的进阶需求。压缩包共149个文件结构清晰60个JavaScript文件承担数据抓取与逻辑处理22个HTML文件组成操作界面11个CSS文件负责页面样式另有46个PNG图片及少量SVG、GIF等资源提供图标与动态提示并附带PowerShell脚本、说明文档及许可证整体体积仅2.59MB轻便易用。目前已有947人浏览学习实用价值得到初步验证。使用者只需按提示完成授权登录即可选择需要的模块进行备份生成结构化的本地数据便于日后查阅或迁移开发者则能从源码中了解QQ空间接口的调用流程、返回数据的解析方式以及前端界面的组织思路无论是为了存档还是二次开发都能从中获得清晰的参考。工具以图形界面降低操作门槛也保留了持续更新能力方便适配QQ空间后续的接口调整。1. 为什么说 QQ空间备份 111111 要先定口径再动手QQ空间备份 111111 这个标题最容易被当成一串无意义的编号实际上它讲的是一件很现实的事把自己账号下的说说、日志、相册、留言板等内容做成一份可离线查阅、可搜索、可迁移的本地存档。111111 可以理解成某次备份任务的批次标记也可以理解成某个固定时段的快照标识关键不在数字本身而在于它逼着你回答三个问题——备份哪些内容、用什么方式抓、后续怎么持续更新。我见过不少把备份想简单了的人以为点开网页另存一遍就完事真到找回数据时才发现线上能看到的只是冰山一角没加载的评论、原图、被压缩的视频全都丢了。也有人熬夜写脚本第二天倒在了登录态这一关上。这篇文章不打算讲玄学只讲一条走通过的路径备份前怎么定范围写一个最小可维护的 Python 备份脚本要做什么准备参数怎么设哪些坑值得提前绕开以及备份之后如何让这份数据真正用起来。适合正在做个人数据归档、想摆脱对在线页面依赖的开发者和有一定命令行基础的技术用户。2. 备份之前的三个口径决策触发方式、内容边界、增量策略动手之前先别碰代码上面这三个决策才是整个备份项目的地基。触发方式决定你要不要写脚本内容边界决定你要拉多少数据增量策略决定这份备份是跑一次就扔还是能长期自动维护。三者互相咬合漏掉其中一个备份做到最后大概率要么少文件要么大量重复。2.1 手动备份和脚本备份的分界什么时候不用写代码也够用如果目标只是把几百条公开的说说内容留个底不要求图片原图、不要求评论区、不要求以后逐日更新手动备份是性价比最高的方案。常见做法是直接打开自己的空间主页一条条往下滚动触发懒加载全部展开后用浏览器自带的“另存为完整网页”保存页面。这里有个前提页面本身是登录态下渲染的存下来的 HTML 会把已加载的正文和部分缩略图引用一起带走原图能不能保住取决于加载策略。什么时候必须转脚本我的判断标准看三个信号内容超过一千条、图片只显示缩略图、需要按日期定期重跑。手动滚动在数据量小的时候是几十分钟的事数据量大以后翻页本身会触发频繁请求很容易被限制访问中途断一次还要从头数位置血泪经验。脚本真正的价值不是省那几分钟点击而是把“备份到什么位置”交给游标记录每次都能断点续跑。对比项手动备份脚本备份适用量级几百条一次性上千条、持续更新图片质量往往只有缩略图可控可拉原图增量能力无全量重来有游标断点续传维护成本零代码需要处理登录态和接口变化2.2 内容边界说说、日志、相册、留言板怎么选QQ空间的产品结构里个人数据大致分五个模块说说、日志、相册、留言板、个人资料。很多人一上来就想全都要结果脚本写着写着臃肿到跑不动。我更推荐把内容切成“最小集”和“扩展集”两轮来做。第一轮最小集只做三件事说说正文、配图文件、发布时间。这是空间里信息密度最高、最容易被删除影响的部分。评论和点赞数量可以先只存一个统计数字具体评论内容丢进 TODO等正文链路稳定后再接。相册里的图很可能和说说里的配图是同一份原图如果两处都拉磁盘上会重复占空间需要在文件命名时以图片原始 hash 做去重。留言板的隐私属性更强而且往往要翻很多页才能取完我一般建议放到第二轮。做内容边界决策时真正要问自己的是这份备份存在的目的是什么。如果是个人纪念存档说说加相册足够如果要留证据类材料日志、留言板、评论明细一个都不能少。目的不同存储结构完全不同先把目的写进 README后面脚本写歪了还有得救。2.3 增量策略时间戳和游标谁更可靠接口拉取最核心的问题是你怎么知道哪些内容是新的。最简单粗暴的做法是拿内容发布时间做过滤每次只备份最近 N 天的数据。但时间戳策略有两个隐藏问题同一秒内可能出现多条内容只按时间分界会漏掉边界上的那几条内容被编辑后时间可能变下一轮全量重扫又会把旧数据当新的重复落盘。更可靠的做法是让分页响应里的游标字段来记忆位置。这个字段在内部实现里通常是一个偏移量或是一串不透明的 ID它的含义就是“你已经取到哪里了”下一次请求带上它服务器从那个位置之后继续给数据。你要做的只是把每次响应返回的新游标原样存下来备份中断后从上一次的位置接着跑就行。我建议把增量策略设计成两层第一层用游标保证不丢不重第二层在本地维护一个去重集合以内容唯一 ID 为准凡是已存在的 ID 直接跳过。游标是接口给你承诺去重集合是你给自己上的保险两层都做才敢说这份备份能长期自动跑。3. 用 Python 写最小可维护的备份脚本从登录态到落盘这一章直接进入实现。整个脚本的目标不是做一个面面俱到的商业工具而是能在一台普通电脑上跑起来、出错了能看懂日志、半年后还能维护的最小版本。我采用的做法是先打通单模块比如只拉说说正文和配图跑通后再复制同样的模式去接相册和留言板。3.1 目录设计与登录态准备从环境变量注入凭证脚本启动前先把目录结构定好。原始接口返回、可读导出、图片附件、游标配置必须分开否则后面做校验和增量时根本分不清哪些是源文件哪些是产物。# backup.py 顶部结构 import json, os, re, time, html, pathlib, hashlib BASE_DIR pathlib.Path(.) RAW_DIR BASE_DIR / raw # 接口原始返回逐行 JSON EXPORT_DIR BASE_DIR / export # 可读导出文件 MARKDOWN_DIR EXPORT_DIR / markdown IMAGE_DIR EXPORT_DIR / images META_DIR BASE_DIR / meta # 游标与配置 for d in [RAW_DIR, MARKDOWN_DIR, IMAGE_DIR, META_DIR]: d.mkdir(parentsTrue, exist_okTrue)这套结构里raw是黑匣子里的原始证据任何时候导出逻辑写错了都能回头从原始数据重新生成不需要重新请求接口。export是给人看的产物可以随时删除重建。这里的关键设计是原始数据和导出数据分离备份脚本任何时候都不会修改raw下的文件。登录态是第一个绕不过去的门槛。常见做法是从浏览器开发者工具里复制自己账号的 Cookie把它注入环境变量代码里不出现真实凭证字符串。这样脚本即使被人看到也不会直接泄露登录信息。import requests def build_session(): session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), }) # QZ_UIN 是账号标识QZ_SKEY 是登录态凭证 # 从浏览器开发者工具的请求头 Cookie 里复制只放环境变量 session.cookies.set(uin, os.environ[QZ_UIN]) session.cookies.set(skey, os.environ[QZ_SKEY]) return session代码逻辑说明requests.Session会自动保存和携带 Cookie比每次请求手动拼 Cookie 头可靠。uin和skey是这套登录态里最常见的两个字段但不同产品版本可能有其他字段实际以你抓到的请求为准。把敏感信息放在环境变量里是为了防止把备份脚本提交到代码仓库时连凭证一起交出去。3.2 拉取说说分页数据游标、页大小、返回结构说说列表通常是分页接口每一页返回若干条记录同时返回下一个游标。第一步别管具体字段先把这个分页拉通。def pull_feeds(session, host, uin, pos0, num20): host: 登录后的接口域名前缀从浏览器地址栏复制 pos: 分页游标第一次传 0 num: 每页条数一般不建议超过 30 params { uin: uin, pos: pos, num: num, format: json, } resp session.get( fhttps://{host}/cgi-bin/feeds, paramsparams, timeout10, ) resp.raise_for_status() payload resp.json() items payload.get(data, {}).get(list, []) next_pos payload.get(data, {}).get(next_pos, 0) return items, next_pos参数说明pos是游标位置不是页码服务器返回什么下一轮就原样传什么num是页大小设得过大容易触发服务端限制一般 20 比较稳妥。真实接口路径会随产品改版而变化cgi-bin/feeds这里只是脱敏后的示意路径你实际操作时要在开发者工具里找到那一个真正返回说说列表的请求把路径原样抄进来。第一次能拉到数据后再把返回 JSON 整体保存下来确认哪些字段是正文、哪些是图片、哪些是时间。拿到 items 列表后别急着解析。先原样落盘到raw目录这一步是整条链路的可靠性根基。def save_raw(item): content item.get(content, ) json.dumps(item, ensure_asciiFalse) fid hashlib.md5(content.encode(utf-8)).hexdigest()[:12] path RAW_DIR / f{fid}.json if path.exists(): return fid path.write_text( json.dumps(item, ensure_asciiFalse, indent2), encodingutf-8, ) return fid这段代码做的事是用内容的 hash 生成唯一 ID已经存在的直接跳过。这样即使同一个游标被重复请求第二次也不会产生重复文件。fid后续会作为 Markdown 文件名和图片文件名前缀保证整条链路的文件对应关系清晰。3.3 正文和图片分离落盘从 HTML 到 Markdown说说正文在接口返回里往往带着简单 HTML 标签比如换行br或表情图片。需要先清洗成纯文本再把配图下载到本地最后生成一个可读的 Markdown 文件。def clean_html(raw: str) - str: text re.sub(rbr\s*/?, \n, raw) text re.sub(r[^], , text) return html.unescape(text).strip() def save_image(session, url: str, image_dir: pathlib.Path) - str: name url.rsplit(/, 1)[-1].split(?, 1)[0] local image_dir / name if local.exists() and local.stat().st_size 0: return local.name r session.get(url, timeout15, headers{Referer: fhttps://{host}/}) local.write_bytes(r.content) return local.name解释一下图片 URL 末尾通常带有签名参数文件名只取路径最后一节并去掉问号后面的部分Referer头是很多防盗链机制的判断依据不带这个头下载下来的可能是提示页面而不是图片。下载前先检查目标文件是否存在且非零字节避免断点重跑时反复下载。def build_markdown(item, fid): text clean_html(item.get(content, )) ts item.get(create_time, ) lines [f {ts}, , text, ] for idx, img in enumerate(item.get(images, [])): local save_image(session, img[url], IMAGE_DIR) lines.append(f![{fid}-{idx}](images/{local})) out MARKDOWN_DIR / f{fid}.md out.write_text(\n.join(lines), encodingutf-8)经过这一层转换每条说说都是一个独立 Markdown 文件图片用相对路径引用。目录结构里export是可以随时删掉重建的因为raw里存着原始数据任何时候都能重新生成可读格式。4. 备份脚本最常见的 5 个问题现象、原因、解决接口对接过程中翻车几乎不可避免我自己也是一路踩过来的。下面五条是高频问题按现象到原因再到解决的方式记录每条都能直接对应到诊断动作。4.1 第一页正常、第二页开始报错现象是脚本跑第一次请求顺利返回第二次请求直接抛出超时或登录失效错误甚至返回一个登录跳转的 HTML 页面而不是 JSON。原因通常是 Cookie 只在单个请求维度有效服务端对同一会话的请求频率做了限制也可能是 Cookie 里某个字段的有效期极短比如会话级 skey 在几小时后失效。第二页报错不一定代表全部失效而可能只是请求频率太高被临时拒绝。解决方法是先确认是限频还是过期人工打开浏览器访问页面如果能正常看到数据说明只是频率限制如果浏览器也要重新登录说明 Cookie 过期。频率限制就在分页循环里加time.sleep(0.8)把请求间隔拉长过期则需要在脚本里检测响应内容发现 JSON 结构不对时立即停止提示重新复制 Cookie。4.2 网页能看到数据接口却返回空列表现象是浏览器里打开空间一切正常脚本请求同一接口却拿到list: []连一条内容都没有。原因大概率是请求参数少了签名信息。网页端发起请求时除了 Cookie 之外通常还会带一个动态计算的签名参数它由当前时间、部分 Cookie 字段和固定算法生成只在本次请求内有效。脚本里没带这个参数服务器就直接当作无效请求处理返回空列表而非报错。解决方式是在开发者工具里选中这个接口请求把请求头里所有看起来像签名的参数逐个复制到脚本里。注意签名参数往往和时间戳绑定不能写成固定值要改成每次请求前重新计算。大多数情况下签名算法就在页面引用的某个 JS 文件里搜索一下参数名就能找到。4.3 图片下载下来只有几字节现象是图片文件落盘了但用系统自带看图工具打不开文件大小只有几百字节甚至几十字节用文本编辑器打开能看到一串 HTML 标签。原因是图片服务器做了防盗链没有带正确的来源请求头服务器返回的不是图片二进制而是拦截提示页。这个问题在脚本第一次跑通时容易出现因为登录取回来容易让人忽略图片下载还需要独立请求头。解决方法是在下载图片时给请求加上Referer值与浏览器地址栏保持一致一般就能拿到真实图片。如果仍然失败再看 Cookie 是否也在图片请求中生效有些图片服务会校验完整的请求头组合。我习惯把图片下载封装成独立函数失败时把 HTTP 状态码和响应头前几行写入日志便于批量排查。4.4 备份总数对不上少了几天内容现象是备份完成后统计 Markdown 文件数和空间里显示的说说总数差了几十条而且查日志发现某些时间段完全没有数据。原因往往是分页游标在备份过程中遇到新内容插入导致偏移。比如你从第 0 条开始拉拉到第 100 条时空间里刚好新增了一条说说整体列表被推后一位后面请求拿到的游标位置就会跳过一条旧数据。这在活跃账号上很常见频率越高漏数据概率越大。解决方式是把拉取结果按唯一 ID 做去重和补漏。每次拿到一页数据后把这一页的 ID 集合和本地已有 ID 集合做比对缺口超过阈值就从前一条记录的位置重新拉一次。虽然会多几次请求但能保证最终不缺数据。备份脚本里加上这个自我校正逻辑后我基本没有再见过莫名少内容的情况。4.5 脚本在别人电脑上一跑就崩现象是同一份代码在自己的机器上正常换一台电脑后先是各种库报错然后中文路径乱码最后发现文件写到别的目录去了。原因是环境差异。项目没有固定依赖版本没有处理中文目录和文件名的编码也没有对操作系统的路径分隔符做兼容。这些都是刚写完脚本时最容易忽略的收尾工作。解决方式是给项目配一个虚拟环境和依赖清单核心依赖锁定版本所有路径统一转换成绝对路径文件写入时显式指定encodingutf-8。还要记得在任何一台新机器上先跑一个小型冒烟测试比如只拉一页数据确认链路通再开始全量备份。5. 校验、增量与轮转让备份不是一锤子买卖备份跑完一遍只是起点不校验就等于没备份。这一章说三件事怎么确认数据没有缺口怎么做增量更新怎么用轮转策略防止备份目录无限膨胀。5.1 三条校验路径按天计数、附件完整性、断档检查校验第一步是按天统计条数。把 Markdown 文件名或原始 JSON 里的时间字段拿出来按日期聚合输出一个 CSV。连续日期里出现空缺的那几天往往是漏数据的信号重点复查那几天的游标记录。# check_report.py 片段 import json, pathlib, collections, csv rows [] for p in pathlib.Path(raw).glob(*.json): item json.loads(p.read_text(encodingutf-8)) day item.get(create_time, )[:10] rows.append((day, p.stem)) counter collections.Counter(day for day, _ in rows) with open(daily_count.csv, w, newline, encodingutf-8) as f: w csv.writer(f) w.writerow([date, count]) w.writerows(sorted(counter.items()))这段脚本本身不复杂核心是把原始数据变成可审查的统计表。执行后打开daily_count.csv任何一天条数异常偏少都能直接看到。第二步检查图片附件。图片文件和 Markdown 文件是一对多关系如果某篇 Markdown 引用的图片在images目录里不存在说明下载中断或文件名被截断。写个简单脚本扫一遍 Markdown 里的所有](images/引用逐个确认文件存在且非空。第三步检查时间线连续性。这个比较玄学但有效把每天的条数分布画出来正常情况下活跃用户每天数量在某个范围内波动某天突然归零要确认是当天真没发还是接口漏了。前者合理后者需要回头补拉。5.2 增量更新把游标做成持久化状态增量更新的核心是把游标从内存变量变成持久化状态。脚本每次跑完都把最后一条的位置写进meta/cursor.json下次启动直接读取。def read_cursor(): path META_DIR / cursor.json if path.exists(): return json.loads(path.read_text(encodingutf-8)) return {pos: 0, last_fid: } def save_cursor(cursor): (META_DIR / cursor.json).write_text( json.dumps(cursor, ensure_asciiFalse), encodingutf-8, )脚本主循环里每成功处理完一页就立即保存游标而不是等全部跑完再写。这样中途断电、断网下次启动还是从最近位置继续。增量更新不需要时间戳过滤只依赖游标实现简单而且稳定。运行脚本时可以根据需要传入模式参数full全量重扫inc增量更新。全量重扫时不去重用于修复历史数据增量更新正常跑。实际使用中我一般每周跑一次增量每月跑一次全量对比。5.3 轮转与快照用硬链接做日备份备份目录跑久了images目录很容易占用几十 GB 空间。如果每天都往同一个目录里写没法回答“上周的版本是什么样”这种问题。常见做法不是拷贝文件而是用硬链接做快照。cp -al export/ snapshot-$(date %Y%m%d)/cp -al在有变化时才占用新空间没有变化的文件只多一个目录项几乎不占额外磁盘。每天备份结束后生成一个带日期的快照目录一周后删除七天前的快照保留最近两个全量版本。这个方案比把整个目录打包成压缩包灵活恢复时只需要选取任意一天目录直接浏览。find snapshot-* -maxdepth 0 -mtime 7 -exec rm -rf {} 注意执行删除前先确认find列出的目录名准确防止误删正式目录。建议先把这条命令放在一个独立脚本里并在删除前输出将要移除的目录列表人工或定时任务确认后再执行。6. 把备份盘活的两个技巧本地全文检索与静态导出备份文件躺在硬盘里不算真正被利用至少要让它能被搜索、被浏览。两个技巧都不难但会让备份价值提升一个层次。6.1 用 SQLite 做正文搜索如果只有十几个 Markdown 文件直接用编辑器搜索就够了。文件上千后就需要一个本地索引。SQLite 自带 FTS5 全文检索模块不需要额外装服务。把每条说说的正文和原始 JSON 导入一张虚拟表搜索变成一条 SQL。import sqlite3, json, pathlib conn sqlite3.connect(search.db) conn.execute(CREATE VIRTUAL TABLE IF NOT EXISTS feeds USING fts5(tid, content, image_refs)) for p in pathlib.Path(raw).glob(*.json): item json.loads(p.read_text(encodingutf-8)) tid p.stem content item.get(content, ) images .join(img[url] for img in item.get(images, [])) conn.execute(INSERT OR IGNORE INTO feeds(tid, content, image_refs) VALUES (?, ?, ?), (tid, content, images)) conn.commit()搜索操作可以落成一句命令比如SELECT tid, snippet(feeds) FROM feeds WHERE content MATCH 关键词。这样不用打开任何在线页面直接命令行里查自己的历史内容速度比翻网页快得多。6.2 把 Markdown 推成静态目录另一个能落地的技巧是生成一个自包含的静态 HTML 索引页。没必要引入完整静态站点框架用 Python 标准库读取 Markdown 文件按日期分组输出成一个带简单样式的 HTML 文件即可。配合本地文件协议直接在浏览器打开整个备份就变成一个小型多媒体存档库。实际维护这份备份这几年我最大的教训是它不值得追求完美。能用游标保底、能用时间去重、能接受一周跑一次这已经足够。数据安全这件事最有效的做法不是造一个复杂的系统而是让备份流程简单到可以长期坚持。有没有体验过那种硬盘坏掉后翻出一份旧备份发现关键内容恰好都在的感觉如果有你会明白每天多跑一遍脚本完全值得。希望这篇笔记能帮你少走几段弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑