☰
微信聊天记录导出实战:解密SQLite数据库并生成年度报告
2026/10/2 15:11:50 网站建设 项目流程

简介:面向需要长期保存、导出与分析微信聊天记录的用户和开发者,该资源提供了一套从备份提取、格式转换到年度报告生成的全流程方案,涵盖聊天记录迁移、解析工具选型、数据可视化与隐私安全等要点,既适合普通用户留存珍贵回忆,也方便技术人员进一步做数据挖掘和二次开发。压缩包共238个文件,约25MB,其中94个Python脚本用于解析备份与格式转换,17个HTML模板用于报告展示,61个PNG和36个SVG图表素材用于统计图形呈现;另含配置、说明文档及可执行程序,目录结构清晰,便于按需取用。已有646人学习下载。借助内置的解析与导出脚本,可将微信备份转为网页、Word、CSV等格式并保留图文信息;通过可视化模板可生成词云、活跃时段统计等年度报告,适合希望永久留存聊天内容、或进一步做微信数据分析和二次开发的读者参考。

1. 提取微信聊天记录并永久保存:先看清加密库,再谈导出和年度报告

很多人以为微信聊天记录只能靠滚动截屏或者第三方备份工具一条条导,真到自己想永久保存时才发现,微信 PC 端把全部聊天内容存在本机的一个加密 SQLite 数据库里,图片和语音也以加密的 dat 文件形式躺在磁盘上。做提取这件事,第一步不是写导出脚本,而是先把数据库解密、读懂表结构,然后才能把它变成 HTML、Word、CSV 三种文档,最后再基于同一份导出数据生成年度聊天报告。这套流程适合有大量重要对话需要归档的个人用户、想复盘自己一年沟通习惯的从业者,以及需要把聊天数据交接给非技术人员的场景。难点不在“导出”这个动作,而在解密、字段映射、编码和增量维护这些看不见的坑上。

2. 先把加密库变成可读数据:库文件定位、解密与最小验证

2.1 数据库文件在哪:三个常见位置与文件命名

微信 PC 版不像手机端那样把数据存在私有沙箱里,它把数据写进了本机用户目录。常见做法是直接搜索两个关键目录,一个是资源文件目录,另一个是数据库所在目录。不同版本位置略有差异,但核心规律是看 “WeChat Files” 或 “xwechat_files” 这两个名字。

老版本的数据一般位于%APPDATA%\Tencent\WeChat\WeChat Files\下,每个微信号一个独立文件夹,里面是Msg、Image、FileStorage等子目录;新版本则在%APPDATA%\Tencent\WeChat\xwechat_files\下,按微信号拆成多个目录。真正存放聊天记录的数据库是MSG.db,这是一个几十 MB 到几个 GB 不等的 SQLite 文件,表结构里保存了消息类型、发送者、时间戳、内容等字段。辅助库还有MicroMsg.db(存联系人)、ChatMsg.db(部分版本存消息索引)等。

定位数据库时,我一般不用文件管理器翻,直接开一个终端跑下面这段命令,把候选路径全部列出来:

dir /s /b "%APPDATA%\Tencent\WeChat\*MSG*.db" 2>nul dir /s /b "%APPDATA%\Tencent\WeChat\*MicroMsg*.db" 2>nul

第一行是查找主消息库,第二行是查找联系人库。如果这两个命令什么都没输出,说明微信版本较新,数据目录挪到了Documents下的 “WeChat Files” 或xwechat_files,可以把路径替换成%USERPROFILE%\Documents\WeChat Files再跑一遍。找到文件后先复制一份副本再操作,不要直接拿原始库去做解密和导出,因为后续脚本如果有误写操作,原始库一坏就再也没有后悔药了。

2.2 解密最小路径:拿到密钥、导出为可读 DB

加密库用的是 SQLCipher,直接拿 sqlite3 打开会报 “file is not a database”。SQLCipher 需要密钥才能打开,而密钥不在任何配置文件中,它由微信在启动时生成并保存在内存里。所以常见做法是:让微信保持登录状态,用工具从运行中的微信进程里读取密钥,然后用这个密钥把加密库导出成非加密库。

