首页 / 资讯中心 / 文章详情

解密微信SQLite本地数据库:WeChatMsg密钥提取与聊天记录导出

解密微信SQLite本地数据库:WeChatMsg密钥提取与聊天记录导出 ★ FEATURED ARTICLE
简介这款微信聊天记录导出工具 WeChatMsg 2.0.10 为完整开源包主要面向有数据备份、留存与分析需求的用户可解决微信聊天记录体积臃肿、难以长期保存的痛点。资源共 259 个文件压缩包大小 24.69MB核心为 Python 源码并包含 HTML 模板、SVG/PNG 图标、Markdown 文档、配置文件及可执行文件便于直接使用或二次开发。实测备份一个约 1.4GB 的微信群聊四分钟即可导出全部文本、图片、表情包与系统信息输出体积仅 18MB再经 RAR 压缩可降至 2MB大幅去除微信自身冗余数据。导出结果按月份与页面总数分类可直接在任意浏览器中阅读也支持转存 Word、Excel并可基于聊天记录生成年度报告、训练个人 AI 助手。已有 1940 人学习/下载适合注重数据自主权、希望本地归档微信记录的个人用户。1. 项目定位一句话讲清楚 WeChatMsg 解决什么问题你需要一份聊天记录归档的时候想的可能是截图但更好的方式是导出一份结构化数据。微信 PC 端的所有消息都写在本地的加密 SQLite 文件中没有密钥时你连打开文件都做不到。WeChatMsg 2.0.10 就是那把钥匙它从微信进程内存里提取解密密钥解开本地聊天库导出为 HTML、TXT、CSV、JSON 等格式全程不依赖云端接口、不需要手机配合。这篇笔记写给三类人要留证归档的、要换机迁移记录的、想拿自己聊天数据做统计的开发者。接下来我按存储原理、核心实现、运行步骤、踩坑记录和二次开发的顺序把这个工具拆清楚。2. 原理与架构微信聊天记录在本地怎么存、工具怎么解2.1 数据落盘WeChat Files 目录里的三个核心数据库微信桌面版其实是典型的“本地优先”软件你发出去的每一条消息除了请求服务器转发外还会在收到响应后写入自己电脑上的数据库。这个数据库不是普通 SQLite 文件老版本微信可以直接用 SQLite 工具打开新版本则对文件做了整体改造。数据目录通常长这样%USERPROFILE%\Documents\WeChat Files\wxid_xxxxxxxxx\在账号目录下真正决定聊天记录完整度的东西都在Msg目录里。第一次拆这个目录时我遇到的文件和职责大概是下面这个对应关系数据库文件存放内容导出时怎么用MicroMsg.db联系人、群成员、会话列表把内部 wxid 映射成可读昵称MSG.db文字消息、引用消息、系统通知消息正文的主要来源MediaMSG.db图片、语音、视频的文件索引定位 FileStorage 下真实媒体文件FTSMSG.db全文检索索引关键词搜索不是导出必需现代微信的存储格式已经不是“一个完整的 SQLite 文件”而是把每个数据库切成多个固定大小的块逐块做加密再附加块索引。这种结构社区里叫 EFBEncrypted File Block。所以你把MSG.db拷出来用sqlite3直接打开只会收到 “file is not a database”。这也解释了为什么“把微信文件夹整个拷走”不等于备份成功因为后面还缺一把解密的钥匙。除了数据库还有一个独立目录FileStorage里面按Image/2024-05/xxx.dat、Video/2024-05/xxx.mp4分月存放真实媒体文件。数据库里存的只是这些文件路径和索引导出成 HTML 时如果只带库文件图片和视频引用会全部失效。这个点后续避坑章节会专门展开。2.2 密钥在哪进程内存里那把“后悔药”微信运行时要读数据库就必须把解密密钥放在自己进程的地址空间里。也就是说你不需要去破解算法只要能在内存里找到那把 key。WeChatMsg 的获取流程大致是四步按进程名WeChat.exe找到 PID通过 Windows API 打开进程并枚举可读内存区在内存中按特征串搜索例如固定偏移、固定开头字节命中特征串后在紧随其后的固定偏移处取出 32 字节密钥。这步听起来像黑魔法但本质上只是一个“读进程内存”的操作。不需要 root不需要手机端配合只要微信保持登录状态且版本在适配列表内工具就有机会把 key 找出来。微信一退出密钥随之释放下一次启动又重新生成所以每次导出前都要先登录微信。这里有一个容易误会的点拿到了 key就能解所有库吗不一定。每个数据库文件内部还有随机的盐参与运算同一个 key 对同一批文件有效。如果你把数据目录从另一台电脑拷过来即使拿到了当前微信进程的 key也解不开那批旧文件的概率很大。这也是跨机器迁移时不能用“只拷数据目录”方案的原因。2.3 整体架构一条导出请求要过的五个关卡WeChatMsg 从设计上分成了五层每一层有独立的失败原因排查问题之前最好先定位是哪一层挂了进程层定位微信进程、读取内存数据。失败症状是无密钥解码层用 pyefb 库读取 EFB 块并解密还原成临时 SQLite 文件。失败症状是“file is not a database”数据层对临时库执行 SQL按联系人和时间范围过滤。失败症状是导出为空解析层把消息类型码映射成文本、图片、语音等结构化字段渲染层把解析后的记录用模板或序列化器输出成 HTML、TXT、CSV、JSON。数据流可以画成这样WeChat.exe 运行中 │ ReadProcessMemory ▼ key32 字节 │ pyefb key ▼ 解密后的临时 SQLite 数据库 │ SQL 过滤 类型映射 ▼ 结构化消息列表 │ jinja2 / csv / json ▼ export.json / export.html / export.csv你只需要理解这条链路后面跑命令时才知道每一个进度提示在做什么报错时也知道该往哪个方向查。接下来进入核心代码拆解我会按这五层各选一个最关键的函数讲。3. 核心模块拆解从进程内存到 JSON 文件的完整链路3.1 密钥提取模块用 ReadProcessMemory 扫描微信内存读进程内存是用 Windows API 完成的我用 ctypes 封装了最简版本。这段代码的作用是打开微信进程从指定地址连续读若干字节再返回原始字节串。import ctypes from ctypes import wintypes PROCESS_VM_READ 0x0010 PROCESS_QUERY_INFORMATION 0x0400 READ_SIZE 4096 def read_memory_stream(pid: int, address: int, size: int) - bytes: 从指定进程地址空间读取 size 字节失败时返回空字节串 h_process ctypes.windll.kernel32.OpenProcess( PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid ) if not h_process: raise OSError(OpenProcess 失败请确认微信正在运行并重试) try: buffer ctypes.create_string_buffer(READ_SIZE) read_bytes ctypes.c_size_t(0) chunks [] current_addr address while current_addr address size: real_size min(READ_SIZE, address size - current_addr) ok ctypes.windll.kernel32.ReadProcessMemory( h_process, ctypes.c_void_p(current_addr), buffer, real_size, ctypes.byref(read_bytes) ) if ok: chunks.append(buffer.raw[:read_bytes.value]) else: chunks.append(b\x00 * real_size) current_addr real_size return b.join(chunks) finally: ctypes.windll.kernel32.CloseHandle(h_process)关键参数说明pid是 WeChat.exe 的进程 IDaddress是虚拟内存地址实际操作中会从 0 开始遍历整个用户态内存而不是只读某一个区间size是本次要扫描的总长度我一般给 256MB 上限。我每次读到 4KB 作为一个分片是因为 ReadProcessMemory 对单次长度有限制同时避免一次性申请过大内存导致失败。拿到内存流后下一步就是找特征串并提取 keyKEY_LEN 32 def find_key(pid: int, marker: bytes) - bytes: 在微信进程内存中定位特征串返回其后的 32 字节密钥 stream read_memory_stream(pid, 0, 256 * 1024 * 1024) idx stream.find(marker) if idx -1: raise RuntimeError(特征串未命中请检查微信版本是否在支持列表内) return stream[idx len(marker): idx len(marker) KEY_LEN]marker是随微信版本变化的特征串这个工具 2.x 系列会维护一张“微信版本号 → 特征串”的对照表。不同版本在同一版本的同一路径下偏移可能是固定的微信升级后特征串位置漂移这是工具发布新版本的最常见原因。从稳定角度讲特征串越短误命中越多所以真实项目里一般会用“特征串 偏移”双条件判断我只写了最简逻辑。3.2 数据库解密模块用 pyefb 把 EFB 还原成 SQLite拿到 key 之后需要面对的是 EFB 格式。我一般用 pyefb 库完成块读取与解密然后落盘成临时 SQLite 文件再交给标准 sqlite3 连接。from pyefb import EFB import sqlite3 import tempfile import os def open_wechat_db(db_path: str, key: bytes): 解密微信数据库文件返回(sqlite3.Connection, 临时文件路径) if not os.path.exists(db_path): raise FileNotFoundError(f{db_path} 不存在请检查数据目录) efb EFB(db_path, keykey) temp_path None with efb.open() as raw_db: fd, temp_path tempfile.mkstemp(suffix.db) os.close(fd) raw_db.save(temp_path) # 还原成明文 SQLite 文件 conn sqlite3.connect(temp_path) return conn, temp_path这段代码里需要理解三个细节。EFB(db_path, keykey)会根据文件头里的块索引逐块读取密文并解密但它不会直接返回一个文件对象而是暴露一个可写入的目标。raw_db.save(temp_path)是我最常用的输出方式因为后续 sqlite3 需要真实文件路径如果你二次开发想减少磁盘占用可以把临时文件删掉但要注意用完数据库连接后立刻conn.close()否则临时文件会一直被占住无法删除。tempfile.mkstemp(suffix.db)会生成一个带 .db 后缀的临时文件避免某些驱动因为后缀问题拒绝识别 SQLite 库。这一步看似多余实际上在 Windows 下很值得保留。3.3 消息解析模块把数据库行映射成可读字段消息解析是整个导出里最需要耐心的部分。数据库的MSG.db里每条消息有一堆字段其中最关键的是Type和StrContent。我给常用类型码做了一张映射表Type 值消息含义解析策略1文本StrContent 直接作为正文字段3图片从 BytesExtra 的 JSON 中解析路径34语音记录 FileStorage 下音频文件路径42公众号文章提取标题和链接49文件/引用/小程序解析 XML取 type 子节点10000系统消息过滤或标记实际解析的代码长下面这样import json from xml.etree import ElementTree as ET def parse_message(row: dict) - dict: 把一条数据库记录解析为导出用的字典 msg_type row.get(Type) content row.get(StrContent, ) if msg_type 1: return {type: text, content: content} if msg_type 3: extra json.loads(row.get(BytesExtra) or {}) return { type: image, path: extra.get(filePath, ), md5: extra.get(md5, ) } if msg_type 49: # 49 型内部是 XML需要再取一层 app type try: tree ET.fromstring(content) app_node tree.find(.//type) app_type app_node.text if app_node is not None else except ET.ParseError: app_type return {type: app, app_type: app_type, raw: content} return {type: unknown, raw: content[:200]}解析时最大的坑在Type 49。这是一个“复合消息”类型文件、引用回复、小程序卡片都混在这里面不能简单把StrContent当纯文本导出要先解析 XML 再判断内部子类型。如果你在做二次开发绝大多数新增导出字段的扩展点也在这个函数里因为它决定了下游所有渲染层能看到什么字段。3.4 导出渲染模块Jinja2 模板生成 HTML导出的最终格式各不相同。JSON、CSV 可以直接用标准库序列化HTML 则会用模板渲染。我这里贴一个最简模板片段from jinja2 import Template html_tpl Template( !DOCTYPE html html langzh-CN headmeta charsetutf-8title{{ talker }}/title/head body {% for msg in messages %} div classrow span classtime{{ msg.time }}/span span classtype{{ msg.type }}/span p{{ msg.content | e }}/p /div {% endfor %} /body /html ) def render_html(talker: str, messages: list) - str: 把解析后的消息列表渲染成 HTML 字符串 return html_tpl.render(talkertalker, messagesmessages)这里的核心逻辑在{{ msg.content | e }}。| e是 Jinja2 的转义过滤器会把script之类的内容转成纯文本避免导出文档里嵌入可执行脚本。这个细节在离线归档场景里很重要因为聊天记录内容不可控万一对方发来一段 HTML 或脚本片段不做转义会在本机打开时触发意外行为。经验上任何“导出为 HTML”的功能转义都应该是默认行为而不是可选配置。4. 快速跑通Windows 上用 2.0.10 完成第一次导出4.1 环境准备解压、装依赖、确认微信版本把 WeChatMsg-2.0.10.zip 解压后第一件事是检查 Python 版本。项目基于 Python 3.10 以上开发依赖了不少类型注解特性Python 3.8 和 3.9 实测跑起来会有兼容性报错。推荐的初始化步骤python --version python -m venv .venv .venv\Scripts\activate pip install -r requirements.txt python -m wechatmsg --version我习惯用虚拟环境而不是全局安装原因是requirements.txt里有 PyQt5这个包和某些版本的全局环境会互相污染。虚拟环境装好后如果最后一条命令能打印出版本号说明基础依赖都齐了。微信版本方面打开微信 PC 端确认它处于登录状态。不需要刻意使用旧版本工具 2.0.10 的适配列表通常跟随当前官方稳定版但是记住一个原则微信一旦升级先重新运行一次python -m wechatmsg --version再开始导出别拿旧的内存特征串去适配新版本。4.2 GUI 模式第一次点击导出按钮在项目目录下执行python main.py启动后是 Qt 窗口主界面有三个区域需要操作左侧选择一个地址微信数据根目录也就是上一章提到的WeChat Files父目录。工具会自动识别下面的wxid_xxx账号目录中间选择导出的联系人或群聊。如果想全部导出直接选“全部会话”右侧选择格式和输出目录。首次不建议直接选“全部会话 HTML”因为大库的 EFB 解码会耗时几分钟看起来像界面卡死容易误以为程序崩了。我首次跑通时用的策略是挑一个聊天记录最少的联系人导出 JSON打开文件确认字段正确然后再进行全量导出。这个习惯能帮你快速区分“代码有问题”和“数据量太大需要等待”这两种情况。4.3 命令行模式适合批量备份与定时任务GUI 适合交互式操作但如果要做每周定时备份还是命令行更可靠。2.0.10 提供了类似下面的入口python -m wechatmsg export \ --data-dir D:\WeChat Files \ --key auto \ --talker wxid_xxx \ --format json \ --out .\backup参数作用默认行为--data-dir微信数据根目录路径自动搜索Documents\WeChat Files--key指定密钥来源auto表示从内存提取从内存自动提取--talker只导出某个联系人或群聊不指定则导出全部--formathtml/txt/csv/json 之一html--out导出产物输出目录当前目录下 export 文件夹命令行模式有一个优点它把微信进程检查、EFB 解码、SQL 过滤、导出渲染全串在一个进程里任何一步出错都会返回非零退出码并往 stderr 打印错误信息。写定时任务时你可以用一段简单的 shell 逻辑判断退出码成功了再压缩归档失败了推送告警。不过要注意--talker参数接受的是wxid_xxx或群聊的wxid_xxx_cxxxx形态不是显示名称需要在 GUI 里先查到对应的真实 ID。4.4 导出完成后的验证清单导出完成不代表可信我会做三个快速检查wc -l export.json # 看消息总量是否与你预期量级一致 du -sh output_dir # 看产物总体积 ls output_dir/image/ # 确认媒体文件是否被复制过来第一条验证数量如果导出结果显示只有几十条而微信界面里明明有几百条大概率是时间过滤或联系人过滤设置错了。第二条验证体积如果 HTML 动辄几十 MB 但输出目录只有几百 KB说明媒体文件没有跟随导出去检查 FileStorage 目录是否完整。第三条验证图片如果输出目录里没有image子目录你要么没开启媒体复制选项要么源目录本身已缺文件。做完这三个检查这份导出才敢拿去归档。5. 避坑清单WeChatMsg 运行中常见的 5 个翻车现场5.1 现象提示“未获取到密钥”或“密钥为空”现象是最常见的工具卡在第一步永远进不了导出界面。原因基本是两种微信没有处于登录状态进程内存里根本没有可用的 key或者是微信版本太新旧特征串已经定位不到偏移位置。解决先打开微信并保持在线重试一次。还是失败的话查看当前微信版本号对照工具适配列表如果版本号不在表内换回列表内的版本再跑。这个问题的“后悔药”是提前冻结微信自动更新归档机器上不要总是升到最新版。当然微信本身也不能长时间挂机不退出否则数据库文件句柄膨胀内存读取速度会明显变慢。5.2 现象导出的 HTML 能打开但图片全部是破图现象是文字完整图片位置却全是空白或灰色图标。原因数据库里存的只是图片路径字符串图片真实文件放在FileStorage\Image\2024-05\xxx.dat工具默认不会把这些文件复制到导出目录。如果微信做过聊天图片清理源文件已经不存在这条记录就彻底无法恢复。解决导出时带上媒体复制选项把FileStorage里对应文件复制到输出目录。我做留档时习惯把整个FileStorage一起归档而不只依赖 HTML 里的相对路径因为 HTML 内的路径是数据库记录的原始路径目录结构稍有变化就找不到。更稳妥的方式是导出时做一次路径改写把媒体文件全部改名为image/2024-05-xxx.jpg这类可读格式同时修改数据库中的引用。这个小改造值得做以后你翻看归档时会感谢当时的自己。5.3 现象执行解密时报 “file is not a database”现象是 pyefb 解码完成落盘临时库但 sqlite3 打开时直接抛这个错。原因有两个一是直接拿原始MSG.db当明文库去连跳过了 EFB 解密步骤二是微信进程正在写库你趁它写一半时复制了文件抽出的是不完整的块。解决先关闭微信或至少先等待一段时间让微信把数据库写入落盘再复制整个Msg目录对副本执行解密。如果仍然报错检查数据库文件大小是否过小微信端会话太多导致数据库超过 2GB 的文件在部分文件系统下会被截断这属于源数据问题工具无法逆转。5.4 现象导出的 CSV 里自定义表情全是空单元格现象是文本消息正常但所有自定义表情、部分合并转发消息显示为空。原因自定义表情的真实文件存放在FileStorage\emoji目录数据库里只有 MD5 和路径索引而且旧版本微信并不保证元数据完整性。另外CSV 导出时如果直接用逗号分隔表情消息的StrContent里有换行符会被不严格的 CSV 解释器切断。解决把FileStorage\emoji整个复制出来按 MD5 与导出字段关联处理 CSV 时设置quotechar让包含换行符的字段被正确引号包裹。如果你二开要导入 pandas用pd.read_csv(..., quoting1)才能拿到完整字段否则字段会被截断。5.5 现象GUI 启动闪退或界面文字全部变成方框现象是在某些 Windows 机器上双击python main.py后窗口短暂出现就消失或者按钮文字显示成方块。原因PyQt5 依赖 Microsoft Visual C 运行库缺了MSVCP140.dll就会闪退另一类情况是系统默认非中文区域Qt 找不到中文字体渲染出方块。解决安装 VC 2015-2022 Redistributable 运行库把系统区域和非 Unicode 程序语言改成简体中文。如果来不及改系统直接放弃 GUI用第 4.3 节的命令行模式纯文本终端没有字体渲染问题备份功能完全不受影响。我维护这类工具时遇到过多次 GUI 环境问题最后都退回命令行方案这也侧面说明 CLI 接口在这个项目里的可用性不亚于 GUI。6. 二次开发思路把导出的 JSON 变成聊天年报与活跃度统计拿到export.json就拿到了自己微信对话的结构化副本剩下的就是数据挖掘。我在这里展示两个最实用的统计入口。import json import pandas as pd with open(export.json, r, encodingutf-8) as f: data json.load(f) msgs pd.DataFrame(data[messages]) msgs[time] pd.to_datetime(msgs[createTime], unitms) # 关系热榜谁和你说话最多 volume msgs.groupby(talker).size().sort_values(ascendingFalse) print(volume.head(10)) # 作息指纹你最常在哪个小时回消息 hourly msgs.groupby(msgs[time].dt.hour).size().sort_values(ascendingFalse) print(hourly.head(5))这里有两个关键参数要注意。createTime是毫秒时间戳unitms不能省我第一次做年度统计时漏了它所有消息时间全部变成了 1970 年数据量越大越难发现排查花了很久。talker字段在微信数据库里是原始 wxid如果你想展示成昵称需要再关联一份 MicroMsg.db 里的映射表别直接在界面层写死显示名。再往下延伸可以给解析模块加一个回调钩子在不改动工具核心的情况下注入自定义逻辑def export_with_extra(parser, message: dict) - dict: parsed parser.parse_message(message) parsed[keyword_hit] len(extract_keywords(message[StrContent])) return parsed这种模式的好处是把“工具核心解析”和“业务扩展字段”解耦。我通常把这类扩展函数放在一个独立文件里而不是塞进项目主类这样后续升级工具时可以直接替换源码包业务分析逻辑不受影响。改动后怎么验证我一般会做两步。第一步抽查单条消息在微信里找到同一条对比时间戳、发送人、正文是否一致重点看图片和文件类型的路径字段有没有少一层目录。第二步做总数比对把导出的记录条数和微信会话界面的消息数对比允许 5% 以内的误差如果误差过大优先怀疑解析分支漏掉了某个消息类型。做完整套流程后我的习惯是每改完一次二次开发代码就强制跑一遍“单条抽查 总量对比 时间戳校验”三件套不再靠肉眼翻浏览器页面。希望这篇拆解能帮你少走几步弯路也期待你在二次开发里玩出比我更顺手的统计报表。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站