简介这款微信聊天记录提取与分析工具专门面向有长期备份与复盘需求的个人用户以及希望研究聊天数据解析和可视化的开发者。它支持将微信聊天导出为HTML、Word、CSV等常用格式便于永久存档、跨平台查阅与二次编辑并内置分析引擎可生成年度聊天报告从而系统回顾重要对话与互动趋势。压缩包共257个文件大小约24.96MB其中103个Python脚本构成核心功能61个PNG与43个SVG提供界面图标与图形素材18个HTML为报告模板另有JSON/MD配置文件、QSS界面样式及可直接运行的EXE程序整体结构清晰、开箱即用。目前已有9700余人学习下载工具既能满足普通用户一键备份、生成可视化聊天报告的需求也可为高级用户深入理解数据提取、词云展示等二次开发提供完整参考。1. 微信聊天记录提取为什么“能导出成 HTML、Word、CSV”比“能看聊天记录”更重要微信自带的聊天记录备份功能解决的是“迁移”不是“保存”。很多人真正常用手机号登录微信等手机坏了、换工作要交接、打官司要留证时才发现备份里的聊天记录只能在另一台微信上恢复没法单独打开、打印、检索。标题里的工具把这件事拆得很清楚从微信数据库拉出聊天记录再导出成 HTML、Word、CSV 三种格式让每一段对话变成普通文件能点开看、能打印、能长期归档。这篇文章按一条实操技术线路讲透微信聊天记录以什么形式存在、怎么解密拿到明文、用 Python 怎么做三种导出、过程中会翻车的细节。适合想自己动手做聊天存档的开发者也适合准备把这类提取工具做成产品的团队做技术选型。2. 先搞懂微信的“聊天记录”到底是什么数据库结构、加密机制与读取前提2.1 EnMicroMsg.db 与 SQLCipher为什么直接打开是乱码微信客户端并不把聊天记录存成一个个文本文件而是统一写进本地 SQLite 数据库。安卓端的数据库路径通常是/data/data/com.tencent.mm/MicroMsg/{hash}/EnMicroMsg.dbWindows 端则在WeChat Files/{wxid}/db/下的MSG*.db系列文件中。真正动手时会发现一个问题直接用任意 SQLite 工具打开这些库得到的不是表结构而是file is not a database之类的报错。原因在于微信为了防篡改和防第三方读取用 SQLCipher 对 SQLite 文件做了整库加密文件里全部是密文普通读取无从下手。import sqlite3 try: conn sqlite3.connect(EnMicroMsg.db) cur conn.cursor() cur.execute(SELECT count(*) FROM sqlite_master) except sqlite3.DatabaseError as e: print(直接打开加密库失败错误信息, e) # 典型输出: file is not a database这段代码展示了“硬开”加密库的必然结果。逻辑很简单sqlite3 模块按标准 SQLite 格式去解析文件头发现文件头不是 SQLite 的 magic就抛异常。别被这个报错吓退它只说明库是加密的不代表数据损坏。拿到正确的 SQLCipher 密钥后同一个文件可以正常读出完整的表数据。另一个常见的误判是把数据库文件大小当成判断依据。有人看到 EnMicroMsg.db 只有几 MB怀疑记录不全。其实聊天文字内容非常紧凑一条短文本记录在表里占几十字节几 MB 已经能装下数万条消息。图片、语音、视频并不存在这个库里它们以独立文件存到FileStorage等附件目录数据库里只保留路径和索引信息。2.2 解密密钥从哪来IMEI uin 派生与 Windows 端获取差异老版本安卓微信的密钥规律在很长一段时间里是固定的把设备 IMEI 和微信账号的 uin 拼接成字符串计算 MD5取十六进制结果的前 7 位作为 SQLCipher 的密钥。uin 不是微信号是微信在服务端分配的一个内部数值 ID登录状态下的微信配置里能看到。import hashlib imei 864123052716480 # 老版本提取用需替换成实际设备值 uin 106657510 # 微信账号对应的内部 uin不是微信号 raw f{imei}{uin}.encode(utf-8) key hashlib.md5(raw).hexdigest()[:7] print(sqlcipher key:, key)这段代码最终打印出的 7 位十六进制字符串就是用于打开旧版数据库的 SQLCipher 口令。逻辑上没太多花样拼接、哈希、截断。但把它当成万能方案会在新版微信上翻车——微信从某一版本开始把密钥改成了运行时内存中生成的动态密钥简单的 IMEI uin 拼接已经失灵。实际项目里更常用、更可靠的做法是绕开密钥推导Windows 微信登录状态下从微信进程内存中直接读取已解密后的数据库密钥或直接读取连接句柄安卓端在 root 环境下注入进程也能拿到同样的东西。这类方案依赖具体的微信版本实践上通常借助现成工具提取而不是自己重新实现一遍内存逆向。我们的核心工作重心不在这里而是在“拿到明文 SQLite 之后怎么导出成三种格式”。拿到密钥后可以用 pysqlcipher3 打开加密库并把它导成明文 SQLite 文件方便后续所有脚本用标准 sqlite3 读取。# 用 pysqlcipher3 打开加密库并导出为明文库 from pysqlcipher3 import dbapi2 as sqlite in_path EnMicroMsg.db out_path decrypted.db conn sqlite.connect(in_path) conn.execute(fPRAGMA key {key}) # key 来自前面的 7 位 hex conn.execute(PRAGMA cipher_use_hmac OFF) # 老版本微信没开 HMAC要显式关闭 cur conn.execute(SELECT name FROM sqlite_master WHERE typetable) tables [row[0] for row in cur.fetchall()] print(明文库中的表, tables) plain sqlite.connect(out_path) for t in tables: plain.execute(fATTACH DATABASE {in_path} AS encrypted) plain.execute(fCREATE TABLE IF NOT EXISTS {t} AS SELECT * FROM encrypted.{t}) plain.commit()代码逻辑分两段先用加密连接读表名再在明文库里通过ATTACH方式复制结构。cipher_use_hmac这个参数是典型的版本兼容项老库按不带 HMAC 的格式加密新库默认带 HMAC两者不能混用。破解时如果一直报Decryption failed优先排查这一句。提示pysqlcipher3 在部分 Python 环境下需要编译安装依赖 libsqlcipher。装不上时可以用系统自带的 sqlcipher 命令行工具操作同一批 PRAGMA实现思路完全一致。2.3 拿到明文数据库之后的表结构概览解密只是第一步接下来才是重点从几十张表里找到聊天记录真正存哪里。安卓端 EnMicroMsg.db 里跟聊天强相关的主要是两类表消息表message和联系人表rcontact。表名职责关键字段message存储所有对话的消息记录localId, talker, type, subType, isSend, createTime, contentrcontact存储联系人资料username, nickname, conRemarkchatroom群聊基础信息chatroomname, memberlistimg_flag图片等附件的索引信息msgId, imgPath, compressType实际使用中最容易出错的地方是message.talker字段。单聊时它存的是对方的 wxid群聊时它是一个以chatroom结尾的群 ID并不直接告诉你群名字是什么。显示名称要到rcontact表里按 username 关联才能补全所以导出时极少只查一张表。Windows 端 3.x 之后的MSG*.db结构类似但表和字段名在不同版本里有细微差异比如部分版本把createTime写成CreateTime。SQLite 对列名大小写不敏感这个问题一般不会暴露。真正会暴露的是列名拼写差异所以写导出脚本前先执行一次PRAGMA table_info(message)把当前库的字段清单打出来再决定 SQL 怎么写。我一般会把这个检查作为导出脚本的第一步省去后面来回改字段名的麻烦。3. 从解密后的数据库里取出“全部聊天记录”SQL 怎么写、字段怎么选3.1 message 表字段地图类型码才是筛选的核心聊到字段明细必须先讲类型码因为content字段在不同消息类型里的内容是不同形态的。文本消息直接就是纯文字图片消息存的是 XML 片段语音、视频、文件消息存的是路径和描述系统通知存的是中文提示文字。消息类型码含义content 内容示例1文本你好明天见3图片img src... /或路径描述34语音/voice/... .amr等路径43视频视频描述与路径49文件/链接/小程序JSON 或 XML含 title、url、des10000系统通知你已添加了对方现在可以开始聊天了筛选时只需要type IN (1, 3, 34, 43, 49)就能覆盖绝大多数有效聊天内容10000这类系统通知可以选择性保留因为它夹杂着“对方撤回了一条消息”“你已通过对方的好友验证”这类操作记录归档价值不如真正的聊天内容高。isSend字段也要注意0 表示发给我的消息1 表示我发出的消息。导出时要把这个值翻译成“我”和“对方”不能原样输出 0 和 1否则看文档的人还得对照数字猜含义。对于群聊rcontact里没有群成员与消息的映射文本消息的发送者在旧版本中较难直接取出需要从 content 或 subType 中解析这里暂不展开单聊场景下isSend已经够用。3.2 带联系人昵称的查询rcontact 表怎么 JOIN 进来message.talker是 wxid 而不是昵称直接导出会得到一堆wxid_xxx可读性很差。要让每条消息带上可读的发送人名称得去关联rcontact表。rcontact里有两个字段可以直接用于显示nickname是原始昵称conRemark是用户给联系人设置的备注。SELECT m.localId, m.createTime, m.type, m.isSend, m.content, COALESCE(c.conRemark, c.nickname, m.talker) AS display_name FROM message m LEFT JOIN rcontact c ON c.username m.talker WHERE m.talker :talker_id AND m.type IN (1, 3, 34, 43, 49) ORDER BY m.createTime ASCSQL 里用COALESCE做名称降级有备注优先显示备注没有备注用昵称昵称也没有就直接把 wxid 放上去。为什么不直接用INNER JOIN因为微信账号删掉、对方注销、或者联系人被清理后rcontact里可能没有对应记录LEFT JOIN能保证消息一条不丢只是名称回退成 wxid。:talker_id 这个参数就是用户在工具界面输入的联系人 wxid。导出单聊记录时它就是对方 wxid导出群聊记录时它就是群 id。脚本里要把它做成命令行参数或配置文件输入不要硬编码在 SQL 里。3.3 真正能跑的 Python 查询脚本把前面的 SQL 写进 Python 脚本还需要处理两个细节时间戳转成可读时间以及把查询结果封装成字典列表供后续导出模块使用。import sqlite3 from datetime import datetime conn sqlite3.connect(decrypted.db) conn.row_factory sqlite3.Row cur conn.cursor() def ts_to_str(ts_ms): 把微信的毫秒时间戳转成本地时间字符串 return datetime.fromtimestamp(ts_ms / 1000).strftime(%Y-%m-%d %H:%M:%S) def fetch_chat(talker_id, max_time0): sql SELECT m.createTime, m.type, m.isSend, m.content, COALESCE(c.conRemark, c.nickname, m.talker) AS display_name FROM message m LEFT JOIN rcontact c ON c.username m.talker WHERE m.talker :talker_id AND m.type IN (1, 3, 34, 43, 49) AND m.createTime :max_time ORDER BY m.createTime ASC cur.execute(sql, {talker_id: talker_id, max_time: max_time}) rows [] for r in cur.fetchall(): rows.append({ time: ts_to_str(r[createTime]), type: r[type], isSend: r[isSend], sender: 我 if r[isSend] 1 else r[display_name], content: r[content], }) return rows if __name__ __main__: rows fetch_chat(wxid_xxxxxxxx, max_time0) print(导出条数, len(rows)) if rows: print(示例, rows[0])脚本里两个细节值得说清楚。第一个是datetime.fromtimestamp(ts_ms / 1000)微信存的是 13 位毫秒时间戳Python 的 fromtimestamp 接受的是秒不除以 1000 会把时间放大一千倍得到的结果直接偏到 1970 年。第二个是max_time参数它本身是为后续增量导出准备的游标本次传 0 表示全量导出后续传上次导出的最后一条时间戳即可只拉新增部分。注意content字段在导出时保留原始格式。文本消息直接可用图片消息的 XML 片段在三种导出格式里样式不同需要在下一章按目标格式分别处理不能无脑写入文档。4. 三种格式导出的实现拆解HTML 适合存档阅读Word 适合交付CSV 适合分析4.1 HTML 导出最小模板、转义与时间线样式HTML 是所有格式里最适合“永久保存”的理由很简单浏览器天然支持不需要安装任何软件往后十年用任何设备打开都能正常渲染。HTML 导出的实现核心不是花哨样式而是准确的文档头和内容转义。import html as html_lib PAGE_HEADER !doctype html html langzh-cn head meta charsetutf-8 title{title}/title style body {{ max-width: 820px; margin: 0 auto; padding: 24px; font-family: sans-serif; }} .msg {{ padding: 10px 0; border-bottom: 1px solid #eee; }} .time {{ color: #888; font-size: 12px; margin-right: 12px; }} .sender {{ font-weight: bold; margin-right: 12px; }} /style /head body h1{title}/h1 def export_html(rows, title, out_path): parts [PAGE_HEADER.format(titlehtml_lib.escape(title))] for r in rows: sender html_lib.escape(r[sender]) content html_lib.escape(str(r[content])) parts.append( fdiv classmsg fspan classtime{r[time]}/span fspan classsender{sender}/span fdiv classcontent{content}/div f/div ) parts.append(/body/html) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(parts))这段模板结构上是完整的 HTML 文档DOCTYPE、lang、charset 都写全了。为什么要强调这三项因为很多工具导出的 HTML 缺 DOCTYPE 或 charset浏览器会用默认编码猜中文一旦猜错就是满屏乱码存档价值直接归零。转义用html.escape处理这个不能省。聊天内容里出现、、是常态比如“我发了个 标签给你”不转义会被浏览器当成真实标签渲染轻则样式错乱重则整个页面结构崩掉。时间、发送人、内容分别放在不同元素里是为了后续做样式定制和检索时能按类目操作。4.2 Word 导出用 python-docx 生成可打印文档Word 导出的价值在交付场景打官司留证、给同事交接、给长辈打印一份看得见的聊天记录。用 python-docx 生成 .docx 的难点不在生成而在控制中文默认字体和内容层级。from docx import Document from docx.shared import Pt, RGBColor from docx.oxml.ns import qn def export_word(rows, out_path): doc Document() # 设置 Normal 样式的中文字体避免乱码 normal doc.styles[Normal] normal.font.name Calibri normal.element.rPr.rFonts.set(qn(w:eastAsia), 宋体) doc.add_heading(微信聊天记录导出, level0) for r in rows: p doc.add_paragraph() time_run p.add_run(r[time]) time_run.font.size Pt(9) time_run.font.color.rgb RGBColor(0x88, 0x88, 0x88) sender_run p.add_run( r[sender] ) sender_run.font.size Pt(11) sender_run.bold True content_run p.add_run(r[content]) content_run.font.size Pt(11) doc.save(out_path)python-docx 默认的 Normal 样式字体只设置西文字体中文字体走w:eastAsia属性不设置的话在部分 Windows 机器上打开会是回退字体观感很差。所以第 6 行通过qn(w:eastAsia)显式指定宋体是必不可少的一步。时间用 9 磅灰色、发送人加粗、正文 11 磅这个层级关系让阅读者一眼分清时间轴和内容主体。单段单条消息的结构也让 Word 的“查找”功能能够精准定位到某条聊天记录长文档交付时检索体验会好很多。如果导出的量很大几十万条消息写进一个 Word 文档会让文件变得臃肿打开也慢。常见做法是改为按月份拆分多个文档文件名带上月份后缀同时在首页生成一个目录索引段。4.3 CSV 导出Excel 直接打开不乱码的两个细节CSV 面向的是数据分析场景想统计聊天频率、按关键词筛选、做词频分析CSV 是最干净的中间格式。两个细节会直接决定 Excel 打开后是否乱码。import csv def export_csv(rows, out_path): with open(out_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([时间, 发送人, 类型, 内容]) for r in rows: writer.writerow([ r[time], r[sender], r[type], str(r[content]).replace(\n, ), ])第一个细节是encodingutf-8-sig。普通 UTF-8 不带 BOMExcel 打开时默认按 ANSI 解码中文文字直接变乱码utf-8-sig会写入 BOM 头Excel 能自动识别为 UTF-8。如果你从来没有导出过 CSV 给 Excel 用户几乎必然在这一步踩坑。第二个细节是newline。csv 模块在 Windows 平台上如果不显式指定空字符串作为换行符写入的每一行后面会多一个空行导出的 CSV 行数翻倍Excel 打开后出现大量空白行。这个问题只影响 Windows所以不少 Linux 上开发的人第一次在 Windows 跑通 CSV 导出时都会被它坑一次。content里如果包含真正的换行建议在导出时替换成空格。CSV 规范允许字段内换行但 Excel 对多行单元格的支持经常造成列错位提前替换掉省心很多。5. 排障与避坑解密失败、乱码、时间错位、附件丢失——5 个常见坑5.1 解密失败SQLCipher 报错或乱码先核对密钥派生方式现象pysqlcipher3 打开加密库时报file is not a database或Decryption failed偶尔能执行但查出来的表是空的。原因密钥派生方式和数据库版本对不上。老版本用 IMEI uin 的 MD5 前 7 位新版本改成进程内存动态密钥。尝试直接用旧方案打开新库十有八九失败。另一个隐藏原因是 IMEI 获取不准双卡手机取了副卡 IMEI、或者模拟器没有真实 IMEI都会让拼接源错误。解决先确认微信大版本再用PRAGMA cipher_use_hmac OFF和ON各试一次。仍然解密失败就不要再猜了换 Windows 端登录微信、从内存中提取密钥的路线这是当前微信版本下最稳定的通用方案。5.2 导出乱码字符集一路打通到 UTF-8 的坑现象HTML 在浏览器打开正常但脚本在终端里打印出的聊天内容乱码或者反过来终端正常但写入文件乱码。原因终端尤其 Windows 命令提示符和旧版 PowerShell默认编码是 GBKPython 标准输出按 UTF-8 写终端按 GBK 解码中文自然乱码。这不是微信数据的问题是运行环境的编码闭环没打通。解决脚本入口加一行sys.stdout.reconfigure(encodingutf-8, errorsreplace)强制标准输出走 UTF-8。文件写入侧统一用encodingutf-8不要混用 GBK 和其他编码。测试方法很简单print 一条中文如果终端正常再进行后续调试。5.3 时间错位 8 小时createTime 的毫秒与本地时区现象导出后时间显示比实际时间早 8 小时或者直接显示 1970-01-01 07:xx:xx。原因两个错误叠加。第一微信createTime是毫秒时间戳直接传给datetime.fromtimestamp不除 1000得到的年份偏差巨大第二Python 的fromtimestamp返回本地时区时间如果脚本所在机器时区不是东八区显示时间就会偏移。解决统一用datetime.fromtimestamp(ts / 1000)并显式声明本地时区from datetime import datetime, timezone, timedelta tz_cst timezone(timedelta(hours8)) def ts_to_str(ts_ms): return datetime.fromtimestamp(ts_ms / 1000, tztz_cst).strftime(%Y-%m-%d %H:%M:%S)时间偏移 8 小时的坑非常隐蔽因为显示上不是乱码数据看起来也有模有样不仔细对表根本发现不了。导出完成后随机抽 1 条消息和微信客户端原始时间对比是最快的验证方式。5.4 CSV 打开乱码Excel 的编码怪癖现象CSV 用记事本打开正常用 Excel 打开全是乱码。原因Excel 打开 CSV 时依赖 BOM 判断编码没有 BOM 就按系统 ANSI 码页处理。简体中文 Windows 默认 GBK读 UTF-8 无 BOM 文件时中文解析错误。解决写入时用encodingutf-8-sig让文件自带 BOM。如果你必须交付给老版本 Excel或者目标机器是繁体中文系统也可以选择写入 GBK 编码但 GBK 会丢掉部分特殊符号优先推荐 utf-8-sig。这个坑不会有任何报错提示只有肉眼能发现所以导出后一定要亲自用 Excel 打开一次验证。5.5 附件找不到图片、语音、视频在数据库里的路径规律现象数据库里内容字段显示文件名或 XML 路径导出后对应目录下找不到文件。原因图片、语音、视频以独立文件存在MicroMsg目录下的FileStorage等附件文件夹里数据库只记录相对路径和索引。用户清理微信缓存、卸载重装、或者长时间未登录被服务端清理附件文件就没了但数据库记录还在。解决导出时遍历附件路径做存在性检查把缺失文件单独输出一份missing_attachments.txt清单而不是在导出文档里留下大量死链。归档动作要趁早附件完整度取决于本地文件是否还在这一点没有后悔药可吃。提示附件存在性检查在隐私和存证场景下很重要。如果这份导出要作为证据提交缺失附件清单反而能说明导出过程的完整性和真实性不要隐藏缺失项。6. 让导出结果真正可检索可交付增量导出与 HTML 检索页6.1 增量导出记住 createTime 游标全量导出只需要跑一次后续再跑就是浪费。增量导出的思路是把上次导出的最后一条消息时间存到本地文件下次只查比它更新的记录。touch last_time.txt echo 1767225600000 last_time.txt脚本侧在fetch_chat里把max_time换成从文件读出的值导出完成后把最后一条记录的原始时间戳写回文件。这套游标机制可以把导出从一次性任务变成可定时执行的日常任务每次只处理新增聊天。6.2 给 HTML 加一个本地搜索页全量 HTML 归档往往有数万行靠浏览器 CtrlF 查找时如果内容很长滚动定位会很吃力。我的习惯是在导出的 HTML 文件旁边生成一个纯静态的检索页用最基础的 JavaScript 做关键词过滤不依赖任何网络资源。!doctype html html langzh-cn head meta charsetutf-8 title聊天记录检索/title script function search() { var kw document.getElementById(kw).value; var items document.querySelectorAll(.msg); for (var i 0; i items.length; i) { items[i].style.display items[i].textContent.indexOf(kw) 0 ? : none; } } /script /head body input idkw oninputsearch() placeholder输入关键词过滤 /body /html逻辑很简单每次输入触发过滤按.msg节点文本里是否包含关键词来决定显示或隐藏。配合完整 HTML 归档文件这个检索页能让百万条消息的归档也能在秒级内完成关键词定位。对长期保存来说这一页的价值不亚于导出本身。6.3 导出前自动校验行数与样本导出脚本加上校验逻辑可以避免“跑完才发现漏了一部分数据”的尴尬。校验分两部分数量校验和样本校验。def verify_export(rows, db_count, out_path): assert len(rows) db_count, f导出数量 {len(rows)} 不等于数据库记录数 {db_count} with open(out_path, r, encodingutf-8) as f: content f.read() assert content, 导出文件为空请检查上游查询环节 print(样本前 3 条) for r in rows[:3]: print(r[time], r[sender], r[content][:30])断言触发时立刻中断不让错误结果持续覆盖之前的归档。样本打印则用于人工快速确认时间、发送人、内容三个核心字段是否正常。我自己做聊天记录归档时吃过大亏——某次导出全程无报错但时间因时区问题整体偏移了 8 小时事后要重新生成整个归档文件。后来养成的习惯是每次导出都先打印一条样本和微信客户端对照确认无误才继续。这个习惯延续到今天希望帮到你。本文还有配套的精品资源点击获取