微信聊天记录里藏着大量有价值的信息,但它的存储格式一直是个让人头疼的问题。PC端微信用的是加密的SQLite数据库,手机端导出的又是各种奇奇怪怪的dat文件,想把这些数据拿出来做点事情,比如喂给AI做分析、导入笔记软件做知识管理,中间隔着一道不低的墙。最近我在折腾一个开源方案,把微信聊天记录解密、清洗、格式化之后,直接对接Codex和Obsidian,整个过程跑通之后确实省了不少事。这篇文章就把我踩过的坑、试过的方案、最终跑通的流程完整记录下来,适合有Python基础、想把自己的聊天数据用起来的读者参考。
1. 微信聊天记录到底存在哪里,格式是什么样的
1.1 PC端微信的数据库结构
PC端微信(4.x版本)的数据默认存放在Documents\WeChat Files\目录下,每个登录过的账号会有一个以wxid开头的文件夹。进去之后核心目录是Msg,里面按月份分文件夹存放MSG0.db、MSG1.db这样的SQLite数据库文件。这些db文件是加密的,直接用SQLite工具打开会提示"file is not a database"。
加密方式用的是SQLCipher,密钥派生自微信账号相关的信息。不同版本的微信,密钥的生成算法有差异,这也是为什么网上能找到的解密工具往往只针对特定版本有效。我实测下来,4.x版本的密钥获取需要从微信进程的内存中读取,这一步是整个流程里最关键的环节。
除了Msg目录,还有FileStorage目录存放图片、视频、文件等附件,Contact目录存放联系人信息。聊天记录的主体在db文件里,附件是单独存储的,通过消息里的路径字段关联。
1.2 手机端导出的dat文件是什么
手机端微信的聊天记录导出后,常见的是.dat文件。这个dat不是标准格式,而是微信自己的一套封装。文件头通常有固定的magic number,后面跟着加密或压缩的数据。网上有一些"微信dat文件查看器"工具,原理就是识别文件头、做对应的解码。
dat文件里可能是图片(微信对图片做了异或加密)、可能是语音(silk编码)、也可能是文本消息的备份。不同来源的dat文件结构不一样,有的是从手机备份里提取的,有的是通过"迁移聊天记录"功能生成的。处理之前得先判断这个dat到底装的是什么。
1.3 为什么不能直接读
核心障碍有两个:一是加密,SQLCipher的加密不是简单异或,没有密钥基本不可能暴力破解;二是格式不统一,PC端是db,手机端是dat,还有各种备份格式,没有一套通用的解析方案。
我试过直接用Python的sqlite3库去打开PC端的db文件,报错信息很明确——不是有效的数据库文件。换成pysqlcipher3,需要提供密钥,但密钥从哪来又是个问题。后来找到的思路是从运行中的微信进程里读密钥,这个方案在Windows上可行,Linux和macOS上要复杂一些。
2. 解密环节:从进程内存里把密钥捞出来
2.1 密钥获取的基本原理
微信在启动时会根据账号信息生成一个密钥,用于加解密本地的SQLite数据库。这个密钥在微信运行期间会存在于进程的内存空间中。我们的目标就是把这个密钥从内存里读出来。
具体做法是:找到微信进程,读取它的内存,然后根据密钥的特征(比如长度、字符集、在内存中的位置规律)来定位。不同版本的微信,密钥的特征不一样,需要针对性地调整搜索策略。
我用的方案是参考了一个开源项目的思路:先枚举微信进程的内存区域,然后在这些区域里搜索符合SQLCipher密钥特征的字节序列。SQLCipher的密钥通常是32字节的十六进制字符串或者passphrase,在内存里会以特定的形式出现。
2.2 实际操作步骤
在Windows上,我用的工具组合是Python +pymem库。pymem可以枚举进程、读取内存,比直接调Windows API方便很多。
import pymem import re def find_wechat_key(process_name="WeChat.exe"): pm = pymem.Pymem(process_name) # 枚举内存区域 for region in pm.list_memory_regions(): try: data = pm.read_bytes(region.BaseAddress, region.RegionSize) except Exception: continue # 搜索符合密钥特征的模式 # 这里的具体模式需要根据微信版本调整 matches = re.findall(rb'[0-9a-f]{64}', data) for m in matches: # 进一步验证这个key能不能解密db if validate_key(m): return m return None这段代码是简化版,实际用的时候要注意几点:内存区域可能很大,全量读取会消耗大量内存和时间;正则匹配到的候选key可能有多个,需要逐个验证;微信版本更新后,密钥的特征可能变化,正则要跟着调。
验证key的方法是拿它去尝试解密db文件,如果能成功读出SQLite的头部(SQLite format 3),说明key是对的。
from pysqlcipher3 import dbapi2 as sqlcipher def validate_key(key): try: conn = sqlcipher.connect("path/to/MSG0.db") conn.execute(f"PRAGMA key='{key.decode()}'") conn.execute("SELECT count(*) FROM sqlite_master") return True except Exception: return False2.3 踩坑记录
第一个坑是权限问题。读取其他进程的内存需要管理员权限,普通用户运行会报"Access Denied"。解决办法是以管理员身份运行Python脚本。
第二个坑是微信版本更新。我一开始用的方案是针对某个特定版本的,微信自动更新之后密钥特征变了,脚本就失效了。后来改成更通用的搜索策略,不依赖固定的偏移量,而是靠特征匹配,稳定性好了很多。
第三个坑是内存读取的性能。微信进程的内存可能有几个GB,全量读取一次要几十秒。优化方法是先过滤掉明显不可能包含密钥的区域(比如只读的代码段),只扫描可读写的数据段,速度能快不少。
注意:读取进程内存这个操作本身是合法的技术手段,但只应该用于处理你自己的数据。不要用来获取他人的聊天记录,这是底线。
3. 数据清洗:从原始db到结构化消息
3.1 理解微信的消息表结构
解密后的db文件里,核心表是MSG,字段包括localId、TalkerId、MsgSvrID、Type、SubType、IsSender、CreateTime、StrContent、StrTalker等。Type字段标识消息类型,1是文本,3是图片,34是语音,43是视频,49是各种应用消息(链接、文件、引用等)。
StrContent对于文本消息是直接的内容,对于其他类型可能是XML或JSON格式的元数据。比如图片消息的StrContent里会有图片的路径、md5等信息。
TalkerId关联到Contact表,可以拿到聊天对象的昵称、备注等信息。群聊的消息里,发送者的信息在BytesExtra字段里,需要额外解析。
3.2 清洗流程设计
我的清洗流程分几步:先按TalkerId分组,把同一个聊天对象的消息聚在一起;然后按CreateTime排序;接着根据Type字段做不同的处理——文本直接取StrContent,图片和文件提取路径和文件名,语音和视频做标记;最后统一输出成结构化的JSON或Markdown。
def extract_messages(db_path, key): conn = sqlcipher.connect(db_path) conn.execute(f"PRAGMA key='{key}'") cursor = conn.execute(""" SELECT localId, TalkerId, Type, SubType, IsSender, CreateTime, StrContent, StrTalker FROM MSG ORDER BY CreateTime """) messages = [] for row in cursor: msg = { "id": row[0], "talker_id": row[1], "type": row[2], "sub_type": row[3], "is_sender": row[4], "timestamp": row[5], "content": row[6], "talker": row[7] } messages.append(msg) return messages3.3 处理特殊消息类型
文本消息最好处理,直接拿StrContent就行。图片消息的StrContent里通常有<img>标签,里面包含cdnbigimgurl、md5等属性,需要正则提取。文件消息的XML里有title(文件名)和totallen(文件大小)。
引用消息(Type=49,SubType=57)比较麻烦,它的StrContent是一个XML,里面嵌套了被引用的消息内容和当前回复的内容。需要解析XML,把两部分拆开。
系统消息(Type=10000)是"你撤回了一条消息"、"xxx邀请xxx加入了群聊"这类,通常不需要保留,清洗时可以过滤掉。
语音消息(Type=34)的StrContent里只有时长和格式信息,实际的语音数据在Media目录下的silk文件里。如果要转文字,需要额外的语音识别步骤,这个我暂时没做,只是标记了语音消息的位置。
3.4 输出格式的选择
输出成什么格式,取决于后续怎么用。如果是要喂给Codex做分析,JSON比较合适,结构清晰,程序好处理。如果是要导入Obsidian,Markdown更合适,可以直接作为笔记。
我做了两个输出通道:一个生成JSON,包含完整的元数据;一个生成Markdown,按日期分文件,每条消息一行,带时间戳和发送者。Markdown的格式大概是这样:
## 2024-01-15 - **10:23** 张三: 明天的会议改到下午三点了 - **10:25** 我: 收到,我调整一下日程 - **10:30** 张三: [图片] 会议室的预订截图这种格式在Obsidian里看起来很清楚,而且可以用Dataview插件做查询和统计。
4. 对接Codex:让AI帮你分析聊天记录
4.1 为什么要对接Codex
聊天记录里有很多非结构化的信息,比如讨论过的项目、约定的事项、分享的链接。人工翻看效率很低,但让AI来提取关键信息就快很多。Codex作为代码生成和分析工具,处理结构化数据的能力很强,把聊天记录转成它认识的格式,就能让它帮忙做总结、提取待办、分析话题分布。
4.2 数据准备
Codex对输入的长度有限制,不能一次性把几年的聊天记录全塞进去。我的做法是按时间窗口切分,比如按周或按月生成一个摘要文件,然后让Codex逐个处理。
每个摘要文件里包含这个时间窗口内的所有消息,格式化成Codex容易理解的文本。我试过直接用Markdown格式,Codex能很好地解析。关键是每条消息要有明确的时间戳和发送者标识,这样AI才能理解上下文。
def generate_weekly_summary(messages, output_dir): from collections import defaultdict from datetime import datetime weeks = defaultdict(list) for msg in messages: dt = datetime.fromtimestamp(msg["timestamp"]) week_key = dt.strftime("%Y-W%W") weeks[week_key].append(msg) for week, msgs in weeks.items(): lines = [] for m in msgs: dt = datetime.fromtimestamp(m["timestamp"]) sender = "我" if m["is_sender"] else m["talker"] content = clean_content(m["content"], m["type"]) lines.append(f"- **{dt.strftime('%m-%d %H:%M')}** {sender}: {content}") with open(f"{output_dir}/{week}.md", "w", encoding="utf-8") as f: f.write(f"# {week} 聊天记录\n\n") f.write("\n".join(lines))4.3 给Codex的提示词设计
提示词的质量直接决定输出质量。我试了几版,最终稳定下来的提示词是这样的:
你是一个聊天记录分析助手。以下是我和某人的微信聊天记录, 请帮我完成以下任务: 1. 提取所有约定的事项和待办,标注时间和相关人 2. 总结讨论过的主要话题,每个话题用一句话概括 3. 找出所有分享的链接和文件,列出标题和用途 4. 标记出可能需要跟进的消息 输出格式用Markdown,分四个部分。这个提示词的关键是把任务拆得足够具体,每个任务都有明确的输出要求。太笼统的提示词(比如"帮我分析一下")得到的结果往往很泛,没什么用。
4.4 实际效果和局限
实测下来,Codex在提取待办和总结话题方面表现不错,准确率大概在80%左右。漏掉的主要是那些表述比较隐晦的约定,比如"那到时候再说"这种,AI不太能判断是不是真的有待办。
链接和文件的提取基本没问题,因为格式比较固定。话题总结有时候会把相关的话题合并得太粗,需要人工再细分。
一个重要的局限是:Codex看不到图片和语音的内容。如果聊天里大量使用图片沟通,AI能拿到的信息就有限。解决办法是先用OCR把图片转成文字,再一起喂给AI。语音的话需要先做语音识别,这个我还没集成进去。
提示:给AI的聊天记录里可能包含隐私信息,如果用的是云端服务,要注意数据安全。敏感内容建议先脱敏再处理。
5. 对接Obsidian:把聊天记录变成知识库
5.1 Obsidian的目录结构设计
Obsidian的核心是Markdown文件加双链。我把聊天记录按"人-年-月"的层级组织:
ChatRecords/ ├── 张三/ │ ├── 2024/ │ │ ├── 2024-01.md │ │ ├── 2024-02.md │ │ └── ... │ └── 2023/ ├── 李四/ └── ...每个人的文件夹下按年份分,年份下按月份分文件。这样既不会单文件太大,也方便按时间检索。
每个月份文件的开头加一个元数据块,记录这个月的消息数量、活跃天数等信息,方便后续用Dataview查询。
--- person: 张三 year: 2024 month: 01 message_count: 342 active_days: 18 --- # 2024年1月 与张三的聊天记录 - **01-01 09:15** 张三: 新年快乐! ...5.2 用Dataview做查询
Obsidian的Dataview插件可以对这些Markdown文件做结构化查询。比如我想知道和张三最近三个月的消息量趋势:
TABLE message_count, active_days FROM "ChatRecords/张三" WHERE year = 2024 SORT month DESC LIMIT 3或者找出所有提到某个关键词的消息:
LIST FROM "ChatRecords" WHERE contains(file.content, "项目截止")Dataview的查询能力有限,复杂的分析还是得靠外部脚本。但对于日常的检索和统计,已经够用了。
5.3 双链和标签的自动生成
Obsidian的双链是它的核心价值。我在生成Markdown的时候,会自动把一些实体加上双链。比如聊天里提到的人名、项目名、地点,如果已经在知识库里有对应的笔记,就自动加上[[ ]]。
实现方式是维护一个实体列表,生成Markdown时做字符串匹配替换。这个列表可以手动维护,也可以从已有的Obsidian笔记里自动提取。
def add_links(content, entity_list): for entity in entity_list: if entity in content: content = content.replace(entity, f"[[{entity}]]") return content标签的生成类似,根据消息内容自动打上#待办、#链接、#图片这样的标签。这样在Obsidian的标签面板里就能快速筛选。
5.4 增量更新的处理
聊天记录是持续增长的,不可能每次都全量重新生成。我的做法是记录上次处理到的时间戳,每次只处理新消息,追加到对应的月份文件里。
def incremental_update(last_timestamp, db_path, key): conn = sqlcipher.connect(db_path) conn.execute(f"PRAGMA key='{key}'") cursor = conn.execute(""" SELECT localId, TalkerId, Type, IsSender, CreateTime, StrContent, StrTalker FROM MSG WHERE CreateTime > ? ORDER BY CreateTime """, (last_timestamp,)) # 处理新消息,追加到对应文件 ...增量更新要注意的是,微信的消息时间戳是秒级的,如果同一秒内有多个消息,用>可能会漏掉。稳妥的做法是用>=,然后在写入时去重。
6. 整个流程的自动化串联
6.1 用脚本把各环节串起来
手动一步步操作太麻烦,我写了一个主脚本来串联整个流程:
def main(): # 1. 获取密钥 key = find_wechat_key() if not key: print("未找到密钥,请确认微信正在运行") return # 2. 解密并提取消息 messages = extract_messages(DB_PATH, key) # 3. 增量过滤 last_ts = load_last_timestamp() new_messages = [m for m in messages if m["timestamp"] > last_ts] # 4. 生成Markdown generate_markdown(new_messages, OBSIDIAN_DIR) # 5. 生成Codex输入 generate_codex_input(new_messages, CODEX_DIR) # 6. 更新last_timestamp if new_messages: save_last_timestamp(max(m["timestamp"] for m in new_messages)) print(f"处理完成,新增 {len(new_messages)} 条消息") if __name__ == "__main__": main()这个脚本可以手动跑,也可以设置成定时任务。Windows上用任务计划程序,Linux上用cron,每天跑一次就行。
6.2 定时任务的配置
Windows任务计划程序的配置要点:触发器设为每天一次,操作设为运行Python脚本,起始目录设为脚本所在目录。注意要勾选"使用最高权限运行",否则读内存会失败。
Linux上cron的配置:
0 8 * * * cd /path/to/script && /usr/bin/python3 main.py >> /var/log/wechat_export.log 2>&1每天早上8点跑一次,日志输出到指定文件方便排查问题。
6.3 异常处理和日志
自动化流程最怕的是静默失败。我在关键步骤都加了日志和异常处理:
import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('wechat_export.log'), logging.StreamHandler() ] ) try: key = find_wechat_key() except Exception as e: logging.error(f"获取密钥失败: {e}") raise常见的失败场景包括:微信没运行、版本更新导致密钥特征变化、db文件被锁定、磁盘空间不足。每种情况都要有对应的日志输出,方便快速定位。
7. 几个实际使用中的经验教训
7.1 性能优化的几个点
全量处理几年的聊天记录,数据量可能有几十万条消息。我一开始的脚本跑一次要十几分钟,后来做了几个优化,降到两分钟左右。
第一个优化是批量读取。不要一条条查,用fetchall()一次性把结果拉出来。第二个优化是减少重复的密钥验证,密钥获取一次就够了,不用每条消息都验证。第三个优化是Markdown生成时用字符串拼接而不是频繁的文件写入,最后一次性写文件。
7.2 数据备份的重要性
处理聊天记录之前,一定要先备份原始的db文件。我有一次脚本写错了,把解密后的db文件覆盖了原始文件,结果原始数据没了。虽然解密后的数据还在,但万一解密过程有问题,原始数据就是最后的保障。
备份的方法很简单,把整个WeChat Files目录复制一份就行。注意微信运行的时候db文件可能被锁定,复制之前先退出微信。
7.3 隐私保护的边界
聊天记录涉及双方的隐私,处理的时候要有边界意识。我的原则是:只处理自己的聊天记录,不处理他人的;如果要把数据分享出去(比如发到社区),一定要先脱敏,把真实姓名、手机号、地址这些替换掉。
给AI处理的时候也要注意,如果用的是云端服务,数据会离开本地。敏感内容建议用本地的模型处理,或者先做脱敏。
7.4 版本兼容性的坑
微信更新很频繁,每次更新都可能影响密钥获取和db结构。我的经验是:不要追最新版,用一个稳定的版本,把自动更新关掉。等社区确认新版本没问题了再升级。
另外,不同平台的微信(Windows、macOS、Linux)数据结构有差异,脚本不能通用。我主要是在Windows上用的,macOS上的方案需要另外适配。
7.5 关于开源方案的选型
网上能找到不少开源的微信聊天记录导出工具,选型的时候主要看几点:是否支持你用的微信版本、是否还在维护、文档是否清晰、社区是否活跃。我试过几个,有的已经很久没更新了,有的只支持老版本微信。
最终我选择的是自己组装方案,用开源项目里的密钥获取思路,加上自己写的清洗和输出逻辑。这样灵活性最高,出了问题也好排查。
整个流程跑通之后,我现在每天早上的定时任务会自动把前一天的新消息同步到Obsidian,同时生成Codex的分析输入。需要做周报或者回顾项目进展的时候,直接看Obsidian里的汇总就行,省了很多翻聊天记录的时间。这套方案不算完美,但对我来说已经够用了。如果你也在折腾类似的事情,希望这些经验能帮你少走点弯路。