这里给出一套不依赖任何图形界面、命令行可复现的最小路径。前提是你已经安装了 Python 3 和 sqlcipher 命令行工具,并且微信 PC 端正在运行:

# 1. 从内存/运行进程中读取密钥,写入 key.txt # 常见工具会把密钥以 hex 形式输出 wechat_key_dumper > key.txt # 2. 用 sqlcipher 解开加密库并导出为明文库 sqlcipher "MSG.db" \ "PRAGMA key='x''hex';" \ "PRAGMA cipher_migrate;" \ "ATTACH DATABASE 'MSG_plain.db' AS plaintext KEY '';" \ "SELECT sqlcipher_export('plaintext');" \ "DETACH DATABASE plaintext;" # 3. 验证明文库可读 sqlite3 MSG_plain.db "SELECT COUNT(*) FROM MSG;"

第二步里的PRAGMA key是从key.txt读出来的密钥,注意 SQLCipher 4 的 hex 密钥写法必须是x'...'这种格式,少了x前缀会直接报 key 错误。PRAGMA cipher_migrate是为了兼容老版本微信留下的旧加密规格,如果密钥正确但打开后乱码,多半是少了这一步。ATTACH ... AS plaintext和sqlcipher_export的作用是把整个库“另存”成一个无密钥的新库,之后就能用普通 sqlite3 工具或 Python 直接读取了。最后一步的COUNT(*)是验证手段,如果返回几万到几十万行的数字,说明库和解密流程都通了。

2.3 导出的第一道验收:表结构、行数与消息字段

拿到MSG_plain.db后先别急着写导出脚本,先去确认表结构和字段名。微信的库结构在不同版本间有调整,字段名并不完全固定,盲目按网上旧脚本写死了字段名,会在某个版本上直接翻车。我先用 Python 快速看一遍表结构和样例数据:

import sqlite3 conn = sqlite3.connect("MSG_plain.db") cur = conn.cursor() # 列出所有表名 cur.execute("SELECT name FROM sqlite_master WHERE type='table';") for row in cur.fetchall(): print(row[0]) # 查看消息表的字段 cur.execute("PRAGMA table_info(MSG);") for desc in cur.fetchall(): print(desc[1], desc[2]) # 字段名, 字段类型 # 抽样 5 条消息确认内容字段和时间字段 cur.execute( "SELECT createTime, isSender, type, talker, content " "FROM MSG ORDER BY createTime DESC LIMIT 5;" ) for row in cur.fetchall(): print(row)

这段代码的作用是把数据库的黑匣子打开给观众看:字段名、类型、样例值一次全部露出。PRAGMA table_info(MSG)是关键,它告诉我们到底有没有isSender、createTime、talker、content这些字段,以及它们叫什么名字。微信部分版本把发送者字段命名为isSend,把时间字段命名为msgCreateTime,把会话 ID 命名为strTalker,如果脚本写死了旧名字,导出来全是空值。先跑这 3 步再写导出逻辑,能把后面所有的排查工作拦在起点。

3. 导出 HTML、Word、CSV:三套格式的落地脚本与取舍

3.1 导出 HTML:静态网页版聊天记录怎么做

HTML 是最灵活的导出格式,适合永久保存和长线阅读。我的目标不是生成一个花哨的动态网页,而是生成一个零依赖、双击就能打开的静态 HTML:搜索框过滤消息、按日期分块、左侧联系人列表、消息气泡式排版。只要一个文件,谁拿到都能看。

实现上我用 Python 从解密库里读消息,然后拼接 HTML 字符串。关键点有两个:一是所有样式内联或写进<style>,不要引用外部 CSS,保证单文件可迁移;二是图片、语音、文件路径统一用相对路径,导出目录结构固定为html/(网页文件) +files/(附件)。

