1. 这个项目到底在解决什么问题
微信聊天记录的价值,很多人其实低估了。日常对话里藏着项目决策的来龙去脉、客户需求的原始描述、团队协作的关键结论,甚至还有不少灵感和待办事项。但这些内容一旦沉淀在微信里,基本就等于进了黑洞——搜索靠关键词碰运气,导出只能截图,想拿来做知识管理或者喂给 AI 分析,几乎无从下手。
这个项目的核心思路很直接:把微信聊天记录变成结构化、可迁移的数据,然后分别对接两个下游场景——Codex这类 AI 编程助手,以及Obsidian这类本地知识库工具。前者可以让 AI 直接读取你的聊天上下文来辅助写代码、整理需求;后者可以把聊天记录变成可检索、可双链、可长期沉淀的笔记资产。
说白了,它做的是一件“数据摆渡”的事:从微信这个封闭生态里,把属于你自己的数据安全地取出来,转成通用格式,再送到你真正想用的工具里去。适合谁参考?三类人最值得看:一是重度使用 Obsidian 做知识管理的用户,二是想把聊天上下文接入 AI Agent 工作流的开发者,三是单纯想给自己多年聊天记录做个本地备份的普通用户。
我先把结论放前面:这件事技术上完全可行,但难点不在“导出”,而在“解密”和“结构化”这两步。下面我按实际操作的逻辑,一层层拆开讲。
2. 整体方案设计与技术选型思路
2.1 为什么不能直接复制粘贴
很多人第一反应是:微信不是有“迁移聊天记录”功能吗,直接导出来不就行了?问题在于,微信的迁移文件是加密的,而且格式是私有的,你拿到的是一个.db或者加密的备份包,普通工具根本读不了。更麻烦的是,不同平台(Windows、macOS、Android、iOS)的存储方式和加密密钥来源都不一样。
所以整个方案的第一步,必然是定位数据库文件 + 获取解密密钥。这一步是绕不过去的,任何声称“一键导出”的工具,底层做的都是这件事,只是把复杂度藏起来了。
2.2 解密之后为什么还要“结构化”
假设你已经拿到了明文的 SQLite 数据库,打开一看会发现:消息不是一条条干净的文本,而是分散在多个表里,有message表、contact表、chatroom表,消息类型还分文本、图片、语音、引用、合并转发等等。语音是.amr或.silk格式,图片是加密的.dat文件。
如果直接把原始数据库丢给 Codex 或 Obsidian,效果会很差——AI 读不懂表结构,Obsidian 也没法做双链。所以中间必须有一层转换逻辑:把消息按会话分组、按时间排序、把发送者 ID 映射成昵称、把多媒体文件解密并关联到对应消息。这一步做得好不好,直接决定了最终产出的可用性。
2.3 对接 Codex 和 Obsidian 的两种输出形态
这两个下游的需求其实不一样,我在设计时是分开处理的:
| 下游工具 | 输出格式 | 核心诉求 |
|---|---|---|
| Codex / AI Agent | Markdown 或 JSONL | 上下文完整、角色清晰、便于切片喂给模型 |
| Obsidian | Markdown + 附件目录 | 双链友好、按日期或联系人分文件、图片可预览 |
对 Codex 来说,最重要的是角色标注和时间线,因为 AI 需要知道“谁在什么时候说了什么”,才能理解对话逻辑。对 Obsidian 来说,最重要的是文件粒度,一个会话一个文件、或者一天一个文件,配合[[双链]]和标签系统,才能融入现有的知识库结构。
提示:不要试图用一套输出同时满足两个场景,格式耦合会让两边都别扭。分开生成,成本很低,收益很高。
3. 核心细节解析与实操要点
3.1 数据库定位:不同平台的文件藏在哪里
这一步是很多教程含糊带过的地方,我按实际踩过的坑说清楚。
Windows 端,微信的数据默认在文档\WeChat Files\下面,每个微信号一个文件夹,里面Msg目录存放消息数据库,FileStorage存放图片、视频、文件。注意新版微信(4.0 之后)目录结构有变化,Msg可能变成db_storage,需要实际进去看一眼。
macOS 端,路径在~/Library/Containers/com.tencent.xinWeChat/下面,层级比较深,而且沙盒机制会导致部分文件权限受限,需要先给终端完全磁盘访问权限。
Android 端,未 root 的情况下数据在/data/data/com.tencent.mm/里,普通用户访问不了。root 之后可以adb pull出来,但要注意 Android 高版本的加密策略更严格。
iOS 端,最麻烦,需要完整备份到电脑,然后从备份里提取,备份本身也是加密的,需要知道备份密码。
我的建议是:优先在 Windows 或 macOS 桌面端操作,因为桌面端的数据库结构相对稳定,工具链也最成熟。手机端除非你有明确需求,否则不建议折腾。
3.2 密钥获取:为什么这一步最容易卡住
微信的数据库用的是 SQLCipher 加密,密钥不是固定值,而是从你的登录状态里动态推导出来的。获取密钥的常见思路有两种:
一种是内存扫描,在微信运行时从进程内存里找到密钥。这种方法对版本敏感,微信一更新就可能失效,而且需要一定的调试工具使用经验。
另一种是从本地缓存推导,部分版本会把密钥相关的信息存在本地文件里,通过特定算法还原。这种方法相对稳定,但需要你对微信的存储机制有了解。
注意:密钥获取是整个流程里最脆弱的一环,微信每次大版本更新都可能改变机制。建议在操作前先确认你的微信版本,并找对应版本的资料。不要盲目套用旧教程。
我实测下来的经验是:先把微信版本号记下来,再去社区找对应版本的方案,比先找工具再对版本要高效得多。另外,操作前务必备份原始数据库文件,解密过程出错不会影响原文件,但误操作可能会。
3.3 消息解析:那些容易被忽略的细节
拿到明文数据库后,解析环节有几个坑必须提前知道。
第一,消息类型判断。微信的消息类型用数字编码,1 是文本,3 是图片,34 是语音,43 是视频,49 是各种应用消息(包括引用、合并转发、小程序卡片等)。49 类型最复杂,里面还嵌套了 XML,需要二次解析才能提取出真正的文本内容。
第二,发送者映射。群聊里消息的发送者是 wxid,需要和contact表做关联才能拿到昵称。但有些联系人没有备注,有些群成员已经退群,映射会失败,需要做兜底处理。
第三,时间戳处理。微信存的是 Unix 时间戳,但要注意时区问题,以及部分消息的时间戳精度是秒级,排序时可能出现同一秒内顺序错乱。
第四,多媒体文件关联。图片和语音在数据库里只存了路径或 MD5,实际文件在FileStorage目录里,而且图片是.dat格式,需要根据文件头判断真实格式(jpg、png、gif)再重命名。
# 消息类型映射的简化示例 MSG_TYPE_MAP = { 1: "text", 3: "image", 34: "voice", 43: "video", 49: "app", 10000: "system", } def parse_message(row): msg_type = row["type"] if msg_type == 1: return row["content"] elif msg_type == 49: # 需要解析 XML 提取真实内容 return extract_from_xml(row["content"]) else: return f"[{MSG_TYPE_MAP.get(msg_type, 'unknown')}]"这段逻辑看着简单,但实际写的时候,49 类型的 XML 结构五花八门,引用消息、合并转发、文件传输各有各的格式,需要逐个 case 处理。我的做法是先把所有 49 类型的原始 XML 导出来看一遍,归类之后再写解析规则,比边写边猜高效得多。
3.4 输出格式设计:让下游工具真正好用
给 Codex 的输出,我推荐用 JSONL(每行一个 JSON 对象),字段包括timestamp、sender、content、type。这样 AI 读取时可以按需切片,不会一次性塞爆上下文。如果对话很长,还可以按主题或时间段预切分,每个片段加一个摘要头。
给 Obsidian 的输出,我推荐按“会话 + 日期”分文件,文件名格式类似2024-01-15-项目群.md,文件头加 YAML front matter 标注参与人和标签,正文用**发送者**:内容的格式。图片放在同目录的attachments文件夹里,用![[图片名]]引用,这样在 Obsidian 里可以直接预览。
提示:Obsidian 的双链语法是
[[文件名]],如果你想让某个联系人自动形成双链,可以在 front matter 里加aliases,或者在正文里把联系人名写成[[张三]]。这样点进去就能看到这个人的所有相关对话。
4. 完整实操流程与关键环节实现
4.1 环境准备与工具清单
在动手之前,先把环境理清楚。我按桌面端 Windows 为例,macOS 逻辑类似,只是路径不同。
需要准备的东西:
- Python 3.9 以上环境,用于跑解析脚本
- SQLite 命令行工具或 DB Browser for SQLite,用于查看数据库
- 一个趁手的文本编辑器,VS Code 就行
- 足够的磁盘空间,聊天记录加上多媒体文件,几个 G 很正常
不需要的东西:任何来路不明的“一键导出工具”。这类工具要么捆绑广告,要么偷偷上传你的数据,风险极高。自己写脚本虽然麻烦,但数据全程在本地,可控性完全不一样。
4.2 第一步:定位并备份原始数据
先关闭微信,找到数据目录,把整个WeChat Files文件夹复制一份到安全位置。这一步看起来笨,但非常重要——后续所有操作都在副本上进行,原数据不动。
复制完成后,进入副本目录,找到消息数据库文件。通常是一个或多个.db文件,大小从几十 MB 到几个 G 不等。用 DB Browser 打开,如果提示加密,说明需要先解密;如果能直接看到表结构,说明这个版本没加密或者已经被解密过。
4.3 第二步:解密数据库
解密的核心是拿到密钥,然后用 SQLCipher 或对应的 Python 库打开。这里我不展开密钥获取的具体技术细节,因为不同版本差异太大,展开反而容易误导。核心原则是:确认版本、找对应方案、小范围验证。
验证是否解密成功的方法很简单:用 SQLite 打开解密后的文件,能查到message表并且有数据,就说明成功了。如果表是空的或者报错,说明密钥不对或者解密参数有误。
# 用 sqlcipher 解密的概念性命令 sqlcipher encrypted.db PRAGMA key = 'your_key_here'; ATTACH DATABASE 'decrypted.db' AS plaintext KEY ''; SELECT sqlcipher_export('plaintext'); DETACH DATABASE plaintext;注意:密钥是敏感信息,不要写死在脚本里,更不要提交到任何公开仓库。用环境变量或者本地配置文件管理,配置文件记得加进
.gitignore。
4.4 第三步:解析消息并生成中间数据
解密后的数据库,我建议先做一次全量导出,生成一个中间层的 JSON 文件。这样做的好处是:后续无论输出成什么格式,都从 JSON 转换,不用反复查数据库。
解析脚本的核心逻辑分四步:
- 读取
message表,按talker分组 - 关联
contact表,把 wxid 映射成昵称 - 解析消息内容,处理 49 类型的 XML
- 关联多媒体文件,解密
.dat并重命名
import sqlite3 import json from collections import defaultdict conn = sqlite3.connect("decrypted.db") conn.row_factory = sqlite3.Row cursor = conn.cursor() # 建立联系人映射 contacts = {} for row in cursor.execute("SELECT username, remark, nickname FROM contact"): contacts[row["username"]] = row["remark"] or row["nickname"] or row["username"] # 按会话分组导出 conversations = defaultdict(list) for row in cursor.execute("SELECT * FROM message ORDER BY create_time"): talker = row["talker"] conversations[talker].append({ "timestamp": row["create_time"], "sender": contacts.get(row["sender"], row["sender"]), "content": parse_message(row), "type": row["type"], }) with open("messages.json", "w", encoding="utf-8") as f: json.dump(conversations, f, ensure_ascii=False, indent=2)这段代码是骨架,实际跑的时候你会发现各种边界情况:有的消息sender是空的,有的talker是群 ID 需要再映射,有的时间戳是毫秒有的是秒。这些都要在跑的过程中逐个修。
4.5 第四步:分别输出给 Codex 和 Obsidian
输出给 Codex,我习惯按会话切分成多个 JSONL 文件,每个文件对应一个会话,文件名用会话名。如果单个会话特别长,再按月份或按消息数量切分,每个片段控制在 2000 到 4000 条消息之间,这样喂给模型时不会超上下文。
输出给 Obsidian,用 Python 的pathlib和字符串模板生成 Markdown 文件。每个文件的结构大致是这样:
--- date: 2024-01-15 participants: [张三, 李四] tags: [聊天记录, 项目群] --- # 2024-01-15 项目群 **张三** 09:15 今天的方案我看了一下,整体没问题,但第三部分需要再确认。 **李四** 09:20 好的,我下午改完发出来。 ![[attachments/20240115-001.jpg]]生成完之后,把整个文件夹拖进 Obsidian 库,图片能正常预览,双链能正常跳转,就算成功了。
4.6 第五步:验证与增量更新
第一次跑通之后,建议做一次完整验证:随机抽几个会话,和微信里的原始记录对比,确认消息数量、顺序、内容都对得上。特别是图片和语音,要确认能正常打开。
增量更新是个容易被忽略的需求。微信每天都有新消息,不可能每次都全量重跑。我的做法是记录上次导出的最大时间戳,下次只处理这个时间戳之后的消息,追加到已有文件里。这样既快又不会重复。
5. 常见问题与排查技巧实录
5.1 解密失败:先查版本,再查密钥
解密失败是最常见的问题,表现是打开数据库报错或者表为空。排查顺序应该是:先确认微信版本和数据库版本是否匹配,再确认密钥是否正确,最后确认解密工具的参数是否对。
我遇到过一种情况:密钥是对的,但数据库有多个,其中一个是用旧密钥加密的,另一个是新密钥。这种时候需要分别处理,不能一把梭。
5.2 消息缺失:多半是类型没覆盖
导出后发现消息数量比微信里少,通常是某些消息类型没处理。最常见的是 49 类型里的子类型,比如引用消息、合并转发、接龙、小程序卡片。这些在数据库里都是 49,但 XML 结构完全不同,需要逐个解析。
排查方法:把所有 49 类型的原始内容导出来,按 XML 根节点分类,看看有几种,然后逐个写解析规则。
5.3 图片打不开:.dat文件需要转格式
微信的图片存成.dat,文件头有加密标识。简单的判断方法是看文件头两个字节:0xFF 0xD8是 jpg,0x89 0x50是 png,0x47 0x49是 gif。判断出真实格式后,改扩展名就行。如果文件头是其他值,说明是加密的,需要先解密再判断。
5.4 Obsidian 里双链不生效:检查文件名和语法
双链不生效通常是两个原因:一是文件名里有特殊字符(比如#、[、]),Obsidian 解析不了;二是双链语法写错了,必须是[[文件名]],不能有多余空格。
我的做法是生成文件名时统一做一次清洗,把特殊字符替换成下划线,这样最稳。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 数据库打不开 | 未解密或密钥错误 | 确认版本和密钥 |
| 消息数量偏少 | 消息类型未覆盖 | 检查 49 类型解析 |
| 图片无法预览 | .dat未转格式 | 检查文件头并重命名 |
| 双链不跳转 | 文件名含特殊字符 | 清洗文件名 |
| 增量更新重复 | 时间戳记录有误 | 检查时间戳单位和时区 |
| 中文乱码 | 编码未指定 utf-8 | 读写都指定编码 |
5.6 几个我踩过的坑
坑一:不要在生产数据库上直接操作。我有一次图省事,直接在原始数据库上跑脚本,结果脚本有个 bug 写入了数据,虽然最后恢复了,但吓出一身冷汗。从此一律先复制再操作。
坑二:时间戳单位不统一。有的表存秒,有的存毫秒,混在一起排序会乱。统一转成秒再处理。
坑三:群聊的 talker 是群 ID,不是人名。需要额外查chatroom表才能拿到群名称,否则文件名全是@chatroom结尾的 ID,根本没法看。
坑四:Obsidian 库太大时会卡。如果聊天记录有几十万条,一次性导入会让 Obsidian 索引很慢。建议按年份或按会话分批导入,先导入最近半年的,确认没问题再补历史。
6. 关于数据安全和个人使用的几点体会
整个流程跑下来,我最深的体会是:数据在自己手里,和放在别人服务器上,是完全不同的两件事。这套方案的所有环节都在本地完成,不依赖任何第三方服务,密钥、数据库、解析脚本、输出文件,全程可控。对于聊天记录这种包含大量个人信息的数据来说,这一点比什么都重要。
另一个体会是,不要追求一步到位。我一开始想做一个全自动的工具,结果卡在密钥获取上很久。后来改成半自动:密钥手动获取,解析脚本自动跑,输出格式手动调。反而更快跑通了。工具是给自己用的,能解决问题就行,不必追求完美。
最后分享一个小技巧:如果你只是想把聊天记录导入 Obsidian 做检索,其实可以先不做多媒体解密,只导出文本消息,这样流程能简化一大半。等文本跑通了,再逐步加上图片和语音。分阶段推进,比一次性全做要稳得多。
这个方向后续还能扩展的地方不少,比如把聊天记录做成时间线视图、按关键词自动打标签、或者接入本地大模型做摘要。但那是下一步的事了,先把数据取出来、存好、能用,就已经解决了最核心的问题。