☰
微信聊天记录导出实战:解密SQLite生成HTML/Word/年度报告
2026/10/2 13:44:51 网站建设 项目流程

简介:资源聚焦微信聊天记录的本地提取、格式转换与年度数据分析,面向希望将聊天内容永久保存为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 密钥从哪来:imei+uin 组合与运行读取的常见做法

微信早期的数据库加密密钥由手机 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(f"PRAGMA 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(f"ATTACH 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", encoding="utf-8") as fin, \ open("chat_export.csv", "w", encoding="utf-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])

这里的几个参数是多年踩坑踩出来的。encoding="utf-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( f'<div class="msg {cls}">' f'<span class="time">{r["ts"]}</span>' f'<div class="bubble">{body}</div>' f'</div>' ) page = f"""<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <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", encoding="utf-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 默认生成的表格没有显式设置tcW,Word 的自动调整会干扰固定布局。

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(rows=1, cols=3) 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 节里写的encoding="utf-8-sig"。这个参数会给文件开头加上三个字节的 BOM 头,Excel 看到 BOM 就自动按 UTF-8 解析。顺带提醒,如果你之后要写 pandas 读取这份 CSV,记得也指定encoding="utf-8-sig",否则第一列名会带着\ufeff前缀,groupby 结果看起来人畜无害,实际上列名对不上。

5.3 现象:图片和语音导出来只剩一堆 XML 引用

消息 content 字段对于文本消息是纯文本,但对图片、语音、视频和文件消息,存的是 XML 格式的引用信息,类似<msg><img .../></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表会把微信公众账号、服务通知的文本消息也记进来,它们也是type=1,但不算「人的聊天」;群聊里talker是群 id,不是成员 id,按群分组的人数和你想的不一样;还有大量系统消息type=10000混在统计里。解决方式是统计前先构造一份排除名单,把公众号和服务通知的 talker 过滤掉,再把type=10000的消息剔除。验证时不要凭记忆,用某一天的具体时间和某一会话去微信客户端搜「按照日期」确认条数,这是唯一的可靠校准手段。

6. 生成年度聊天报告:先定指标,再让数据自己开口

导出完成只是第一步,这个标题真正的落点是「年度聊天报告」。我的经验是先定三个核心指标:总消息数、活跃天数、深夜聊天占比。这三个指标最能反映一段关系的真实温度,也最容易解释给不写代码的人听。总消息数和活跃天数用 pandas 分组聚合就能算出来:

import pandas as pd df = pd.read_csv("chat_export.csv", encoding="utf-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(ascending=False).head(10)) # 深夜消息占比(23点至次日5点) night = (df["小时"] >= 23) | (df["小时"] < 5) print(df.loc[night, "会话"].value_counts().head(10))

这份报告做出来后,我常用的验证方法是挑选一个具体日期,到微信客户端手动搜那一天的对话记录,核对条数和 TOP 联系人是否一致。确认无误后,把plain.db和chat_export.csv一起压缩归档,一份放在本地磁盘,一份放到云端网盘。这个双份归档的习惯帮我避免了至少两次「电脑换了数据全丢」的惨剧,也顺便保证了年度报告可以逐年累积对比。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询