import sqlite3, html, datetime conn = sqlite3.connect("MSG_plain.db") rows = conn.execute( "SELECT createTime, isSender, content FROM MSG " "WHERE type=1 ORDER BY createTime;" ).fetchall() blocks = [] for ts, is_sender, content in rows: dt = datetime.datetime.fromtimestamp(ts / 1000) cls = "me" if is_sender else "them" content = html.escape(str(content)) blocks.append( f'<div class="msg {cls}">' f'<span class="time">{dt:%Y-%m-%d %H:%M}</span>' f'<span class="text">{content}</span>' f"</div>" ) page = f"""<!doctype html> <html lang="zh-cn"><head><meta charset="utf-8"> <title>微信聊天记录导出</title> <style> body {{ max-width: 800px; margin: 40px auto; font-family: sans-serif; }} .msg {{ margin: 8px 0; padding: 8px; border-radius: 8px; }} .me {{ background: #d3f0d3; text-align: right; }} .them {{ background: #f2f2f2; }} .time {{ color: #999; font-size: 12px; margin-right: 8px; }} </style></head> <body> <h1>微信聊天记录导出</h1> {''.join(blocks)} </body></html>""" with open("chat.html", "w", encoding="utf-8") as f: f.write(page) print("HTML 生成完成:", len(rows), "条消息")

这段代码的核心是cls变量:发送方为自己时用绿色右对齐气泡,对方为灰色左对齐气泡,看存档时有非常强的可读性。html.escape必须加,否则消息里的<、>、&会把网页结构撑破。时间字段createTime是毫秒级时间戳,所以要用ts / 1000转成秒再格式化成可读时间。生成之后用浏览器打开chat.html,检查搜索、滚动、样式是否正常,图片路径后面再补充。

3.2 导出 Word:一张参数表把版式改明白

Word 导出的场景通常是给完全不懂技术的领导和家人看,他们只认 Word。用python-docx生成 Word 比 HTML 省事得多,但坑在样式:表格列宽不听话、中文字体丢失、长对话分页混乱。我一般直接用表格承载消息列表,三列分别是时间、发送方向、内容,然后固定列宽和字体,避免 Word 在不同机器上渲染失效。

from docx import Document from docx.shared import Pt, Cm import sqlite3 doc = Document() table = doc.add_table(rows=1, cols=3) table.style = "Table Grid" # 固定列宽:时间 3cm,方向 1.5cm,内容自适应 for row in table.rows: row.cells[0].width = Cm(3) row.cells[1].width = Cm(1.5) # 内容列不设宽,让 Word 默认撑开 conn = sqlite3.connect("MSG_plain.db") rows = conn.execute( "SELECT createTime, isSender, content FROM MSG " "WHERE type=1 ORDER BY createTime LIMIT 5000;" ).fetchall() for ts, is_sender, content in rows: cells = table.add_row().cells direction = "我" if is_sender else "对方" cells[0].text = datetime.datetime.fromtimestamp(ts / 1000).strftime("%Y-%m-%d %H:%M") cells[1].text = direction cells[2].text = str(content) doc.save("chat.docx")

LIMIT 5000是故意写的,因为几百 MB 的聊天记录一次性全塞进 Word 会导致文档卡死、打开缓慢,甚至 Office 直接提示修复。Word 更适合做“精选片段”或“最近一年”的导出,不可能也不应该替代 HTML 承担全量存档。列宽这里只设置了时间列和方向列,内容列让它自然自适应,这是血泪经验:如果你强行把三列都设成固定宽度,在 Windows 的 Word 里内容太长就会发生列宽无法拖动的现象,越调越乱。中文字体方面,python-docx默认的字体在某些系统上回退到宋体,这是可接受的,如果想更好看可以在doc.styles["Normal"].font.name = "微软雅黑"里显式指定。

3.3 导出 CSV:字段设计、字符编码与 Excel 打开不乱的细节

CSV 是给数据分析和程序化处理用的格式,不需要可读性,但需要干净的结构和被生态广泛支持。导出 CSV 时我推荐直接全量导出,不要加 LIMIT,因为 CSV 没有渲染压力,几十万行也毫无问题。字段设计上,至少要有时间戳、日期、发送方向、类型、会话ID、内容六列,这会直接决定后续年度聊天报告能不能顺利做。

import csv, sqlite3 conn = sqlite3.connect("MSG_plain.db") rows = conn.execute( "SELECT createTime, isSender, type, talker, content FROM MSG;" ).fetchall() with open("chat.csv", "w", encoding="utf-8-sig", newline="") as f: writer = csv.writer(f) writer.writerow(["createTime", "date", "isSender", "type", "talker", "content"]) for ts, is_sender, msg_type, talker, content in rows: date = datetime.datetime.fromtimestamp(ts / 1000).strftime("%Y-%m-%d %H:%M:%S") writer.writerow([ts, date, is_sender, msg_type, talker, content]) print("CSV 导出行数:", len(rows))

