微信PC端本地语音备份实战:从SQLite分库到SILK转MP3
发布时间:2026/9/18 9:11:34 作者:尧图编辑部 阅读量:1,286

很多人的微信聊天记录越攒越多想备份语音却只能看着聊天窗口里那个播放小圆点干瞪眼。实际上微信 PC 端的本地数据就摊在WeChat Files\wxid\Msg\Multi\这个 Multi 文件夹里消息内容被拆成多个 SQLite 分库语音消息则以 SILK 编码的 .dat 文件散落在旁边的 Voice 子目录中。这篇文章我要讲的就是完整的数据库解析链路——怎么从msg_*.db里筛出语音消息记录怎么匹配到对应的语音文件再把它转成 MP3。整套方案只用免费开源工具适合那些想彻底搞明白微信本地数据结构、又不想依赖在线解析服务的开发者。先说清楚边界以下所有操作只针对你自己设备、自己账号产生的数据分析时也请务必把微信退出后再复制文件不要在正在运行的微信目录上直接折腾否则很容易把数据库弄出不可逆的损坏。搞懂原理之后你会发现微信的本地语音其实就是一层很薄的格式包装真正的难点几乎全在“定位文件”这一步。1. 动手前先看清目录Multi 文件夹到底装了些什么1.1 微信数据目录怎么找微信 PC 版的本地数据默认放在当前用户的文档目录下面常见路径是C:\Users\用户名\Documents\WeChat Files。如果你当初安装时改过位置可以在微信客户端的“设置 - 文件管理”里直接看到当前的存储路径。进入WeChat Files后通常会看到All Users和若干个以wxid_xxx命名的目录那个wxid_xxx就是你的微信账号标识。每次登录不同账号微信都会在这里建一个独立目录账号之间互不干扰。我这次分析的完整目录结构大致长这样WeChat Files\ 9f2...fx7\ Msg\ Multi\ msg_0.db msg_1.db msg_2.db ... Voice\ 3f5a...dat 7b2d...dat Voice2\ ...不同版本会有一些差异有些版本还会把语音文件直接放在Multi下面或者多出Media、File、Video之类的子目录但Msg\Multi这个位置基本是稳定的。你只要把Multi整个文件夹复制到一台装了 Python 的电脑上做离线分析就行。1.2 Multi 文件夹的核心是分库数据库msg_0.db、msg_1.db这一系列文件本质上是同一个大数据库切成的小块专业叫法叫“分库”。微信之所以要拆是因为消息量一大单个 SQLite 文件体积会非常夸张查询和写入都会变慢。拆开后每张表的结构基本一致但数据按时间和会话分散在不同分片里。所以后面所有查询都不要只盯着msg_0.db必须遍历所有msg_*.db文件否则导出的语音列表一定不完整。我自己第一次做的时候就只查了msg_0.db结果导出量少了将近一半这个坑相当隐蔽。另外msg_*.db并不是某一个“语音数据库”它存的是所有类型的聊天消息。图片、视频、文本、系统提示、语音都混在同一个MSG表里靠Type字段区分。我们要导出语音核心就是把这堆混合消息里Type34的那部分挑出来。1.3 数据库到底能不能直接打开msg_*.db表面上是 SQLite 文件但微信不同版本的处理方式不一样。有的版本直接裸存 SQLite用命令行工具就能打开有的版本会做 SQLCipher 加密文件头是完全随机字节用普通 sqlite3 打开会直接报错。判断方法很简单在命令行里看文件头xxd msg_0.db | head如果前 16 字节是53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00也就是 “SQLite format 3”那就能直接打开。如果我朋友机器上那个版本出现了加密的情况建议先别急着折腾内存提 key优先考虑用微信自带的“备份与恢复”功能把聊天记录导到手机或其他设备再从备份目录里拿数据安全系数高很多。能直连的情况下命令行先摸个底sqlite3 msg_0.db .tables sqlite3 msg_0.db .schema MSG第二条命令输出可能非常长因为MSG表的字段少说二十来个。不用怕我们真正关心的只有其中几个字段。2. 从数据库里把语音消息记录筛出来2.1 MSG 表的关键字段MSG表是微信本地消息的总账本每条聊天记录占一行。对语音导出来说最常用的字段是这些localId本地自增 ID不跨库保证唯一定位记录。MsgSvrID服务器返回的消息 ID全局唯一后面匹配语音文件时有大用。Type消息类型语音固定是 34。IsSender0 表示别人发给我的1 表示我发出的。StrTalker对端 wxid群聊里存的是群 ID。CreateTimeUnix 时间戳单位是秒。StrContent文本内容对语音消息来说通常为空或只有部分 XML。BytesExtra二进制扩展字段语音文件路径、语音长度、hash 这些信息经常藏在这里。先按类型做个统计确认语音消息确实有SELECT Type, COUNT(*) AS cnt FROM MSG GROUP BY Type ORDER BY cnt DESC;正常你会看到Type34有一堆记录。如果一条都没有那说明这台电脑上的语音根本没下载到本地或者数据库版本字段结构完全不一样先检查前面的目录路径有没有找对。2.2 用 SQL 跑通第一个语音消息列表直接在msg_0.db里跑这样一条查询测试一下字段名对不对SELECT localId, MsgSvrID, IsSender, StrTalker, datetime(CreateTime, unixepoch, localtime) AS time_str, length(BytesExtra) AS extra_len FROM MSG WHERE Type 34 ORDER BY CreateTime DESC LIMIT 20;如果这条 SQL 能正常返回说明数据库结构是标准的。如果提示no such column: MsgSvrID或者no such column: BytesExtra说明你手上的版本字段名做了调整那就先执行.schema MSG把实际字段名抄出来再对应替换。注意datetime(...)那个函数是 SQLite 自带的转换出来的是本地时间方便人眼核对。实际批量导出时我更建议直接在 Python 里处理时间别在 SQL 层做太多格式化后面命名文件名可以更灵活。2.3 写 Python 遍历所有 msg_*.db单查一个分片肯定不够。我习惯把这些查询逻辑写成一个独立脚本放在复制出来的分析目录里对整个msg_*.db通配符做循环import sqlite3 import os import glob base_dir rD:\wechat_analysis all_rows [] for db_path in glob.glob(os.path.join(base_dir, msg_*.db)): conn sqlite3.connect(db_path) cur conn.cursor() try: cur.execute( SELECT localId, MsgSvrID, IsSender, StrTalker, CreateTime, BytesExtra FROM MSG WHERE Type 34 ) rows cur.fetchall() except Exception as e: print(f跳过 {db_path}: {e}) rows [] conn.close() all_rows.extend(rows) print(语音记录总数:, len(all_rows))这一步跑通之后你已经拿到了所有语音消息的“索引信息”。接下来最麻烦的问题来了怎么根据这些索引找到真正的 .dat 语音文件。3. 从数据库记录定位到真正的声音文件3.1 别指望 StrContent 里直接给文件路径很多人以为查到了语音消息StrContent里就会写着C:\path\to\voice.dat。实际并不是这样。微信语音消息的StrContent大多为空或者只写了一段没啥用的 XML真正的文件信息在BytesExtra里而且是一段二进制的序列化数据。不同版本里这段二进制可能是缩略的 protobuf也可能是自定义二进制结构。想纯靠手工解析字段不太现实我一般先用blackboxprotobuf这种通用库暴力识别一下import sqlite3 import binascii import blackboxprotobuf conn sqlite3.connect(rD:\wechat_analysis\msg_0.db) cur conn.execute(SELECT MsgSvrID, BytesExtra FROM MSG WHERE Type34 LIMIT 5) for msg_svr_id, extra in cur.fetchall(): if extra is None: continue try: message, typedef blackboxprotobuf.decode_message(bytes(extra)) print(msg_svr_id, message.keys()) except Exception: print(msg_svr_id, 解析失败) conn.close()blackboxprotobuf会把字节流里的字段按数字编号解析出来字段号 1、2、3 这类就是 protobuf 的原始字段编号。微信版本更新以后字段编号和类型经常变所以你不能把解析结果写死而是先打印出来看看里面有哪些看起来像 hash、路径、长度的值。我在某个版本的BytesExtra里看到过一个 32 位的十六进制字符串和 Voice 目录里的文件名正好对得上。这就是语音文件定位的关键线索。3.2 用文件名 hash 做最粗暴的匹配语音文件通常以.dat为后缀文件名是一长串字母数字长度多为 32 个字符看着像 MD5。对应关系最稳的方法就是把 Voice 目录下所有.dat文件的文件名全部建立一个索引然后用数据库里挖出来的 hash 去撞import os def build_voice_index(voice_dirs): index {} for d in voice_dirs: if not os.path.isdir(d): continue for root, _, files in os.walk(d): for f in files: stem os.path.splitext(f)[0] index[stem] os.path.join(root, f) return index voice_dirs [ rD:\wechat_analysis\Voice, rD:\wechat_analysis\Voice2, rD:\wechat_analysis\Msg\Multi\Voice, ] voice_index build_voice_index(voice_dirs) print(语音索引数量:, len(voice_index))然后对每一条语音消息把BytesExtra里所有长度为 32 的十六进制片段都提取出来去voice_index里验证import re hex_pattern re.compile(rb[0-9a-fA-F]{32}) def find_voice_file(extra, voice_index): if not extra: return None for match in hex_pattern.findall(bytes(extra)): key match.decode().lower() if key in voice_index: return voice_index[key] return None这个方法的逻辑很简单数据库里只要出现过和文件名一样的 hash基本就能认定是同一个语音文件。实测下来它能解决大部分版本的定位问题尤其是微信 3.7 和 3.9 这两个大版本。3.3 兜底方案按 MsgSvrID 猜文件名如果BytesExtra里挖不出名字还有一个笨办法直接拿MsgSvrID去文件系统里猜。因为微信有些版本给媒体文件命名时会直接用服务器消息 ID 做前缀或整体文件名。import os def guess_by_msg_svr_id(msg_svr_id, voice_dirs): candidates [ f{msg_svr_id}.dat, f{msg_svr_id}.silk, f{msg_svr_id}.amr, fvoice_{msg_svr_id}.dat, fmsg_{msg_svr_id}.dat, ] for d in voice_dirs: for root, _, files in os.walk(d): for name in candidates: p os.path.join(root, name) if os.path.exists(p): return p return None这个方案有点碰运气但在老版本上偶尔能用。排错顺序我建议是先走 hash 匹配再走 MsgSvrID 匹配两个都不行就把这条记录单独导出来人工看一下BytesExtra的十六进制内容重点找.dat、silk、voice这些 ASCII 字符串附近的数据。一旦文件定位成功后面的事情就简单多了。4. SILK 解码与格式转换三条路线任选4.1 先看文件头确认是不是 SILK拿到.dat文件后不要急着改后缀。微信语音文件虽然叫.dat但内容基本是 SILK v3 编码的音频流。用xxd看一眼文件开头xxd 3f5a...dat | head如果前面出现了#!SILK_V3这样的纯文本签名那么它就是一个标准的 SILK 文件只是文件名后缀被换成了.dat。微信 PC 端常见采样率是 24000Hz但这并不是绝对的后面解码时如果音调不对要记得换成 16000 试试。如果文件头不是#!SILK_V3而是一堆随机字节有可能文件做了按字节 XOR 混淆。这种情况我在老版本上遇过一次处理方法在后面“常见问题”里详细说。4.2 路线一Python 的 pilk 库直接解码现在 Python 生态里最省事的是pilk这个库它是 SILK v3 解码器的 Python 绑定安装后可以直接把.dat转成 PCM 或者 WAVpip install pilk解码一个文件只需要两行代码import pilk pilk.decode(rD:\wechat_analysis\3f5a...dat, rD:\wechat_analysis\3f5a.wav, sample_rate24000)pilk.decode的第三个参数指定采样率。如果声音出来后变快、变尖说明采样率给高了改成 16000 重新解码。如果文件报错说不是 SILK那就回到 4.1 检查文件头。这里要提醒一句SILK 编码是有损压缩解码成 WAV 后体积会膨胀不少。一分钟的语音WAV 可能有三四 MB而原始 SILK 可能只有几百 KB。所以后续打包成 MP3 时可以在音质和体积之间取个平衡。4.3 路线二silk-v3-decoder 命令行工具如果不想在 Python 环境里装依赖或者pilk在你机器上编译失败可以试试 SILK 官方源码翻出来的命令行版——silk-v3-decoder。下载源码后进入silk子目录编译git clone https://github.com/kn007/silk-v3-decoder cd silk-v3-decoder/silk make编译完会生成一个decoder之类的可执行文件用法大致是./decoder input.silk output.wav这个工具的缺点是比较挑平台在 Windows 上要么装 MSYS2要么用项目里附带的其他工具链在 Linux/macOS 上通常一次就能过。它输出的 WAV 可以直接喂给 ffmpeg 继续压 MP3。4.4 路线三ffmpeg 收尾压成 MP3不管用 pilk 还是 silk-v3-decoder拿到中间结果后最终压制 MP3 我都会交给 ffmpeg。如果上一步直接生成了 WAV命令行非常短ffmpeg -y -i 3f5a.wav -c:a libmp3lame -q:a 2 3f5a.mp3如果 pilk 只输出了裸 PCM 文件那就需要显式告诉 ffmpeg 采样率、位深和声道数。微信语音几乎都是单声道、16bit、PCM 小端序所以命令长这样ffmpeg -y -f s16le -ar 24000 -ac 1 -i 3f5a.pcm \ -c:a libmp3lame -q:a 2 3f5a.mp3-f s16le表示输入格式是 16bit 有符号小端 PCM-ar 24000是采样率-ac 1是单声道。这行命令里采样率一定要和 pilk 解码时用的一致否则压出来的 MP3 依然会是变调的。4.5 给批量导出写一个完整的处理流程单文件跑通后批量就水到渠成了。我一般会把整个过程串成一个脚本读取数据库筛选语音记录 → 定位 .dat 文件 → pilk 解码成 WAV → ffmpeg 压成 MP3 → 按时间重命名。核心循环如下import os import subprocess import pilk def convert_one(src_dat, out_mp3, sample_rate24000): wav_path out_mp3 .wav pilk.decode(src_dat, wav_path, sample_ratesample_rate) subprocess.run([ ffmpeg, -y, -i, wav_path, -c:a, libmp3lame, -q:a, 2, out_mp3 ], checkTrue) os.remove(wav_path)对每一行语音记录调用这个函数时建议在out_mp3文件名里带上CreateTime、StrTalker、IsSender比如20240512_193000_wxidabc_send.mp3 20240512_193100_wxiddef_recv.mp3这样导出几百个文件后依然能按时间轴和聊天对象快速找到想要的那条语音。5. 常见问题与排查技巧5.1 数据库打不开提示 file is not a database这是最常见的翻车点。先看文件头是不是SQLite format 3。如果不是多半是遇到了 SQLCipher 加密版本。加密数据库用普通 sqlite3 打不开需要对应的 key 才能解密。处理这类问题我的原则是优先用微信官方备份功能把聊天记录先备份到手机再恢复一次从备份产物里拿数据。不要一上来就去进程内存里翻数据库密钥那条路虽然可行但涉及客户端逆向风险高、版本依赖强而且很容易把安全问题放大。本篇文章只覆盖能直接打开的明文数据库场景。5.2 一条语音消息都筛不出来先确认Type字段是不是 34。不同版本偶尔会把语音 AppMsg 消息的 Type 记成 49子类型里才写 34。如果你发现语音消息一条都查不到把Type49的记录也拉出来看看它的StrContent里通常有一段voicemsg开头的 XML里面也会出现voicemd5这样的字段。5.3 文件头不是 #!SILK_V3全是乱码这是老版本微信一个很坑的细节部分语音.dat文件做过整文件异或混淆把每一个字节都和固定 key 异或了一遍。判断方法很简单读前 16 个字节对从 0 到 255 的每个单字节 key 逐一试看能不能还原出#!SILK_V3签名data open(voice.dat, rb).read() target b#!SILK_V3 for key in range(256): head bytes([b ^ key for b in data[:9]]) if head target: print(找到 XOR key:, hex(key)) restored bytes([b ^ key for b in data]) open(voice_restored.silk, wb).write(restored) break上面这段代码我实际救回过一批老语音文件。还原后再用前面的 pilk 方案解码即可。当然这个 key 不一定总是同一个值所以写成循环扫描最稳妥。5.4 解码后声音变调或者时长不对判断标准很简单人声变尖、语速变快就是采样率设高了人声变闷、语速变慢就是采样率设低了。微信 PC 端常见的是 24000但部分语音可能来自旧手机备份实际是 16000 甚至 8000。先拿单条语音多试几组采样率确定了再跑批量。5.5 匹配不到文件但数据库里明明有记录这种情况往往是语音消息没有在本地完整下载。微信为了省空间会对很久以前的语音做“过期清理”只保留数据库里的元信息不保留音频文件本体。另一个可能是当天语音还停留在“未下载”状态需要先在手机上点开播放一次再重新登录 PC 端同步。如果实在找不到还有一个歪门邪道去Msg目录下属的File、Image、Video等目录里全局搜.dat文件名因为语音文件可能被微信挪到别的缓存目录。全盘搜一次也没有那就是真没下载下来了。6. 我自己的几个实操习惯这套流程跑顺之后我把部分逻辑固定成了一个小工具每次微信提示存储空间不足时先把语音批量导出到 NAS再从手机上清理。整个过程我最在意的是三件事。第一永远只在复制出来的目录上做分析。微信客户端只要开着就会持续写数据库直接连到正在运行的msg_*.db上轻则查询结果不一致重则把数据库搞坏。现在我的操作顺序永远是“退出微信 - 复制整个 Multi 目录 - 在副本上跑脚本”。第二原始.dat文件先保留一份导出 MP3 之后不要急着删。MP3 是有损格式万一后面想转成更高码率或者做语音转写原始文件才是最可靠的材料。等确认整批语音都完好无误后再考虑清掉原始数据。第三命名信息要够全。一个小技巧是先把语音记录写入 CSV把MsgSvrID、CreateTime、StrTalker、源文件路径都保留下来然后再逐条转换。这样万一某条语音转换失败还能从 CSV 里反查重试不用把整个数据库再扫一遍。微信本地语音导出这件事技术上并不复杂真正的门槛在于不同版本之间的字段和存储差异。拿到一个新版本时不要急着跑批量脚本先用.schema MSG和.tables把结构看清楚再花两分钟确认一个语音文件的真实编码后面基本就是流水线操作了。希望这篇实战记录能帮你少踩几个坑顺利把聊天记录里的声音都搬回家。