用 Rust 重写了 QQFlow,从 188MB 到 10MB
起因
事情是这样的。
我有个习惯,每隔一段时间会把 QQ 聊天记录导出来备份。不是为了什么,就是怕哪天号没了,聊天记录也没了。
之前一直用一个叫 QQFlow 的工具,Electron + Python 写的,能用。但有两个问题:一是软件太大了,安装包 188MB;二是打开大数据库的时候卡得要命,190MB 的数据库要等好几分钟。
有天晚上我又在等它加载,看着进度条一动不动,突然想:要不用 Rust 重写一个?
其实没那么难
说”重写”有点高估我了。
我做的事情更像是”整合”。核心的思路是参考了 WeFlow 这个微信聊天记录导出工具的架构——用一个内存缓存把所有消息一次性加载进来,后续查询直接走内存,不再碰数据库。
具体的实现是这样的:
密钥提取:用 Windows Debug API 注入 QQ 进程,从内存里找到 SQLCipher 的加密密钥。这部分代码网上有现成的,我改了改就用了。
数据库解密:用 rusqlite + bundled-sqlcipher,流式复制一份解密后的数据库到临时目录。只复制一次,后续直接读缓存。
消息加载:
SELECT *全表扫描,把所有消息按群号/私聊对象分组存到 HashMap 里。190MB 数据库大概 30-60 秒,之后所有查询都是 O(1)。前端:React + TypeScript + Vite,抄了几个开源项目的样式,拼拼凑凑。
说实话,我没写多少原创代码。大部分时间都在调试、在踩坑、在看报错日志。
踩过的坑
坑一:死锁
第一个版本有个很诡异的 bug:点击群聊分析,永远显示”正在加载…”。
调试了两天才发现是死锁。load_store_if_needed 在持有 Mutex 锁的情况下执行数据库操作,而另一个线程也在等这把锁。两个线程互相等,就卡死了。
解决方法是把”检查缓存”和”执行数据库”分开:先检查缓存,释放锁,执行数据库操作,然后再加锁存储。听起来简单,但我改了三个版本才稳定下来。
坑二:BLOB 解析
QQ 的消息内容存在 BLOB 字段里,是 Protobuf 编码的二进制数据。早期版本的解析器把 varint 字节解码成了 CJK 扩展区字符,输出一堆”生僻字”垃圾。
后来我加了个纯净度过滤器:只有 60% 以上的字符是常见汉字才保留。但这个阈值太高了,英文消息会被过滤掉。改成先尝试提取可读文本,再回退到 ASCII 分类,才解决。
还有一个更离谱的问题:某些二进制 BLOB 会让解析器进入无限循环,CPU 直接拉满 100%。最后加了个操作预算(budget = blob_size × 50),超出预算自动回退,才搞定。
坑三:UID 映射
QQ 的发送者不是直接存 QQ 号,而是存一个 UID(像 u_-PBswiplK-7J7bmaQLA-mA 这种)。QQ 号存在另一个映射表里。
问题是这个映射表的列名全是数字(48901、48902、1002),没有语义。我原来的逻辑是找第一个非数字列作为 UID、第一个数字列作为 QQ 号,但第一列的值是纯数字序号,被误判为 QQ 号了。
最后写了 4 级查找策略:已知列名直查 → 候选表自动检测 → 模糊表名搜索 → 全表暴力扫描。才把各种 QQ 版本的映射表都兼容了。
坑四:CSV 乱码
导出的 CSV 文件用 Excel 打开,中文全是乱码。查了半天发现是编码问题——UTF-8 没有 BOM 头,Excel 默认按 GBK 解析。
在文件开头加了 3 个字节的 BOM(\xEF\xBB\xBF),问题解决。这种小问题最烦人。
最终效果
| 特性 | 原版 (Electron + Python) | Rust 版 (Tauri) |
|---|---|---|
| 安装包大小 | ~188MB | ~10MB |
| 内存占用 | ~200MB | ~30MB |
| 打开 190MB 数据库 | 几分钟 | 30-60 秒(首次),后续秒开 |
功能清单
- 密钥提取(Windows Debug API)
- 数据库解密(SQLCipher)
- TXT 导出(按时间排序、日期分组)
- CSV 导出(带 BOM,Excel 可直接打开)
- 聊天分析(24h 分布、类型饼图、成员排行、高频短语)
- 多账号支持(每个 QQ 号独立缓存)
- 深色/浅色主题
开源
项目地址:yfgug/QQFlow
目前只支持 Windows,因为密钥提取用的是 Windows API。Mac 和 Linux 的话,密钥提取方式不一样,我还没研究。
写在最后
这个项目前前后后写了两个月。说是”重写”,其实大部分时间都在调试。Rust 的编译错误真的让人头大,有时候一个生命周期的问题能卡我半天。
但最后还是写完了。
写完之后最大的感受是:很多时候你不需要从零开始造轮子。看看别人怎么做的,把好的思路拿过来,用自己的方式实现一遍,这个过程本身就是学习。
感谢 WeFlow 作者的开源贡献,没有他的项目,我可能不会想到用这种架构。
“代码是整合的艺术,不是原创的专利。”