编码必须用utf-8-sig,这不是玄学,而是在 Windows 上用 Excel 直接打开 CSV 时,只有带 BOM 的 UTF-8 才能被正确识别为中文编码。如果用普通的utf-8,Excel 打开后中文全是乱码,虽然用记事本看没问题,但那已经不是 CSV 跨生态交换的本意了。newline=""是 Python csv 模块的标准要求,不加的话每行之间会多出空行。导完以后用 pandasread_csv读一遍校验字段数量,这就是后面年度报告的输入文件。

4. 从 CSV 到年度聊天报告:统计口径、词频与可视化输出

4.1 数据口径:一年报告统计哪些指标,怎么定

年度聊天报告不是把消息数据简单求和,而是定下一套可解释的口径,否则做出来的数字自相矛盾。我的默认指标是:总消息数、总字数、活跃天数、日均消息数、最长连续聊天天数、深夜聊天次数占比、消息最多的一天、Top10 联系人、高频词 Top20。这些指标看起来多,实际在 pandas 里只需要几条聚合语句。

口径上有一个容易混淆的点:总字数,是只统计type=1的文本消息,还是把图片、语音、文件都算进去?我建议文字报告只统计文本消息,但单独列一项“图片数量”和“语音数量”作为附属指标。理由是后续生成的词频依赖文本,图片数量可以反映沟通密度而非表达密度。时间维度上以本地时间为准,一天的定义是 00:00-23:59,跨年那天的时间戳要小心转换。

4.2 统计脚本:基于 pandas 的指标计算代码

CSV 已经导出,下一步直接用 pandas 读进来算:

import pandas as pd df = pd.read_csv("chat.csv") df["date"] = pd.to_datetime(df["date"]) df["hour"] = df["date"].dt.hour df["day"] = df["date"].dt.date # 基础指标 total_msgs = len(df) total_chars = df["content"].astype(str).str.len().sum() active_days = df["day"].nunique() avg_msgs_per_day = total_msgs / active_days # 深夜时段:23点-5点 night_mask = df["hour"].isin([23, 0, 1, 2, 3, 4, 5]) night_rate = night_mask.mean() # 最活跃的一天 busiest_day = df.groupby("day").size().idxmax() busiest_day_count = df.groupby("day").size().max() # Top10 联系人 top_talkers = ( df.groupby("talker") .agg(消息数=("content", "count"), 字数=("content", lambda s: s.astype(str).str.len().sum())) .sort_values("消息数", ascending=False) .head(10) ) print("总消息数:", total_msgs) print("总字数:", total_chars) print("活跃天数:", active_days) print("日均消息数:", avg_msgs_per_day) print("深夜消息占比:", f"{night_rate:.1%}") print("最活跃的一天:", busiest_day, busiest_day_count) print(top_talkers)

df["hour"] = df["date"].dt.hour是后面深夜统计的基础,isdin方式判深夜比写多个比较条件更清晰。talker字段在微信里是加密后的会话 ID 而非联系人名字,所以 Top10 出来是一串字母数字,这点要靠前面导出 CSV 时同时把MicroMsg.db里的联系人映射表也导出来,做一个talker -> 备注名的字典,报告才有人味。

4.3 报告怎么“年度化”:对比、趋势图的落地

单看一年总量没有说服力,年度报告的真正价值在于对比:今年和去年比,哪个月聊天最频繁,什么时候彻底断联。做对比的前提是至少有两年的 CSV 数据,所以我建议 CSV 文件名带上年份,例如chat_2023.csv、chat_2024.csv,而不是一个笼统的chat.csv。

