微信聊天记录导出实战:解密SQLite生成HTML/Word/年度报告
简介资源聚焦微信聊天记录的本地提取、格式转换与年度数据分析面向希望将聊天内容永久保存为HTML、Word、CSV文档的普通用户以及需要参考自动化解析方案的微信开发者。包内共238个文件以Python脚本、HTML页面模板、PNG/SVG图表和JSON配置为主体另含样式表、图标、可执行程序等辅助文件压缩包整体约25MB完整覆盖数据解析、格式导出、图表统计和报告展示链路。项目参考WeChatMsg的解析思路支持将微信备份数据转换为多种文档格式并生成包含词云、活跃时段、消息频次等维度的年度聊天报告同时对图片、语音等非文本资源的处理方式也有涉及方便完整归档与后续分析。已有646人学习下载适合希望从零搭建聊天记录备份、导出与可视化分析流程并理解相关技术细节的实操型读者。1. 提取微信聊天记录导出只是表象难点在本地数据库的加密与还原把微信聊天记录导出成 HTML、Word、CSV 文档永久保存再基于这些数据生成年度聊天报告听起来像是个导出小功能真正动手才发现它是一条完整的「数据抢救」链路。微信把聊天记录锁在本地 SQLite 加密数据库中官方没有开放导出接口你在手机上看到的每一句话落到磁盘上都是密文。能做的只有三步把数据库取出来、把库解开、再按目标格式落地。这套方案尤其适合三类人想永久保存重要聊天记录的个人用户、被要求做数据归档的工程师、想做个人年度报告但不打算手工翻聊天记录的分析爱好者。下面按「记录存在哪→怎么取→怎么解密→怎么导出→哪些坑」的顺序展开每一步都给出可复现的命令、参数和失败时的排查思路。2. 微信聊天记录存在哪本地数据库结构与三种取数入口聊到微信记录导出第一步不是写代码而是先搞清楚数据是不是真的有本地副本。微信不像某些即时通讯软件那样把历史记录全部放在服务端它采用「端侧存储 同步」的混合策略手机端是主库电脑端是登录后同步出来的缓存库。两边的物理载体都是 SQLite 文件只是位置和加密状态不一样。搞清楚这一点后面的取数路径才选得对。2.1 EnMicroMsg.db 与 message 表先认识核心字段再谈导出微信 Android 端的聊天记录主库叫 EnMicroMsg.db位于/data/data/com.tencent.mm/MicroMsg/{32位hash}/目录下用的是 SQLCipher 加密的 SQLite 数据库。电脑端微信 4.x 的缓存库放在安装目录的xwechat_files或WeChat Files下不同版本目录名差很多但库内表结构大同小异。整个数据库里最核心的三张表是message存消息正文、rcontact存联系人资料、chat存会话摘要。做导出和年度报告90% 的字段都在message表里。动手之前我建议先打开库看一眼表结构和字段内容别急着写完整导出脚本。下面这条查询是检验库是否可用的「最小命令」SELECT createTime, type, isSend, talker, substr(content, 1, 60) AS preview FROM message WHERE talker wxid_xxx ORDER BY createTime DESC LIMIT 20;这段 SQL 的核心价值在于验证两件事第一createTime是不是 13 位毫秒时间戳第二content字段里到底是纯文本还是 XML。微信的message表里type字段是消息类型枚举1 是文本、3 是图片、34 是语音、43 是视频、49 是文件/链接/小程序、10000 是系统通知。isSend只有 0 和 1 两个值0 代表自己发出1 代表对方发出注意这个方向值很容易记反。talker存的是对方的 wxid 或群聊 id不是备注名备注名要到rcontact表里关联查找。我见过不少人第一步就翻车拿到库之后直接用文本编辑器打开 EnMicroMsg.db发现全是乱码于是怀疑文件损坏。实际上这是 SQLCipher 加密库的正常表现明文容量和表结构要等解密之后才能看到。所以务必先用上面的 SQL 验证你拿到的「库」到底是加密库还是已解密的明文库再决定走哪条解密路径。2.2 手机 root、PC 缓存、官方迁移三条取数路径怎么选取数路径直接决定整个项目的难度。根据我的经验微信聊天记录有且仅有三条稳定路径可走取数路径操作难度数据完整性适合场景Android root 后拉取 EnMicroMsg.db高最完整含全部历史手机就在手边、愿意折腾PC 微信 4.x 本地缓存库中只含已同步会话长期使用电脑端登录主聊记录在 PC官方「聊天记录备份与迁移」低完整但备份文件二次加密不想 root能接受备份文件再解析第一条路是传统方案手机 root 后用 Root Explorer 或 adb shell 把EnMicroMsg.db复制出来。这条路数据最全从第一句聊天到最后一句都在但 root 本身有门槛而且新版微信把数据库文件放在应用私有目录里普通文件管理器看不到。第二条路是 PC 微信 4.x 的本地缓存。电脑端登录时会把当前账号的相关会话同步到本地如果你长期用电脑版微信聊天那本地缓存库覆盖度相当可观。常见做法是打开微信客户端的设置查看文件保存路径然后去对应目录找数据库文件。这里有个容易混淆的点网上总有人搜「微信 dat 文件查看器」以为聊天记录是 .dat 文件实际上 .dat 只是微信用来混淆图片资源的文件后缀真正的聊天记录库是 SQLite 的 .db 文件两者完全不是一回事。第三条路是微信官方自带的「聊天记录备份与迁移」功能。它会把手机记录备份到电脑备份文件本身经过二次加密第三方脚本可以解析但格式不公开。除非前两条路都走不通否则我不太推荐这条解析成本高、字段还可能被截断。我的建议是有 root 条件就走第一条一次拷贝一劳永逸只有普通电脑端就用第二条配合 4.x 本地库路径提取两条都不行再考虑官方备份。整个流程只处理你自己账号的数据不要去碰别人账号的库文件。3. 从加密库到明文数据解密密钥与最小可跑链路拿到 .db 文件之后你面对的是一个 SQLCipher 加密库。SQLCipher 本质上是 SQLite 的一个加密分支每一页数据都被 AES 加密没有密钥连表结构都读不出来。所以这一章的核心是两件事拿到密钥以及用密钥把库导出成普通 SQLite 库。3.1 密钥从哪来imeiuin 组合与运行读取的常见做法微信早期的数据库加密密钥由手机 IMEI 和微信 UIN 拼接后做 MD5 得到取前 7 位社区里管这个叫「老算法」。公式大致是key md5(imei uin)[:7]。但这个算法在新版本里基本失效新客户端改用运行时动态生成的密钥再加上微信对文件目录权限收紧靠静态计算拿 key 越来越不现实。社区里目前稳定的做法是两类一类用调试器或注入脚本在微信进程运行时把内存里的密钥读出来另一类直接针对 PC 微信 4.x 的缓存库利用客户端落盘时的处理逻辑做绕过。这类工具体积小、更新快但版本兼容性参差不齐同一把密钥在不同版本微信上经常对不上。我一般建议把密钥提取当成一次性的「黑匣子操作」重点是拿到 key 之后立刻做明文库导出不要反复依赖在线读取工具。拿到密钥后解密这一步用 Python 的 pysqlcipher3 就能完成from pysqlcipher3 import dbapi2 as sqlite SRC EnMicroMsg.db DST plain.db KEY 你的密钥 conn sqlite.connect(SRC) conn.execute(fPRAGMA key{KEY}) conn.execute(PRAGMA cipher_memory_security OFF) # 老版本 SQLCipher 需要关闭此参数 # 校验密钥查询 sqlite_master 能返回就说明 key 正确 try: conn.execute(SELECT count(*) FROM sqlite_master) except Exception as e: print(密钥校验失败请检查 KEY 或数据库版本:, e) raise # 导出明文库 plain sqlite.connect(DST) conn.execute(fATTACH DATABASE {DST} AS plain KEY ) conn.execute(SELECT sqlcipher_export(plain)) conn.execute(DETACH DATABASE plain) print(明文库已导出:, DST)这个脚本有几个参数要注意。PRAGMA key的字符串拼接是特意保留的因为密钥通常是固定长度的十六进制串很少包含特殊字符直接拼接比参数绑定更直观。cipher_memory_security OFF是老版本 SQLCipher 库的兼容开关如果你用的是新版数据库而没开这个 PRAGMA某些字段读出来会是空白。sqlcipher_export是 SQLCipher 提供的整库导出函数它会逐表重建数据比单独VACUUM INTO更稳。密钥校验失败时不要急着换工具。先确认密钥源的账号是否与数据库一致再确认数据库文件是否有 .db-wal 或 .db-shm 伴随文件没有一起拷贝这两个文件里可能还存着未合并到主库的最近记录。把 wal 文件一并放到同目录再重新导出能解决不少「密钥明明对了却打不开」的玄学问题。3.2 先导出明文库和中间 CSV再逐层转换拿到明文库之后最忌讳的事是直接在上面写全套导出代码。明文库是后面所有工作的地基一旦在分析过程中把库改坏前功尽弃。我的习惯是先把它导出成一份不带任何格式的中间 CSV后续 HTML、Word、年度报告全部从这份 CSV 重建明文库只读不写。用 sqlite3 命令行工具做这一步比 Python 更快几万条消息的库眨眼就完成sqlite3 plain.db .headers on .mode csv \ SELECT createTime, type, isSend, talker, content FROM message; \ messages_raw.csv注意.mode csv会在内容包含逗号时自动加上双引号包含换行时会把换行嵌进引号字段里。这不是 bug是 CSV 标准做法。headers on让第一行输出字段名后面用 Python 的 DictReader 读取时可以直接按列名访问省去手工记索引的麻烦。我选择先把数据落到 CSV 而不是直接进 HTML/Word还有一层性能考虑。SQLite 查询在数据量大时会受content字段里的长文本影响而 CSV 是一份纯文本快照后面三个导出任务各自独立互相之间不会因为某次查询写错 SQL 而污染原始数据。消息量在十万级以下时这份中间 CSV 文件大小通常不超过 200MB日常处理毫无压力。到这里你实际上已经有了一份可搜索、可导入 MySQL 的干净数据。下一步就是把这份中间 CSV 分别加工成三种最终交付物。4. 导出成 CSV、HTML、Word三套最小脚本与关键参数三种格式定位完全不同CSV 是给机器和数据库用的存档格式HTML 是给人看的可离线浏览档案Word 是可以打印、可以放进档案柜的正式文档。所以三个导出脚本不能互相替代但它们都只做一件事从中间 CSV 读取再转成目标格式。4.1 CSV保留完整字段的存档格式与导出脚本导出 CSV 的核心不是把数据抄一遍而是洗干净字段。我写过一个固定套路先转时间戳、再处理换行、最后统一编码。下面这段脚本可以直接套用import csv import time def ts2str(ms): 13 位毫秒时间戳转为本地时间字符串 return time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(int(ms) / 1000)) with open(messages_raw.csv, r, encodingutf-8) as fin, \ open(chat_export.csv, w, encodingutf-8-sig, newline) as fout: reader csv.DictReader(fin) writer csv.writer(fout) writer.writerow([时间, 方向, 会话, 类型, 内容]) for row in reader: ts ts2str(row[createTime]) direction 发出 if row[isSend] 1 else 收到 content row[content].replace(\n, \\n) writer.writerow([ts, direction, row[talker], row[type], content])这里的几个参数是多年踩坑踩出来的。encodingutf-8-sig必须有否则在 Excel 里打开中文会乱码这个 BOM 头就是 Windows 生态的后悔药。newline是 Python csv 模块的标准要求不加的话每一行后面会多一个空行。content里的换行替换成\\n是为了防止 Excel 把一条消息拆成多行这是 CSV 导出的经典坑。字段顺序我特意固定成「时间、方向、会话、类型、内容」因为这份 CSV 之后要喂给年度报告脚本用方向字段挨着时间字段能让 pandas 的 groupby 少写几行代码。type字段保留原始数字而不转成「文本」「图片」等中文也是有意的年度统计时要按类型分组数字比中文更适合做 groupby 的 key。4.2 HTML生成可离线搜索的时间线归档HTML 导出是最有「永久保存」感的一种格式。浏览器直接打开零依赖搜索用浏览器自带功能就能全文检索。关键是不能把聊天内容直接拼进 HTML 文本里要用转义函数处理否则聊天记录里一个符号就能打崩整个页面。import html import time def build_html(records): cards [] for r in records: body html.escape(r[content]).replace(\n, br) cls me if r[isSend] 1 else other cards.append( fdiv classmsg {cls} fspan classtime{r[ts]}/span fdiv classbubble{body}/div f/div ) page f!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title微信聊天记录归档/title style body {{ max-width: 800px; margin: 2rem auto; padding: 0 16px; background: #f7f7f7; }} .msg {{ display: flex; margin-bottom: 12px; }} .msg.me {{ flex-direction: row-reverse; }} .bubble {{ max-width: 72%; padding: 10px 14px; border-radius: 12px; background: #fff; }} .msg.me .bubble {{ background: #95ec69; }} .time {{ font-size: 12px; color: #999; align-self: center; margin: 0 8px; }} /style /head body{.join(cards)}/body /html with open(chat.html, w, encodingutf-8) as f: f.write(page)html.escape是必须做的它把、、转成实体符号聊天内容里即使有人发了script也只会显示成文本。replace(\n, br)要在 escape 之后做顺序反了的话br会被二次转义。消息方向用 CSS 的flex-direction: row-reverse实现左右分栏比给每条消息额外拼 class 更干净。这份 HTML 文件做完之后是自包含的拷到 U 盘里插到任何电脑都能离线打开。如果想进一步做「年度聊天报告」的可视化版本可以在这个模板基础上把统计数字写进页面顶部再加个侧边栏导航按月份跳转。4.3 Word把对话整理成可打印的表格文档Word 导出的场景通常是「把某段重要对话交给别人看」或者存档打印。用 python-docx 生成三列表格是最稳的方案但这里有个高频翻车点表格列宽在 Word 里看着是对的一打印就超出页面而且手动拖动列宽拖动不了。原因是 python-docx 默认生成的表格没有显式设置tcWWord 的自动调整会干扰固定布局。from docx import Document from docx.shared import Cm from docx.enum.table import WD_TABLE_ALIGNMENT from docx.oxml.ns import qn from docx.oxml import OxmlElement doc Document() table doc.add_table(rows1, cols3) table.style Table Grid table.alignment WD_TABLE_ALIGNMENT.CENTER def set_col_width(cell, width_cm): 同时设置 cell.width 和底层 tcW二缺一都会导致 Word 列宽失效 cell.width Cm(width_cm) tc_pr cell._tc.get_or_add_tcPr() tc_w tc_pr.find(qn(w:tcW)) if tc_w is None: tc_w OxmlElement(w:tcW) tc_pr.append(tc_w) tc_w.set(qn(w:w), str(int(width_cm * 567))) # 1cm 567 twips tc_w.set(qn(w:type), dxa) cells table.rows[0].cells set_col_width(cells[0], 3) # 时间列 set_col_width(cells[1], 2) # 方向列 set_col_width(cells[2], 11) # 内容列 cells[0].text 时间 cells[1].text 方向 cells[2].text 内容set_col_width这个函数是 Word 表格布局的关键。只设cell.width不行python-docx 有些版本不会把它写进最终 XML只改tcW也不行部分 Word 打开后仍然自动调整。两个一起设才算锁死。567是厘米与 twips 的换算系数Word 内部用 twips 存储列宽不转这个数会得到异常小的列宽。表头设置好之后后面每一行数据用table.add_row()添加然后对每个新单元格调用同样的set_col_width。这个函数会一直重复粘贴所以写成工具函数而不是内联代码是值得的。另外如果对话很长建议按日期拆成多个表格每段对话之间插入一个 Heading 标题这样 Word 的导航窗格能直接看到每天的对话范围。别让一个 5000 条消息的聊天记录变成一张望不到头的表格打印出来也没法看。5. 微信聊天记录导出避坑指南5 个血泪现场解密、导出、统计这套流程跑下来真正耗时间的往往不是业务逻辑而是各种「看起来没问题但就是不对」的怪现象。以下五个问题是我自己和同行踩过最多、最典型的坑按现象到解决的顺序记录在这里。5.1 现象sqlite3 打开明文库报 file is not a database拿到一个 db 文件sqlite3打开后直接报错file is not a database。原因有两种要么密钥错误导致PRAGMA key实际没有生效要么这个文件根本就是加密库而你当明文库去读。我在 2.1 节提过EnMicroMsg.db 是 SQLCipher 加密库直接用原生 sqlite3 打开必然报这个错。解决方式是先跑解密脚本用密钥校验步骤确认 key 正确后导出 plain.db。如果校验通过但导出后仍报错检查cipher_memory_security是否已经设置为 OFF部分老库需要这个参数才能兼容。另一种常见情况是拷贝时漏掉了.db-wal文件导致最后几条记录缺失或数据库页损坏把伴随文件一起拷贝即可。5.2 现象CSV 在 Excel 里打开中文全部乱码这是 CSV 导出里最经典的翻车场景。代码在终端里 print 出来中文正常用 Python 读也没问题唯独 Excel 打开是乱码。原因是 Python 默认写出的 UTF-8 不带 BOM而 Windows 版 Excel 默认按 ANSI 解析 CSV。解决方式就是 4.1 节里写的encodingutf-8-sig。这个参数会给文件开头加上三个字节的 BOM 头Excel 看到 BOM 就自动按 UTF-8 解析。顺带提醒如果你之后要写 pandas 读取这份 CSV记得也指定encodingutf-8-sig否则第一列名会带着\ufeff前缀groupby 结果看起来人畜无害实际上列名对不上。5.3 现象图片和语音导出来只剩一堆 XML 引用消息 content 字段对于文本消息是纯文本但对图片、语音、视频和文件消息存的是 XML 格式的引用信息类似msgimg ...//msg。直接把这个 XML 写进导出文档看到的是一堆尖括号不是图片本身。解决办法是解析 XML按type字段判断消息类型把图片路径和文件名提取出来再按路径去微信的资源目录复制对应文件。图片和语音资源在 Android 端的实际存放位置是MicroMsg/{hash}/attachment目录文件名与 XML 里的索引对应。如果需要完整还原图文时间线导出 HTML 时要把图片文件的相对路径写进src属性图片文件本身复制到导出目录下的images文件夹里。5.4 现象Word 表格列宽拖不动、打印超出页面4.3 节的set_col_width就是为了解决这个。很多人导出 Word 时只设置了table.style和cell.width结果 Word 打开后列宽完全不受控拖拽也拖不动打印时内容列溢出页面。原因是 Word 表格的实际列宽由底层 XML 的tcW决定python-docx 的cell.width只是高层封装部分版本更新后不再写入底层 XML。解决方式就是用 OxmlElement 直接操作w:tcW并且把表格的autofit关掉确保 Word 不会按内容重新计算列宽。具体写法在 4.3 节已经给出置为固定值后表格在打印预览里不会超出页面宽度。5.5 现象年度统计数字和微信官方对不上生成年度聊天报告之后你一定想验证一下数字准不准。最坑的是按talker分组统计消息数结果比微信「聊天记录搜索」里显示的少了或多了不少。原因主要有三个message表会把微信公众账号、服务通知的文本消息也记进来它们也是type1但不算「人的聊天」群聊里talker是群 id不是成员 id按群分组的人数和你想的不一样还有大量系统消息type10000混在统计里。解决方式是统计前先构造一份排除名单把公众号和服务通知的 talker 过滤掉再把type10000的消息剔除。验证时不要凭记忆用某一天的具体时间和某一会话去微信客户端搜「按照日期」确认条数这是唯一的可靠校准手段。6. 生成年度聊天报告先定指标再让数据自己开口导出完成只是第一步这个标题真正的落点是「年度聊天报告」。我的经验是先定三个核心指标总消息数、活跃天数、深夜聊天占比。这三个指标最能反映一段关系的真实温度也最容易解释给不写代码的人听。总消息数和活跃天数用 pandas 分组聚合就能算出来import pandas as pd df pd.read_csv(chat_export.csv, encodingutf-8-sig) df[日期] df[时间].str[:10] df[小时] df[时间].str[11:13].astype(int) # 按会话按日统计消息数 daily df.groupby([会话, 日期]).size().reset_index(name消息数) print(daily.groupby(会话)[消息数].sum().sort_values(ascendingFalse).head(10)) # 深夜消息占比23点至次日5点 night (df[小时] 23) | (df[小时] 5) print(df.loc[night, 会话].value_counts().head(10))这份报告做出来后我常用的验证方法是挑选一个具体日期到微信客户端手动搜那一天的对话记录核对条数和 TOP 联系人是否一致。确认无误后把plain.db和chat_export.csv一起压缩归档一份放在本地磁盘一份放到云端网盘。这个双份归档的习惯帮我避免了至少两次「电脑换了数据全丢」的惨剧也顺便保证了年度报告可以逐年累积对比。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →