多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

微信.dat缓存图片恢复与清理工具:XOR异或原理与Python实现

微信.dat缓存图片恢复与清理工具:XOR异或原理与Python实现 先说一个我自己的经历。某天准备清理微信电脑版占用的几十个G空间打开文件管理目录发现里面除了聊天记录数据库还有一个叫 FileStorage 的文件夹点进去全是按照日期分的子目录再点进去好家伙清一色的.dat文件一张张图片预览根本打不开。当时第一反应是“缓存坏了”后来查了才知道这些就是微信收到的图片和表情只是被用一种很轻量的方式“搅乱”了字节顺序。这个项目就是围绕这件事做的写了一个小工具扫描 VX微信电脑端缓存目录下的.dat文件识别出真实图片格式并转成 jpg/png同时支持在确认转换成功后删除原始.dat文件把磁盘空间要回来。适合三类人一是想恢复旧聊天图片的普通用户二是想搞懂微信缓存文件原理的开发者三是天天被缓存膨胀困扰、需要定期清理的老哥。1. 这个项目到底是干嘛的把 VX 的 .dat 图片变成能看的图1.1 微信图片为何全是一堆 .dat微信电脑版收到图片后并不会直接以.jpg或.png的形式存在本地而是会统一保存成扩展名为.dat的文件。文件名通常是当天的时间戳或者一串无规律的哈希例如2024-03-18_093251.dat。这么做有它的考虑一方面微信不希望其他看图软件直接读取并散布用户聊天里的图片dat这种“陌生”扩展名能挡住八成误点另一方面这种变换对性能影响极小打开聊天窗口翻图片时才不会卡。所以.dat不是文件损坏也不是什么高深加密它就是原始图片数据经过逐字节异或XOR处理后的结果。明白了这一点处理思路就清晰了只要找到异或用的那个 key把字节还原回去文件就能变回正常图片。1.2 异或混淆看着像乱码其实是错一位字节异或运算的规则很简单两个二进制位相同结果为 0不同结果为 1。还原的原理更简单——对同一个值做两次异或就会回到原值。也就是说若原始图片字节是A微信用来混淆的 key 是K缓存里的字节是B A XOR K那么只要再执行一次B XOR K就能得到A。举个例子JPEG 图片开头三个字节固定是FF D8 FF。如果微信用的 key 是06那么缓存文件开头三个字节就变成了F9 DE F9。直接看十六进制你根本认不出这是 JPEG因为文件头完全变了。可一旦用06对每个字节做一次异或文件头立刻恢复成FF D8 FF图片就能正常打开了。这套机制特别像“给文件化了个妆”不是加密保险柜只是把特征藏起来。它没有密钥协商、没有算法复杂度靠的就是“大多数人不知道.dat是什么”这个信息差。对个人而言恢复这种文件不需要破解任何密码纯属格式还原。1.3 工具定位查看、转换、删除一体这个小工具的定位很明确三步走。第一步是扫描指定目录及其子目录下所有.dat文件列出文件名、大小、推测的图片格式和异或 key第二步是把每个可识别的.dat转换为正常的.jpg/.png/.gif文件第三步是在确认转换成功、输出文件非空的前提下再删除或者移到临时回收目录。为什么要把“删除”单独拎出来设计因为直接删缓存有风险。有些.dat文件本身就不完整可能是网络中断时留下的半截文件也可能是微信其他功能产生的非图片数据。无脑全删容易误伤。所以工具里的删除逻辑有个硬性前提只有成功识别格式并且转换出的图片文件大小大于零才允许动原始文件。这样既满足清理需求又把误删概率降到最低。2. 核心算法拆解怎么判断 .dat 的真实格式和密钥2.1 图片魔数Magic Number是关键入口所有标准图片格式的文件头都有固定的“魔数”相当于每种格式的防伪标识。转换.dat的第一步就是拿这些魔数去和.dat文件头做比对推算出 key。常见格式的魔数如下图片格式文件头十六进制常见扩展名JPEGFF D8 FF.jpg / .jpegPNG89 50 4E 47.pngGIF47 49 46 38.gifBMP42 4D.bmp微信聊天里九成以上的图片是 JPEG聊天表情和截图里 PNG 也不少GIF 主要用于动图BMP 少见但偶尔会出现。所以识别这四类基本够用后续想支持 WebP 可以再加原理一样。这里有一个重要认知魔数比对不是“猜一下 key 对不对”而是用多个字节交叉验证。只验证第一个字节容易撞车因为不同格式魔数的第一个字节可能差异不大验证前两到三个字节后误判率就低很多了。实践里我一般取前三个字节做校验JPEG 和 PNG 这类至少校验 3 字节GIF 校验 4 字节能稳定避开误判。2.2 密钥推导从第一个字节反推假设某个.dat文件开头三个字节是F9 DE F9。我先假设它是 JPEG那么原始文件头应该是FF D8 FF于是key 第一个字节异或 0xFF F9 XOR FF06验证第二个字节DE XOR 06应该等于D8确实成立验证第三个字节F9 XOR 06应该等于FF也确实成立三个字节全部对上说明这是 JPEGkey 就是0x06。反过来如果第一个假设不成立再假设它是 PNG用89去算 key然后验证第二字节是否等于50第三字节是否等于4E以此类推。判断逻辑写成伪代码就是枚举所有常见魔数用.dat文件首字节异或魔数首字节得到候选 key再用候选 key 去校验魔数中剩余字节。全部匹配就返回当前格式和 key。这种做法的好处是动态适配老版本微信固定 key 是06但新版本可能变化动态反推出来的 key 才是当前目录实际使用的 key不受版本限制。2.3 处理速度查表法比逐字节循环快真正处理大量文件时性能问题马上浮现。如果直接用 Python 写new_bytes bytes([b ^ key for b in raw])一张 5MB 的图片就要循环五百万次转换一百张图片能把人等急。这里有个非常实用的优化异或运算本质上是把 0 到 255 这 256 个字节值映射到另一组字节值这完全可以用一张 256 长度的查询表来替代。xor_table bytes(i ^ key for i in range(256)) new_bytes raw_data.translate(xor_table)bytes.translate()在 CPython 里是 C 层实现逐个字节查表的速度比 Python 层 for 循环快一个数量级以上。我之前测试过同样一批一万张图片逐字节循环跑了接近十分钟换成translate查表几十秒就完事。在处理动辄几万个缓存文件的场景里这步优化不是锦上添花是刚需。2.4 边界情况不是所有 .dat 都是图片还要泼一盆冷水目录里叫.dat的文件并不都是图片。有些是表情面板的缩略图有些是视频封面有些可能是后续版本引入的其他类型缓存。更麻烦的是某些文件本身已经损坏比如下载到一半被中断、磁盘写入异常这类文件即使 key 正确转换出来也是半个文件图片软件打不开。所以工具的识别函数必须“挑食”识别不了就跳过不强行输出转换出来以后还要检查输出文件是否为空。遇到大量无法识别的.dat先不要急着删挑几个用十六进制工具看看文件头确认是不是有新的格式类型或者微信更新了混淆方式。动态识别算法天然比硬编码 key 的方案抗版本升级这也是我坚持用魔数反推而不是写死0x06的原因。3. 实操写一个可直接运行的命令行工具3.1 找对微信数据目录动手之前最容易被卡住的一步是找目录。微信电脑版的数据目录在不同版本之间差别很大千万别死记一个路径到处套。老版本微信 3.x 一般在C:\Users\你的用户名\Documents\WeChat Files\wxid_xxx\FileStorage\Image\日期\微信 4.0 之后数据目录可能变成类似xwechat_files这样带新命名规则的路径里面的层级也不再是原来的结构。最靠谱的办法打开微信电脑版进入「设置 - 文件管理」点「打开文件夹」就会自动跳转到当前版本实际使用的数据目录然后从那里往上或往下找Image、FileStorage或类似名字的文件夹。我建议直接把整个微信数据目录作为扫描根目录交给工具自动遍历省得手动一层层翻。另外强调一个操作禁忌如果打算删除或移动文件最好先把微信退出。微信运行时会占用一批缓存文件的句柄有些文件处于锁定状态删除会失败强行跳过又会导致清理不完整。如果只是转换和查看微信开着一般也能读但稳妥起见还是全流程都关闭微信操作。3.2 完整代码实现下面是我整理好的可运行脚本核心功能包括遍历目录找.dat、识别格式和 key、批量转换、可选删除或移入回收目录。代码不长拿到就能用。#!/usr/bin/env python3 # -*- coding: utf-8 -*- vx_dat_tool.py 微信(WeChat) .dat 缓存图片查看/转换/清理工具 用法示例 python vx_dat_tool.py scan C:/Users/me/Documents/xwechat_files python vx_dat_tool.py export C:/Users/me/Documents/xwechat_files --out ./recovered python vx_dat_tool.py export C:/Users/me/Documents/xwechat_files --out ./recovered --delete-after-export python vx_dat_tool.py export C:/Users/me/Documents/xwechat_files --out ./recovered --trash ./trash import argparse import os import sys from pathlib import Path # 常见图片格式魔数表文件头 对应扩展名 MAGIC_FORMATS [ (b\xff\xd8\xff, jpg), (b\x89\x50\x4e\x47, png), (b\x47\x49\x46\x38, gif), (b\x42\x4d, bmp), ] def detect_dat_format(raw: bytes): 根据文件头魔数反推 XOR key 和真实图片格式 if len(raw) 4: return None, None for magic, ext in MAGIC_FORMATS: key raw[0] ^ magic[0] if all((raw[i] ^ key) magic[i] for i in range(len(magic))): return key, ext return None, None def find_dat_files(root: Path): 递归遍历目录产出所有 .dat 文件路径 for dirpath, _, filenames in os.walk(root): for name in filenames: if name.lower().endswith(.dat): yield Path(dirpath) / name def convert_single(dat_path: Path, out_dir: Path, delete_after: bool, trash_dir: Path): 转换单个 .dat 文件成功后按参数执行删除/移入回收站 raw dat_path.read_bytes() key, ext detect_dat_format(raw) if key is None: print(f[跳过] 无法识别的 dat 文件: {dat_path}) return False out_path out_dir / f{dat_path.stem}.{ext} # 关键优化256 字节查表 translate避免逐字节 Python 循环 xor_table bytes(i ^ key for i in range(256)) out_path.write_bytes(raw.translate(xor_table)) if out_path.stat().st_size 0: print(f[异常] 转换后为空文件: {dat_path}) out_path.unlink(missing_okTrue) return False print(f[转换] {dat_path.name} - {out_path.name} (key0x{key:02x}, {ext})) if delete_after: if trash_dir: target trash_dir / dat_path.name # 重名时加序号避免覆盖已有文件 idx 1 while target.exists(): target trash_dir / f{dat_path.stem}_{idx}{dat_path.suffix} idx 1 dat_path.rename(target) print(f[移入回收] {dat_path.name} - {target.name}) else: dat_path.unlink() print(f[删除] {dat_path.name}) return True def main(): parser argparse.ArgumentParser(description微信 .dat 缓存图片查看/转换/清理工具) parser.add_argument(action, choices[scan, export], helpscan只扫描统计, export批量转换) parser.add_argument(root, help微信数据目录或包含 dat 文件的目录) parser.add_argument(--out, default./recovered, help转换输出目录默认 ./recovered) parser.add_argument(--trash, default, help删除模式移动到指定目录而非直接删除) parser.add_argument(--delete-after-export, actionstore_true, help转换成功后删除/清理原始 dat 文件) args parser.parse_args() root Path(args.root) if not root.is_dir(): print(f目录不存在: {root}) sys.exit(1) out_dir Path(args.out) out_dir.mkdir(parentsTrue, exist_okTrue) trash_dir Path(args.trash) if args.trash else None if trash_dir: trash_dir.mkdir(parentsTrue, exist_okTrue) total 0 converted 0 skipped 0 for dat_path in find_dat_files(root): total 1 if args.action scan: raw dat_path.read_bytes() key, ext detect_dat_format(raw) if key is None: skipped 1 print(f[扫描] {dat_path} 未识别) else: converted 1 print(f[扫描] {dat_path} 可能是 .{ext} (key0x{key:02x})) else: if convert_single(dat_path, out_dir, args.delete_after_export, trash_dir): converted 1 else: skipped 1 print(f\n完成共 {total} 个 dat 文件成功 {converted} 个跳过 {skipped} 个。) if __name__ __main__: main()这段脚本有几个细节值得说明。missing_okTrue是 Python 3.8 之后才有老环境如果报错可以换成if out_path.exists(): out_path.unlink()。移动文件用rename比shutil.move更快但只能在同一磁盘分区内用跨盘符会报错如果你把输出目录和回收目录放在不同盘需要把rename换成shutil.move。3.3 典型用法与参数说明推荐的工作流分三步走。第一步只扫描不转换先摸清家底python vx_dat_tool.py scan C:/Users/me/Documents/xwechat_files扫描结果会列出每个.dat文件识别出的格式和 key。如果某个文件显示“未识别”先别管继续看后面。第二步批量导出到独立目录注意此时默认不删除任何原始文件python vx_dat_tool.py export C:/Users/me/Documents/xwechat_files --out ./recovered第三步才考虑清理。清理有两种模式。一种是“软删除”把所有已成功转换的.dat移动到回收目录相当于给自己留了后悔药python vx_dat_tool.py export C:/Users/me/Documents/xwechat_files --out ./recovered --delete-after-export --trash ./trash另一种是确认无误后直接硬删python vx_dat_tool.py export C:/Users/me/Documents/xwechat_files --out ./recovered --delete-after-export我个人的习惯是第一轮永远用--trash因为转换成功不代表图片内容正常——万一是花屏的半截文件至少原始文件还能找回来。等抽查完输出目录里的图片都能打开再手动清空回收目录不迟。3.4 删除前的安全检查与策略“支持删除”这四个字写起来容易做起来必须谨慎。我在工具里加了几个硬性安全阀。第一只有识别出格式、转换出非空文件的.dat才有资格被删除。识别失败的、读不了的、转换后为空的一律保留。第二建议按时间分批清理。微信目录下的图片按年月分文件夹清理时优先处理三个月前的缓存最近一周的图片保留因为聊天里很可能还要翻看。缓存一旦从本地删除如果微信服务器已经不再保留原图聊天记录里的图片就会显示成“文件已过期或已被清理”这个后果不可逆。第三删除前先看空间收益。有些.dat只有几 KB删一万个也腾不出多少地方真正占空间的是聊天记录数据库和视频缓存。建议先用扫描模式统计一下可识别图片的总大小值得清理再动手别做无用功。4. 真实使用记录我在这套流程里踩过的坑4.1 新版微信目录变了别死记老路径我最早一次写这套工具的时候网上教程都说路径是WeChat Files开头我照着找半天找不到最后发现在微信设置里点“打开文件夹”才是正确答案。微信 4.x 的目录已经变成了xwechat_files之类的新命名里面的结构也从FileStorage/Image变成了新的层级。所以在代码里我没有把路径写死而是让用户传入微信设置里打开的目录作为根目录再递归扫描。这样做的好处是不管微信以后怎么改目录结构只要它还在本地放.dat文件工具就能找到。4.2 转换出的图片打不开或花屏有次批量转换完抽查发现几张图片在电脑上打不开。排查过程是这样的先用十六进制工具打开原始.dat发现文件头是F9 DE F9理论上用 key06转换没问题但转换出来的文件末尾明显短了一截用图片查看器提示“文件损坏”。原因很快定位原文件本来就不完整是当年网络中断时下载了一半的残留文件。这类文件是“出生即残废”不是转换算法的锅。所以工具对这种情况的处理是尽力转换但通过文件大小校验标记异常而不是盲目报成功。遇到大量花屏先随机抽出几个原始文件看看大小分布如果普遍特别小说明这些本身就不是完整图片删除时更要谨慎。4.3 删除后聊天记录图片变成“文件已过期”删除缓存文件前一定要理解微信的数据策略。微信聊天记录里的图片本地会留一份缓存服务器上是否长期保存取决于多种情况有时候过段时间服务器就不再提供原图下载。本地缓存一旦删掉聊天窗口再点开那张图就真的只能看到“文件已过期”了。我的解决策略是对需要保留聊天记录的账号只清理六个月以上的缓存对已经不用的账号先完整转换归档再放心清理。清理不是目的把有价值的图片安全收回来才是目的。4.4 微信占用导致文件被锁第一次运行删除模式时部分文件删除失败报的是权限错误。原因就是微信还在后台运行几个缓存文件被进程占用。之后我每次清理前都先彻底退出微信包括托盘图标也要右键退出不能只关主窗口。另外数据目录位于系统盘时个别文件有只读属性Windows 下可能因为用户权限不足删不掉这时用管理员权限运行命令窗口即可解决。代码里对这些异常没有做太复杂的重试逻辑就是正常的 try/except 后跳过并打印原因因为人工介入一遍比自动处理十遍更靠谱。5. 扩展场景除了清理缓存还能拿来做什么5.1 恢复旧聊天图片这是这个工具最高价值的场景。很多人换了电脑、重装了系统但旧微信数据目录被完整备份过里面躺着几年的聊天图片。用这个脚本跑一遍所有历史图片都能被还原成可浏览的 jpg直接按原文件名归档。我曾经帮朋友从旧备份里恢复出几百张照片全是当年群聊里保存的合影和资料图微信界面里早就翻不到了但本地.dat文件还在奇迹就是这么发生的。5.2 从手机备份里提取图片微信手机端的图片缓存同样存在.dat类型的文件但 Android 和 iOS 备份文件里的情况比电脑端复杂安卓手机可能需要先通过数据线或备份工具把整个微信目录拷出来iOS 则需要从 iTunes 备份中解析出应用沙盒目录。而且手机端微信对图片的处理方式在不同版本间差异较大有的版本还会在文件头附加额外字节直接套用本文的算法不一定百分百识别成功。但核心思路仍然有效先判断文件头魔数再反推 key。把这套脚本作为第一版探测器识别出一批是一批剩下的用更精细的样本分析后续处理。这个方向我还在完善但基础能力已经能打。5.3 自动化定期清理电脑磁盘常年紧张的朋友可以把工具做成定时任务。Windows 的“任务计划程序”里新建一个任务每周运行一次 export 命令输出目录固定为归档目录回收目录固定为临时目录日志输出到一个文件。我建议把--trash目录放到和微信数据目录不同的磁盘分区这样即使微信目录所在盘已满回收目录还有地方暂存文件。配合脚本末尾打印的统计信息每周花一分钟看一眼转换了多少、跳过多少基本能掌握缓存增长速度及时调整保留策略。5.4 素材归档与取证做自媒体、做运营的同学经常需要从历史聊天里找回已发送的素材图。微信界面只能按聊天记录滚动翻找而本地缓存目录恰恰是全部图片的“底稿库”。转换归档后按日期目录整理搜索效率比在聊天记录里翻高太多。另一个场景是个人维权或证据保全聊天中收到的合同照片、转账截图如果还在本地缓存里即便过期无法翻看也能通过转换恢复出来。当然前提是你对数据拥有合法权利工具只是帮你把属于你自己的数据找回来。6. 最后说点实际操作中的体会这套流程前后改过好几版我最深的感触是微信选择 XOR 混淆而不是真正的加密说明它本质上只想“防君子”并不打算和用户较劲。对普通用户来说这不失为一件好事——自己的数据自己总有办法拿回来。再分享一个小技巧转换完的图片文件名保留了原始.dat的文件名而微信的dat文件名通常带有时间信息比如20240318_093251.dat。这为后续整理提供了很大方便按文件名排序就是按时间排序归档到相册时完全不需要再依赖任何数据库。如果文件名是纯哈希也可以通过目录路径里的月份信息辅助判断时间范围别轻易把目录层级打散。如果你只是清理缓存记住一句话先转换、再抽查、后删除永远给自己留一条退路。这套工具我到现在还在用每次微信更新版本我都会重新扫描一次确认识别率没有下降目测短期之内这种基于文件头反推 key 的思路都不会过时。
返回列表