import pandas as pd def load_year(path): df = pd.read_csv(path) df["date"] = pd.to_datetime(df["date"]) df["month"] = df["date"].dt.to_period("M") return df df_old = load_year("chat_2023.csv") df_new = load_year("chat_2024.csv") # 两年月度消息趋势对比 monthly_old = df_old.groupby("month").size() monthly_new = df_new.groupby("month").size() trend = pd.DataFrame({"2023": monthly_old, "2024": monthly_new}).fillna(0) print(trend) # 活跃时段对比 for name, df in [("2023", df_old), ("2024", df_new)]: hourly = df["date"].dt.hour.value_counts().sort_index() print(name, "凌晨3点消息数:", hourly.get(3, 0))

这段代码把年度对比落到按月趋势和凌晨活跃度两个维度。fillna(0)很关键,某个月完全没数据时 pandas 会产生 NaN,不填零画图和计算都会出问题。导出年度聊天报告时,我一般把 trends 转成 JSON 给 HTML 模板渲染,如果能接一个图表库,就画柱状图和折线图;不接图表库就输出一张 markdown 风格表格,放在 Word 里依然可读。

词频统计我用jieba分词之后直接collections.Counter取 Top20,注意要去掉停用词:

import jieba, re from collections import Counter def extract_keywords(content_list, topn=20): stopwords = {"嗯", "啊", "了", "的", "在", "是", "我", "你"} words = [] for text in content_list: for w in jieba.cut(str(text)): if len(w) >= 2 and w not in stopwords and re.search(r"[\u4e00-\u9fff]", w): words.append(w) counter = Counter(words) return counter.most_common(topn) keywords = extract_keywords(df["content"].tolist()) print(keywords)

len(w) >= 2把单字切出来的碎片全过滤掉,re.search只保留纯中文词汇,数字、URL、英文缩写在个人年度报告里基本都是噪音。把 keywords 输出成keywords.json或直接放进 HTML 模板的标签云,报告的可读性会立刻上一个台阶。

5. 全流程避坑与排查:解密失败、乱码、HTML 体积失控的现场记录

5.1 解密失败与密钥失效:两种高频报错的现场处置

现象:sqlcipher MSG.db之后,命令返回file is not a database或SqliteError: key,密钥看起来没错,但库就是打不开。

原因:这类报错通常有两种。第一种是 PC 微信升级到了 4.x,数据库加密方式整体调整,旧版工具读出的密钥格式不再是 64 位 hex,或者库文件本身被迁移到了新路径;第二种是微信重新登录后,内存中的密钥已经更换,而key.txt里存的还是旧进程的密钥。

解决:先确认微信进程是否正在登录状态,如果最近重新登录过,必须重新执行读取密钥的步骤,不能沿用旧文件。然后检查库文件的头部,用xxd MSG.db | head -n1看前 16 字节,是SQLite format 3\x00就是明文库,是乱码则确认加密库。对 4.x 的库,找支持新版本的工具而不是硬凑旧命令,同时考虑回退微信版本批量导出后,再升级回新版,这是最笨但最省时间的路径。

5.2 CSV 在 Excel 里“乱码”:不是编码错,是缺了 BOM

现象:chat.csv用记事本打开正常,用 Excel 直接打开变成一堆我开头的乱码。这是新手遇到最多的一个问题,而且容易被误判成数据损坏。

原因:Excel 在 Windows 区域设置下,默认用 ANSI(GBK)去解析无 BOM 的 UTF-8 文件,UTF-8 的中文字节被 GBK 逐字节解码,自然成了乱码。

解决:写 CSV 时用encoding="utf-8-sig",这个编码会自动写入 BOM 头EF BB BF,Excel 一旦看到 BOM 就会切到 UTF-8 解析。已经生成的乱码文件不用重导,用 Python 读一次再写一次就行:

with open("chat.csv", "r", encoding="utf-8") as f: text = f.read() with open("chat_fixed.csv", "w", encoding="utf-8-sig") as f: f.write(text)

5.3 HTML 导出后图片全挂:路径与 dat 文件解密的坑

现象:chat.html在浏览器里能打开,但聊天记录里的图片位置全是裂图,只显示一个小方块。

原因:微信把图片文件存成了加密的.dat格式,文件名通常是一个 MD5 再加.dat后缀,直接以相对路径引用到浏览器,浏览器无法解码加密内容,自然无法显示。

