资讯动态

Python解密微信SQLite库:从内存提取密钥到导出聊天记录

发布时间:2026/9/20 17:52:18 来源:尧图企业网站定制
简介一套基于Python的微信聊天记录提取与分析系统设计源码面向需要备份社交数据、复盘聊天互动的个人用户也适合具备Python基础、想做数据分析与可视化的开发者。系统实现聊天记录的提取、导出与统计可生成HTML、Word、CSV等格式的文档永久保存并输出年度聊天报告帮助用户回顾与微信好友的互动情况。压缩包共256个文件大小约48.05MB以103个py源码模块为核心配套18个html页面、61个png与43个svg图表素材、json/yml配置、pyd封装模块及辅助可执行工具从模板渲染、词云图表到数据接口均有覆盖qss样式、ico图标与gitignore等工程文件进一步提升了项目完整性。目前已有1870人学习下载。整套源码可直接运行体验也可用作课程设计或数据分析项目的参考能直观学习聊天记录的解析思路与可视化呈现流程。1. 微信聊天记录在加密SQLite库里解不了库后面都白搭很多人以为微信聊天记录是藏在某个文件夹里的 txt 或 json真打开 PC 版微信的WeChat Files目录看到的是一堆名字眼熟的.db文件。用 SQLite 工具直接打开会报file is not a database因为微信从很早的版本开始就把聊天记录存进 SQLCipher 加密的 SQLite 库库头是随机字节不是SQLite format 3。这套基于 Python 的分析源码完整覆盖“定位数据目录 → 从进程内存读密钥 → 解密库 → 提取 MSG 表 → 导出 HTML/Word/CSV → 生成年度聊天报告”的链路解决了两个核心问题一是密钥怎么拿二是拿到加密库之后字段怎么解析。源码里有 PY、PNG、SVG、HTML、JSON、YML 等两百多个文件模板和静态资源分得很细适合想备份聊天记录、做个人年度报告或者研究 SQLCipher 读写和桌面应用数据取证的人。2. 定位微信数据目录并提取SQLCipher密钥拿到源码先别急着写 SQL第一步是把目标库文件找出来。微信 PC 版的数据目录不像一般软件写在固定的 ini 里它会以微信号或wxid_开头的 hash 目录作为用户根目录Msg子目录下才是数据库。我一般会在入口模块里加一个--scan参数本质上就是在做文件系统扫描。2.1 用Python扫一遍文档目录找出MSG.db和MicroMsg.db常见路径是文档/WeChat Files/wxid/Msg微信更新后有的版本会挪到文档/xwechat_files/...但用户目录的命名规律还在。与其让用户手点“设置-文件管理”不如写一段搜索代码import os from pathlib import Path def locate_msg_db(): candidates [] home Path.home() / Documents for root, dirs, files in os.walk(home): # 只挑真正带聊天记录的库避免扫到备份或临时文件 for name in files: if name in (MSG.db, MicroMsg.db): full os.path.join(root, name) candidates.append((os.path.getmtime(full), full)) candidates.sort(reverseTrue, keylambda x: x[0]) return candidatesMSG.db存着消息正文MicroMsg.db存联系人、会话和群信息两个库都需要。按修改时间排序是为了对付“微信数据目录下有以前版本聊天记录”的情况微信升级后可能在旁边生成新目录旧库文件的时间戳更老优先用最新的否则报告会统计到很久之前且不再递增的陈旧数据。os.path.getmtime返回浮点时间戳所以排序用reverseTrue没问题如果你在 ubuntu 桌面版上跑扫描起点要换成~/.local/share或微信数据目录配置但文件名和排序逻辑完全一致。走完这一步你会拿到一组绝对路径下面要用它们去匹配进程内存中的密钥。2.2 用ctypes读微信进程内存把SQLCipher密钥抓出来微信的 SQLCipher 密钥不是写在配置文件里而是每次登录后算好放在进程内存中。想复制出来常见做法是遍历微信进程的私有内存页搜索用户身份特征串再在其附近找长度 32 的随机字节串。这个思路在 Windows 上依赖ReadProcessMemoryPython 里可以直接调 Win32 API。内存读取模块我通常独立放一个文件核心流程如下import ctypes from ctypes import wintypes PROCESS_QUERY_INFORMATION 0x0400 PROCESS_VM_READ 0x0010 MEM_COMMIT 0x1000 kernel32 ctypes.WinDLL(kernel32, use_last_errorTrue) def get_key(pid: int): handle kernel32.OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid) if not handle: raise ctypes.WinError(ctypes.get_last_error()) # 这里省略 MEMORY_BASIC_INFORMATION 结构体定义 mbi MBI() address 0 while kernel32.VirtualQueryEx(handle, ctypes.c_void_p(address), ctypes.byref(mbi), ctypes.sizeof(mbi)): if mbi.State MEM_COMMIT and mbi.Protect 0x04: # PAGE_READWRITE chunk ctypes.create_string_buffer(mbi.RegionSize) read ctypes.c_size_t() kernel32.ReadProcessMemory(handle, mbi.BaseAddress, chunk, mbi.RegionSize, ctypes.byref(read)) data chunk.raw pos data.find(bwxid_) if pos ! -1: # 特征串后面往往就是密钥所在区域多取一段再过滤 return data[pos6:pos38] address mbi.BaseAddress mbi.RegionSize return None这里有两个关键参数要注意。OpenProcess的第一个标志组合必须是PROCESS_QUERY_INFORMATION | PROCESS_VM_READ只读不注入安全性还可以接受只给PROCESS_VM_READ会拿不到进程详情导致VirtualQueryEx返回 0。MBI结构体在 32 位和 64 位进程下大小不同源码工程里已经做了两种分支直接复用即可。多开微信时这个函数的pid要把多个WeChat.exe都过一遍脚本里用psutil.process_iter([name])过滤进程名再逐一尝试直到某次搜索能在内存中命中微信号特征串。2.3 用pysqlcipher3连库先看一眼sqlite_master拿到密钥后连接加密库要选对驱动。pysqlcipher3是 SQLCipher 的 Python 绑定Windows 上装起来最容易的是pip install sqlcipher3-binary前提是你本机的 python 环境已经装好且 pip 可用。连接代码如下from pysqlcipher3 import dbapi2 as sqlite3 conn sqlite3.connect(MSG.db) conn.execute(PRAGMA keythe-32-byte-key) conn.execute(PRAGMA cipher_migrate) cursor conn.execute( SELECT name FROM sqlite_master WHERE typetable LIMIT 20 ) print(cursor.fetchall())cipher_migrate用于兼容旧版页大小和 KDF 参数如果你手上是较早的微信版本不执行这一句PRAGMA key之后查sqlite_master仍会报file is not a database。如果执行后能正常列出表名说明密钥、加载器和 SQLCipher 版本三者匹配读不出表优先怀疑密钥取错或者同一个wxid目录下存在两个长相相似的库文件。连接成功后把MicroMsg.db也用同样的方式打开后面提取联系人字段会用到。3. 从MSG表到CSV/HTML/Word聊天记录导出三件套解密只是开始真正决定交付质量的是字段映射和导出格式。源码里home.html、template.html、index.html这些模板不是随便堆的它们对应导出报告的不同入口christmas.html是主题模板charts.html和wordcloud.html分别给图表和词云用。3.1 先搞清MSG和Contact表的字段语义微信库表结构随版本有调整但核心字段相对稳定。以最常见的结构为例表字段说明MSGmsgId本库内自增主键MSGcreateTime消息时间UTC 秒级时间戳MSGisSender0 对方发送1 自己发送MSGstrContent文本内容或多媒体 XML 描述MSGtalkerId对应 Contact 表中的联系人标识ContactuserName微信 id群聊以 chatroom 结尾ContactnickName好友昵称Contactremark好友备注导出时优先用备注createTime是 UTC 秒直接用datetime.fromtimestamp会得到你本地时区的时间strContent在图片消息里是一段 XML文本消息才是纯文本解析时要做类型判断。isSender字段在群聊里代表“这条消息是不是我发的”要区分具体说话人得配合MSG表里另一个 sender 字段这套导出逻辑在单聊时按talkerId isSender分组在群聊时按sender分组两者不能混用。Contact表的remark经常为空所以模板里要写remark or nickName这样的兜底表达式。3.2 流式导出CSV别一次性把全量记录load进列表导出 CSV 最忌讳把所有行一次性fetchall()进内存一个微信号几年下来能有几十万条消息内存直接被打满。用fetchmany分批发更稳import sqlite3, csv conn sqlite3.connect(decrypted.db) conn.row_factory sqlite3.Row cur conn.cursor() cur.execute( SELECT strftime(%Y-%m-%d %H:%M:%S, createTime, unixepoch, localtime) AS time, isSender, strContent FROM MSG WHERE talkerId? ORDER BY createTime ASC , (target_talker,)) with open(chat.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([时间, 方向, 内容]) while True: rows cur.fetchmany(2000) if not rows: break for row in rows: direction 我 if row[isSender] 1 else 对方 writer.writerow([row[time], direction, row[strContent]])strftime的unixepoch修饰符要求输入是秒级时间戳配合localtime会先按 UTC 转换再偏移到本时区比在 Python 层逐行转换快得多。utf-8-sig会写入 BOMExcel 双击打开 CSV 才能正确识别中文否则表头和内容都会乱码。talkerId是查询条件实际使用中经常有人把userName当成talkerId传进去查出来 0 行两者在微信库里是两个不同字段要通过Contact表先做一次映射。3.3 用Jinja2渲染HTML再用python-docx出一份Word版HTML 导出用 Jinja2 渲染template.html把每条消息映射成左/右气泡。以 3.2 查出的记录为基础转换成模板需要的结构from jinja2 import Environment, FileSystemLoader from pathlib import Path env Environment(loaderFileSystemLoader(templates)) tpl env.get_template(template.html) messages [] for row in chat_rows: messages.append({ time: row[time], mine: row[isSender] 1, text: row[strContent], }) Path(output.html).write_text( tpl.render(chat_messagesmessages), encodingutf-8 )模板里用{% if msg.mine %}决定气泡靠左还是靠右不需要在后端拼字符串这是项目工程化的体现。template.html里引用的why.gif、病假.gif只是模板演示素材替换成你自己的图片不会影响渲染流程。导出完成后应该把templates目录下的静态资源一起复制到输出目录否则在另一台机器上打开 HTML 会丢样式。Word 版用python-docx循环写段落关键点有两个。第一是每条消息独立add_paragraph()方便排版第二是中文字体要显式设置否则到别人机器上显示成方块from docx import Document from docx.shared import Cm from docx.oxml.ns import qn doc Document() for msg in messages: p doc.add_paragraph() p.paragraph_format.left_indent Cm(0.5) run p.add_run(f{msg[time]} - {我 if msg[mine] else 对方}) run.font.name SimSun run._element.rPr.rFonts.set(qn(w:eastAsia), SimSun) doc.save(output.docx)w:eastAsia属性是给中文字体用的只设font.name只对西文字体生效。导出完成后用 Word/WPS 打开一眼就能看出字体有没有生效。CSV、HTML、Word 三种格式各有用途CSV 给后续分析程序读HTML 放在 NAS 上按日期翻阅Word 适合导出后直接发给别人。4. 年度聊天报告聚合统计、中文分词与图表渲染年度报告是这套源码最出效果的部分本质是把 MSG 表从明细数据变成统计视图。拆代码时看到charts.html和wordcloud.html是两个独立模板说明作者把图表和词云拆开渲染便于单独调样式。4.1 把聚合下推到SQLite别在Python里写for循环年度报告需要按年、月、日、小时统计消息条数。在 Python 里逐条 count 会非常慢把聚合交给 SQLite 执行更合理SELECT strftime(%Y-%m, createTime, unixepoch, localtime) AS month, COUNT(*) AS msg_count, SUM(CASE WHEN isSender1 THEN 1 ELSE 0 END) AS my_count, SUM(CASE WHEN isSender0 THEN 1 ELSE 0 END) AS friend_count FROM MSG WHERE createTime strftime(%s, 2024-01-01) AND createTime strftime(%s, 2025-01-01) GROUP BY month ORDER BY month;用COUNT(*)统计数据量SUM(CASE WHEN ... THEN 1 ELSE 0 END)代替 Python 侧判断结果集通常只有 12 行。strftime的月份格式%Y-%m可以直接排序ORDER BY month得到的是字符串排序恰好满足年月递增。除了月度统计还有几个常用维度统计维度SQL 片段输出年度总消息COUNT(*) FROM MSG WHERE ...数字最活跃联系人GROUP BY talkerId ORDER BY COUNT(*) DESC LIMIT 10Top10 列表小时热力strftime(%H, createTime, unixepoch, localtime)0-23 的数组我一般会把这段逻辑封装成build_annual_report(year2024)输入年份返回一个 dict方便前端直接消费。4.2 用jiebawordcloud生成年度词云聊天记录里大量是口语和表情符号直接放进 wordcloud 会把“嗯”“啊”“哈哈哈哈”也画得巨大。先做分词和停用词过滤import jieba from wordcloud import WordCloud stopwords set(嗯 啊 哈哈 哈哈哈 好的 知道 嗯嗯.split()) texts [row[strContent] for row in chat_rows if row[strContent]] words [ w for w in jieba.cut( .join(texts)) if w.strip() and w not in stopwords and len(w) 1 ] cloud WordCloud( font_pathC:/Windows/Fonts/msyh.ttc, width1200, height800, background_colorwhite, colormapcool, ).generate( .join(words)) cloud.to_file(wordcloud.png)font_path必须指向系统中文字体文件Windows 常见的是msyh.ttc微软雅黑ubuntu 桌面版可以填/usr/share/fonts/truetype/wqy/wqy-microhei.ttc。len(w) 1过滤单字可以把“我”“你”这种高频虚词挡在外面。词云效果很大程度取决于停用词表建议把“哈哈”这类词连同变体一起处理比如哈哈哈哈和哈哈哈要在切词前先规整成哈哈。WordCloud.generate会自动合并重复词但不会主动剥离 XML 标签所以这里的chat_rows必须经过前面的正则清洗。4.3 把统计结果推给ECharts让charts.html复用年度报告不止词云还要有月度趋势、小时活跃热力图、最常联系 Top10。Python 端聚合好数据后写 JSONimport json from pathlib import Path payload { year: 2024, monthly: [{month: r[month], count: r[msg_count]} for r in annual_rows], hourly: [{hour: r[hour], count: r[count]} for r in hourly_rows], top_contacts: top_contacts } Path(assets/report_data.json).write_text( json.dumps(payload, ensure_asciiFalse, indent2), encodingutf-8 )前端charts.html用fetch(assets/report_data.json)拉数据初始化一个折线图和一个热力图。ensure_asciiFalse必须保留否则中文月份名会变成\uXXXX虽然 ECharts 也能显示但接口排查问题会变得很别扭。indent2是给调试用的生产环境可以去掉以减小体积。这里有个实用技巧把year设计成函数参数而不是常量build_annual_report(year...)允许传入任意年份。这样home.html上可以放一个年份下拉框切换年份时前端重新 fetch 对应 JSON就变成一份可交互的年度聊天报告了。做统计时还要注意时区统一所有聚合都基于localtime修饰符如果有一处忘了加年度最后一小时的记录会被算到下一年的 0 点边界数据会异常。5. 多开、旧版本残留、媒体文件导出验证时的三个拦截点到这一步报表基本成型但真正要交付给别人还有三个高频问题会在最后一公里把结果打回原形。5.1 多开微信时如何固定目标进程很多人的电脑上挂了两个微信psutil.process_iter([name])会匹配到多个WeChat.exe内存扫描时如果扫到了不常用的那个进程虽然也能取到密钥但导出的并不是你眼前那个窗口的聊天记录。最稳的方式是先通过窗口标题定位主窗口import win32gui def get_front_wechat_pid(): hwnd win32gui.FindWindow(None, 微信) if not hwnd: return None return win32gui.GetWindowThreadProcessId(hwnd)[1]这段代码用窗口标题“微信”精确锁定用户正在使用的主窗口拿到hwnd再取pid避免在多进程里盲目尝试。部分版本窗口标题可能是“微信(2)”可以用win32gui.EnumWindows枚举所有带“微信”的窗口再按是否可见排序取第一个可见窗口的 pid。5.2 旧数据目录的选择微信数据目录下有以前版本聊天记录时扫描会找到多个MSG.db。只看文件修改时间不可靠因为旧库可能在系统盘迁移时被统一 touch 过。更可靠的验证方式是分别打开两个库执行SELECT MAX(createTime), COUNT(*) FROM MSG;选时间戳更大的那个库。如果某个库的MAX(createTime)落后当前日期三个月以上基本可以判定是残留目录导出分析时直接跳过。这种方法还能发现“导出的记录比微信界面多了/少了”的问题对比COUNT(*)和微信内置的搜索数量能快速判断字段过滤条件是否写错。5.3 用ffmpeg验证语音和视频是否导出完整项目里的ffmpeg.exe不是摆设微信媒体文件目录中的.silk语音需要转码。导出的 HTML 里引用语音后用下面命令验证转码结果ffprobe -v error -show_entries formatformat_name,duration -show_streams output.mp3format_name应该变成mp3duration大于 0 才算成功。遇到坏文件时看错误输出里的Invalid data用-fflags genpts重新生成时间戳再转一次能解决大部分音画不同步问题。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价