简介:chatlog 是一款用于导出微信聊天记录的本地化工具源码,GitHub 原仓库已下架,这份资源相当于完整源码备份。面向需要离线解析微信数据库、进行二次开发或研究本地数据提取技术的开发者,可在 Windows、macOS 或 Linux 环境自行编译使用。资源共 151 个文件,压缩包仅 243KB,核心代码以 Go 编写(131 个 .go 文件),涵盖文件复制、时间处理、数据源连接、消息解析等模块;另配套 .proto 协议定义、Markdown 说明文档、YAML 配置、Dockerfile 与 shell 脚本,便于快速构建与部署。目前已有 122 人学习下载,适合中等水平以上开发者参考。源码内部包含数据库连接与字段映射、消息类型识别、附件路径提取等实现,目录结构清晰,可学习 SQLite 结构分析及 Go 工程组织方式;对于需要存档微信聊天记录或构建同类工具的研究者,这份源码可提供可直接运行的参考起点,避免从零逆向。
1. chatlog 下架之后:微信聊天记录导出工具到底在解什么难题
用 chatlog 导出微信聊天记录,曾经是 GitHub 上能找到的最直接方案。工具本身不复杂:读手机里的加密数据库,解出明文,再导出成网页或文本。问题是现在 GitHub 上已经搜不到它的源码,找过来的朋友往往卡在同一步——仓库链接失效,教程里的命令又没看懂。下面把 chatlog 类工具背后那条链路完整拆开:记录存在哪、密钥怎么来、怎么用命令解、怎么写脚本导。适合自己持有手机、想给聊天记录做离线归档的人,也适合想搞懂微信本地数据结构的同学。整条链路并不依赖某个特定工具的源码,只要原理清楚,自己重建一个完全可行。
2. 先摸清微信本地数据库:EnMicroMsg.db、SQLCipher 与密钥派生公式
2.1 微信把聊天记录存在手机哪里
Android 微信的数据全部落在应用私有目录 /data/data/com.tencent.mm/ 下,真正的聊天数据库是 MicroMsg 目录里某个 32 位 hash 子目录下的 EnMicroMsg.db。这个 32 位 hash 目录对应「设备加账号」的组合,换手机号或换设备登录,目录名会变。里面除了 EnMicroMsg.db,还有 WxFileIndex.db、WxVideoIndex.db 等辅助库,聊天记录主体就是 EnMicroMsg.db。数据库文件是 SQLCipher 加密过的 SQLite,直接打开只会看到乱码,头部也没有普通 SQLite 的 "SQLite format 3" 标志。
这里要强调一个常见误区:只拷贝 EnMicroMsg.db 一个文件是不够的。老版本微信把账号内部编号 uin 存在 /data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml 里,后续密钥派生要用它。备份时要把整个 MicroMsg 目录连同 shared_prefs 一起拷走,否则拿到数据库也推不出密钥。这也是很多导出手法第一次尝试就失败的原因——数据库拿到了,配置和账号信息没拿到。
微信选择整库加密而不是只加密敏感字段,对导出工具意味着两件事。第一,必须先把整库解开才能用任何 SQL 工具读取;第二,密钥不对时 SQLCipher 不会给"密码错误"这种友好提示,而是直接报 "file is not a database"。这个「明明有库却打不开」的现象,是后面避坑章节的重点。
2.2 表结构:message 表、rcontact 表与常用字段
解开之后露出的是普通 SQLite 表结构。最核心的是 message 表,字段包括 msgId、msgSvrId、type、status、isSend、createTime、talker、content、imgPath 等。type 表示消息类型,isSend 表示是不是自己发的,talker 是会话对象的 wxid,content 是消息正文。文本消息 content 就是字面文字;图片、语音这类消息 content 里存的是路径或描述信息,真正的文件按 imgPath 指向外部目录。语音消息的时长信息存在 lvbuffer 字段里,二进制格式,导出时要单独解析。
rcontact 表是通讯录,username 对应 message.talker,nickname 是昵称,remark 是备注。导出时想显示「谁说了什么」,必须把 message 和 rcontact 按 username=talker 关联。群聊特殊一些:message 表里群成员的 talker 是成员的 wxid,但成员在本群的昵称存在 chatroom 表的 memberlist 字段里,JSON 结构,解析时要单独处理。我一般先查 sqlite_master 把表名和建表语句打印出来再决定读哪张,不拿多年前的字段名硬套——微信不同版本加过不少字段,硬编码列名会翻车。
2.3 SQLCipher 版本差异:为什么有的库要指定 cipher_compatibility
SQLCipher 从 2.x 走到 4.x,密钥派生参数一直在变。最影响导出的是 KDF 迭代次数:3.x 默认 64000 次,4.x 默认 256000 次。微信内置的 SQLCipher 版本跟着应用版本走,老版本微信导出的库,用新版 sqlcipher 打开时不指定兼容模式,密钥派生参数对不上,表现就是库能打开但读不出数据。命令行工具对应两个开关:PRAGMA cipher_compatibility=3 告诉引擎按 3.x 的 KDF 参数解析;PRAGMA cipher_migrate 把老库页面重写成新格式。
实际操作中我会固定用一套顺序:先 PRAGMA key,再 PRAGMA cipher_compatibility,最后才考虑 cipher_migrate。顺序不能反,migrate 依赖正确的 key 已经生效。另外,不同版本 message 表的结构差异不大,但 sqlite_master 里建表语句长得不一样,导出脚本不要硬编码列名,先 SELECT * 看一遍实际列更稳妥。
2.4 密钥派生:IMEI、UIN 与 md5(imei+uin) 前七位
Android 微信数据库口令,公开资料里最常用的一条规则是:md5(IMEI + UIN) 的十六进制结果取前 7 位。IMEI 是设备串号,UIN 是账号在微信服务器上的内部编号。UIN 存在 /data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml 里,形如<int name="uin" value="12345"/>。注意不一定是 uin 这个字段名,较新版本可能要去 auth_info_key_prefs.xml 里找。之前的工具源码消失后,社区里流传的多数扒库脚本仍沿用这条规则,新版本微信如果推不出来,多半是 uin 拿错了账号或 IMEI 输成了双卡的另一张。
iOS 端是另一套逻辑:数据在 iTunes 备份的 App 容器里,密钥派生会用到设备 UDID 和更多参数,与 Android 不同。本文后面的脚本默认按 Android 走,iOS 单独说一句:先解备份拿到 Documents 下的库文件,再按 iOS 规则处理,步骤比 Android 多一跳。想先跑通整条链路的话,建议用 Android 老版本微信的库起步,成功率最高。
3. 复现 chatlog 的第一步:用 sqlcipher 命令行把加密库解密成明文库
3.1 装一个能用的 sqlcipher:版本选择直接影响成败
命令行解密是整个导出链路里最值得先跑通的一步。安装方面,macOS 用 brew install sqlcipher,Debian/Ubuntu 用 apt install sqlcipher。装完先看版本:sqlcipher --version。这里有个坑:apt 源里的 sqlcipher 往往停留在 3.x,brew 的通常是 4.x。对微信老库来说 3.x 反而省事,默认兼容参数就对;用 4.x 就必须在会话里补 cipher_compatibility=3。别急着自己编译最新版,发行版自带的够用。
我一般把 sqlcipher 当作黑匣子阶段工具:只用来把加密库转成明文库,后续解析全部交给标准 sqlite3。这样全链路里只有一步依赖编译产物,出问题好隔离。踩坑的人多半是在「让 Python 直接读加密库」上面花了大半天,最后发现是 sqlcipher 版本和库版本不匹配。
3.2 打开加密库并设置密钥:三条 PRAGMA 的正确会话
拿到 EnMicroMsg.db 和 7 位密钥后,先做一个最小验证:
sqlcipher /path/to/EnMicroMsg.db进入交互后依次执行:
PRAGMA key = 'xxxxxxxxx'; -- 7 位十六进制口令 PRAGMA cipher_compatibility = 3; -- 按 SQLCipher 3.x 规则解析 PRAGMA cipher_migrate; -- 老库转新格式,可选第一条是核心。key 设置的是口令字符串,SQLCipher 内部用 PBKDF2 派生真正的 AES 密钥,所以不需要把它补成 32 字节。第二条让解析器按 3.x 的 KDF 参数跑,老库必须加。第三条只在提示需要迁移时执行,如果接下来马上要 ATTACH 导出明文库,migrate 可以跳过——导出过程读的是兼容模式下的数据,写出来的是明文,不需要改原库格式。三条按这个顺序来,反了会出现「key 设置了但数据仍不可读」的诡异现象。
3.3 导出明文库:ATTACH 加 sqlcipher_export 一步到位
验证能 SELECT 出内容后,把它导出成明文库:
ATTACH DATABASE '/path/to/plain.db' AS plaintext KEY ''; SELECT sqlcipher_export('plaintext'); DETACH DATABASE plaintext;sqlcipher_export 会把当前会话里已解密的所有表、索引、触发器一起写入 plain.db。KEY '' 表示明文库不再加密。这一步是整条链路的后悔药:plain.db 生成后,原加密库只作为证据留存,后续所有读取都在明文库上做,速度也快得多。导出完直接用 sqlite3 验证:
.headers on .mode column SELECT count(*) FROM message; SELECT createTime, isSend, type, substr(content,1,40) FROM message LIMIT 5;count 是第一步校验,和微信里聊天记录总数对一下数量级。substr 是为了避免 content 里埋了换行把终端刷屏。
提示:如果 ATTACH 之后 SELECT sqlcipher_export 返回空结果,先检查是不是在设置 key 之前就执行了 ATTACH。导出会话里 PRAGMA 顺序同样严格。
3.4 字段识别:先把表结构打印出来再写脚本
我不建议直接抄网上的 SELECT 语句,先执行:
SELECT name, sql FROM sqlite_master WHERE type='table' AND name IN ('message','rcontact','chatroom');把建表语句打出来,确认 type、isSend、talker、createTime 这些列都在,再写查询。实际遇到过某版本把 content 挪到别的表,或把 createTime 改名的情况,硬编码列名会直接失败。这一步花五分钟,能省后面调试脚本的半小时。确认结构没问题之后,明文库就可以交给 Python 处理了。
4. 用 Python 复刻 chatlog 核心逻辑:密钥推导、消息解析与 HTML 导出
4.1 先推导密钥:IMEI 和 UIN 拼起来算 md5
命令行方式跑通后,把整个流程脚本化。第一步是把密钥派生写成函数:
import hashlib def derive_key(imei: str, uin: str) -> str: """微信 Android 旧版数据库口令:md5(imei+uin) 前 7 位十六进制""" raw = hashlib.md5(f"{imei}{uin}".encode("utf-8")).hexdigest() return raw[:7] if __name__ == "__main__": # 调试时用真实值替换,先打印确认再连库 print(derive_key("862912031234567", "12345678"))逻辑不复杂,但有两个容易错的地方。一是拼接顺序必须是 imei 在前 uin 在后,反了推导结果完全不同;二是返回的是字符串而不是字节数组,它会被当作 SQLCipher 的口令去做 PBKDF2,不是直接用做 AES key。调试时先用一组已知的 imei 和 uin 算出来核对,别等到连接数据库才发现问题。
4.2 读库两条路线:直连加密库还是先生成明文库
写脚本时面临一个选择:直接用 pysqlcipher3 连加密库,还是像第 3 章那样先用 CLI 生成 plain.db 再用标准 sqlite3 读。两条路线我都用过,结论很明确:优先走「CLI 生成明文库 + 标准 sqlite3」。
pysqlcipher3 的安装是血泪经验:它在 Python 3.11 以上的环境里基本编译失败,依赖老版本 OpenSSL 头文件和 SQLCipher amalgamation 源码,补丁常常跟不上。就算装好了,Python 侧还要手动指定 cipher_compatibility 等 PRAGMA,报错信息比 CLI 还难懂。明文库方案里 Python 只做数据解析,不依赖任何加密库,换台机器也能跑。如果你确实想直连,代码长这样:
from pysqlcipher3 import dbapi2 as sqlite3 conn = sqlite3.connect("EnMicroMsg.db") conn.execute(f"PRAGMA key='{key}'") # key 只含 hex 字符 conn.execute("PRAGMA cipher_compatibility = 3")注意 f-string 拼接 PRAGMA 口令时,key 要保证只含 hex 字符;这是工具脚本,口令来源受控,可以不考虑注入。但要明白这条路径的脆弱点:Python 环境一旦变动,编译问题会先于业务逻辑出现,所以生产归档我还是推荐先转明文库。
4.3 读取消息并做基础清洗
明文库到手后,解析逻辑与普通 SQLite 没有区别。我用 sqlite3.Row 让行对象支持按列名访问:
import sqlite3 def load_messages(db_path: str, talker: str | None = None) -> list: """读取 message 表,可按会话对象过滤""" conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row cur = conn.cursor() sql = "SELECT createTime, isSend, type, content FROM message" params = () if talker: sql += " WHERE talker = ?" params = (talker,) sql += " ORDER BY createTime" return cur.execute(sql, params).fetchall()WHERE 条件用 ? 占位符是必须养成的习惯:talker 来自外部参数时,拼字符串会让导出脚本引入注入风险。ORDER BY createTime 保证聊天顺序,不是每个版本都会按时间排序返回。类型映射留到导出阶段做,这里先保证数据裸读出来。
4.4 导出成可读 HTML:类型映射与时间戳清理
消息类型是数字编号,导出时要转成人话:
| type | 含义 |
|---|---|
| 1 | 文本 |
| 3 | 图片 |
| 34 | 语音 |
| 43 | 视频 |
| 47 | 表情 |
| 49 | 链接/小程序 |
| 10000 | 系统提示 |
时间戳的处理是另一处陷阱,别直接格式化:
import html import time TYPE_NAMES = {1: "文本", 3: "图片", 34: "语音", 43: "视频", 47: "表情", 49: "链接", 10000: "系统"} def to_html(rows: list, out_path: str = "chatlog.html") -> None: """把 message 行记录导出成浏览器可读的 HTML""" with open(out_path, "w", encoding="utf-8") as f: f.write("<html><head><meta charset='utf-8'></head><body>") for r in rows: ts = r["createTime"] if ts > 10**12: ts //= 1000 # 毫秒时间戳转秒 t = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts)) who = "我" if r["isSend"] == 1 else "对方" kind = TYPE_NAMES.get(r["type"], str(r["type"])) text = html.escape(str(r["content"])).replace("\n", "<br>") f.write(f"<p><b>{t} {who}</b> [{kind}] {text}</p>") f.write("</body></html>")三处不能省:ts 大于 10 的 12 次方时先除 1000,否则时间会变成 1970 年;html.escape 必须做,聊天内容里可能出现<script>这类字面内容,不转义生成的 HTML 打开时会被当成页面代码执行;写入编码固定 utf-8,缺少 charset 声明会乱码。这样导出的文件双击浏览器就能看,算是一个 chatlog 式工具的最小可用版本。图片等附件要导出的话,按 imgPath 字段在 /sdcard/tencent/MicroMsg/ 下找原文件,message 表里不存二进制。
5. 复现 chatlog 最容易翻车的 5 个点:排查顺序与解决办法
5.1 现象:PRAGMA key 后报 "file is not a database"
原因:密钥不对,或者这个库根本不是预期的微信库。SQLCipher 在口令错误时不会给「密码错误」,而是把文件当作损坏或非数据库处理。最常见的是双卡手机 IMEI 拿错、uin 拿成另一个账号的值。解决:双卡手机把两个 IMEI 都试一遍;uin 重新从 shared_prefs 和 auth_info_key_prefs.xml 两个文件核对;确认这个 db 是当前登录账号产生的,不是换号前的遗留文件。再用一组已知输入跑 md5,确认拼接顺序没写反。
5.2 现象:PRAGMA key 不报错,但 SELECT 内容为空
原因:库按错误的兼容参数解析出来了,表结构在但数据页读不出,常见于用 4.x 的 sqlcipher 打开 3.x 老库。解决:回到 3.2 的顺序,先执行 PRAGMA cipher_compatibility = 3; 再重查。还不行就试 PRAGMA kdf_iter = 4000;,极老版本微信用的迭代次数比默认小。改完参数后重新 ATTACH 导出 plain.db,别在原文件上反复操作。
5.3 现象:导出的时间全是 1970-01-01
原因:createTime 在部分版本里是毫秒时间戳。time.localtime 接收的是秒,喂进去十三位数直接溢出或回绕到 1970。解决:统一在格式化前判断,ts 大于 10 的 12 次方就除以 1000。这个判断对秒级和毫秒级都兼容,是导出脚本里最稳的写法。别用 len(str(ts)) 判断位数,负数和边界值容易出问题。
5.4 现象:群聊记录只有 wxid,看不到昵称
原因:群聊里 message.talker 存的是群成员的 wxid,rcontact 表没有这群人的条目;群内昵称存在 chatroom.memberlist 里。解决:把 chatroom 表读出来,memberlist 是字符串形式的 JSON 数组,解析后建立 wxid 到群昵称的映射,再回填到导出结果。群聊里「我/对方」的判断仍可用 isSend 字段,但展示名要用映射后的昵称,否则整份群记录读起来像乱码。
5.5 现象:解密成功,但导出的消息数量比微信里看到的少
原因:新版微信可能把部分消息拆到其他表,或本地消息已被清理策略删除。message 表只保留本地现存的部分;本地没有的,任何导出工具都拿不回来。解决:先 SELECT name FROM sqlite_master,看有没有 msg_ 前缀或历史记录相关的分表,有就合并。没有的话接受一个事实:导出工具不是备份工具,能导出的最大范围就是本机数据库里现存内容。想长期留存,得在数据还在时定期导。
6. 给导出结果加一道校验:三条手段与一个归档习惯
6.1 数量校验与时间连续性检查
导出后先做两条快速校验。数量上,SELECT count(*) FROM message 与导出的行数必须一致,HTML 里换行符被替换过,不能靠数<p>标签,直接对比行数。时间连续性上,把 createTime 排序后扫一遍相邻间隔,出现跨月或跨年的跳跃,说明中间可能有删除或漏了分表数据。这两条校验加起来不到一分钟,能拦住大多数低级错误。
6.2 给明文库加一个哈希存档
解密出来的 plain.db 是整批归档的依据,给它算 SHA-256 存成同目录文件:
sha256sum plain.db > plain.db.sha256之后每次引用这份导出,先 sha256sum -c 校验再操作。这个习惯防止「解密一次、之后又在半解密状态的文件上做分析」这类事故,也让你能确认归档链路没有被中途替换。旁边再写一个 txt 记录导出时间、微信版本、imei 前六位和 uin,下次重导时能快速判断环境是否变化。
6.3 归档习惯:明文库只在本地留一份
明文库包含完整聊天内容,比聊天记录 app 里现有的更全。我自己的习惯是换机或清理手机前先导一轮,导出文件放本地磁盘专用目录,不放进任何网盘同步路径;分析完的临时明文库及时删掉,只留 sha256 校验文件和 HTML 导出。毕竟能解开这份数据的人不多,但文件一旦落到不该去的地方,问题性质就完全变了。
这套链路我重建过不止一次,每次换手机或微信版本升级,都要重新确认密钥派生和兼容参数。别迷信网上存着的旧脚本,跑一遍 2.4 的派生公式、3.2 的 PRAGMA 顺序,半小时就能验证环境是否还成立。希望帮到你。
本文还有配套的精品资源,点击获取