解决:导出 HTML 前,先遍历微信的Image目录,用 dat 文件查看器或简单脚本把.dat转成.jpg/.png。微信图片 dat 的解密算法很固定:取文件第一个字节和异或字节 0x06,算出偏移后逐字节异或即可还原。转换完再让 HTML 模板引用新生成的.jpg路径。另外注意相对路径,HTML 在html/下,图片在html/images/下,写成images/xxx.jpg不要写成/images/xxx.jpg,后者会被浏览器解析成磁盘根路径导致再次全挂。

5.4 年度报告的“跨年”统计错误:时区与消息时间戳的边界

现象:年度报告里 1 月 1 日凌晨的数据被算到了上一年,或者 12 月 31 日晚 23:59 的消息跑到第二年,导致月度趋势线在跨年点突然断崖。

原因:createTime是毫秒级 Unix 时间戳,默认是 UTC 时区,直接转本地时间时没有加时区偏移。在北京时间 UTC+8 的场景下,UTC 时间的 1 月 1 日 00:00 对应本地 1 月 1 日 08:00,如果只按 UTC 日期分组,就会出现早八点前消息归属错误。

解决:统一用 pandas 的带时区转换处理时间戳,而不是datetime.fromtimestamp:

df["date"] = pd.to_datetime(df["createTime"], unit="ms", utc=True) df["date"] = df["date"].dt.tz_convert("Asia/Shanghai") df["day"] = df["date"].dt.date

utc=True先明确时间戳的基准时区,tz_convert("Asia/Shanghai")再转成本地时间,最后dt.date取到的日期才正确。年度报告的所有分组统计都应基于这个处理后的date列,不要在原始createTime上直接fromtimestamp。

5.5 数据库越滚越大:压缩、备份与增量导出的收益边界

现象:MSG_plain.db导出一次后,过了半年再导出,发现明文库体积已经 2GB,每次解密导出耗时十几分钟,磁盘空间吃紧。

原因:微信数据库本身在膨胀,每导一次又生成一份全量明文副本,等于两倍体积;数据库文件在 SQLite 删除数据后不会自动收缩,长期更新导出的旧文件会越积越大。

解决:导出后立刻sqlite3 MSG_plain.db "VACUUM;"压缩一次,能释放大量空闲页。备份策略上不要每次全量导,每年 1 月导一次全量作为基底,之后每季度只增量导出“新增时间戳”范围的数据,用WHERE createTime > ?按最大时间戳切分。增量导出生成的 CSV 与往年 CSV 合并计算年度报告,逻辑完全一致,但耗时少 80%。数据库文件保留原始加密库,明文库导完即可删除,不要长期留存两份敏感数据。

6. 进阶:把导出做成每月一次的例行归档,顺手验证结果

到了这一步,完整流程已经能跑通,但我不建议你每次打开电脑再手动执行这五六步,那一定会被遗忘。我的做法是把整条链路封装成一个脚本入口,每月执行一次,并自动输出校验报告。

# 每天定时执行的核心步骤 python dump_keys.py # 从运行中的微信进程导出密钥 python decrypt_db.py # 解密 MSG.db 生成 MSG_plain.db python export_csv.py --year 2024 --output chat_2024.csv python export_html.py --input chat_2024.csv --output chat_2024.html python export_word.py --input chat_2024.csv --output chat_2024.docx --limit 3000

执行完之后写一个简单的验证函数,统计今天导出的行数和上个月对比,如果行数小于上月 10%,就打印警告,提醒我关注是否漏了某段会话。年度报告在 1 月 1 日自动生成,指标全部来自上一年合并后的 CSV,这样每年拿到报告时,所有数字都是稳定可复用的,不会因为工具重装而丢失历史口径。

这个流程我跑了两年,最大的感触是导出 HTML 的意义并不在于给所有人看,而在于给了自己一个随时可检索、可回看的时间胶囊;CSV 则保证了未来哪怕换任何分析工具,历史数据都是干净的、可迁移的。建议你先拿一个小号或聊天记录较少的号走一遍全流程,确认每一步的输出都能对上,再切到主力号做全量导出,过程中把每一版脚本都加上日期后缀,给未来的自己留条退路。希望这些经验对你有帮助,让你的聊天记录真正成为可长期留存、可回头分析的个人数据资产。

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

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

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

立即咨询