微信备份中的 WeChat Files/你的微信ID/FileStorage/Image 目录下,经常会出现大量 .dat 文件。它们不是标准图片,而是微信对聊天图片、表情包做的异或混淆缓存。本文围绕 Python 脚本实现,拆解 .dat 文件的异或密钥推导、Pillow 格式识别、argparse 批量转换和常见故障排查,目标是把这些缓存还原为 png、jpg 或 gif。整个过程不依赖微信客户端,适合聊天图片导出、数据归档和本地缓存分析。
一、原理:.dat 文件与异或密钥
微信 .dat 文件的做法,是把原始图片字节流逐字节与一个密钥做 XOR。异或具有 A XOR B = C、C XOR B = A 的对称性,因此同一密钥既能加密也能解密。关键在密钥推导:JPEG 文件头是 0xFF、0xD8,PNG 是 0x89、0x50、0x4E、0x47,GIF 是 0x47、0x49、0x46。脚本假设原图是 JPEG,取 .dat 文件第一个字节 first_byte,则 key = 0xFF ^ first_byte。这个密钥是按文件独立计算的,不是全局固定值。
解密后还需要判断图片真实格式。更稳妥的方式是用 Pillow 的 Image.open() 读取解密后的字节流,再用 img.verify() 做一次完整性检查,然后重新打开获取 img.format。若 Pillow 识别失败,可以回退到检查解密数据前几个字节:GIF 开头、JPEG 的 FF D8、PNG 的 89 50 4E 47。这样能兼顾准确性和健壮性。
二、环境与依赖
Python 3.6+,安装 Pillow:pip install Pillow。os/sys/argparse/pathlib 为内置库。建议先把几个 .dat 文件复制到测试目录,避免误操作原始备份。
核心函数 decode_dat_image(dat_file_path) 返回 (image_data, image_format),失败返回 (None, None)。读取文件用 rb,大小不足 2 字节直接跳过;计算密钥;bytearray 逐字节异或;Pillow 识别;异常时用文件头兜底。
- import io
- from PIL import Image
- def decode_dat_image(dat_file_path):
- try:
- with open(dat_file_path, 'rb') as f:
- dat_data = f.read()
- except IOError:
- print('错误:无法读取文件', dat_file_path)
- return None, None
- if len(dat_data) < 2:
- print('警告:文件过小,可能不是有效图片.dat', dat_file_path)
- return None, None
- first_byte = dat_data[0]
- xor_key = 0xFF ^ first_byte
- decoded_data = bytearray()
- for byte in dat_data:
- decoded_data.append(byte ^ xor_key)
- try:
- img = Image.open(io.BytesIO(decoded_data))
- img.verify()
- img = Image.open(io.BytesIO(decoded_data))
- image_format = img.format.lower()
- return decoded_data, image_format
- except Exception:
- if decoded_data[:3] == b'GIF':
- return decoded_data, 'gif'
- elif decoded_data[:2] == b'\xff\xd8':
- return decoded_data, 'jpeg'
- elif decoded_data[:4] == b'\x89PNG':
- return decoded_data, 'png'
- else:
- print('警告:无法识别解密后格式', dat_file_path)
- return None, None
复制代码
三、批量转换脚本
用 argparse 接收 input_dir、-o/--output_dir、-r/--recursive、-f/--format。format 可选 original、png、jpeg,默认 original。检查输入目录,创建输出目录,递归用 rglob('*.dat'),非递归用 glob('*.dat')。
- import argparse
- from pathlib import Path
- parser = argparse.ArgumentParser(description='批量解密微信图片.dat文件并转换为标准格式')
- parser.add_argument('input_dir', help='包含.dat文件的输入目录路径')
- parser.add_argument('-o', '--output_dir', default='decoded_images', help='输出目录,默认./decoded_images')
- parser.add_argument('-r', '--recursive', action='store_true', help='递归遍历子文件夹')
- parser.add_argument('-f', '--format', choices=['original', 'png', 'jpeg'], default='original', help='输出格式')
- args = parser.parse_args()
- input_path = Path(args.input_dir)
- output_path = Path(args.output_dir)
- if not input_path.exists() or not input_path.is_dir():
- print('输入目录不存在或不是目录')
- raise SystemExit(1)
- output_path.mkdir(parents=True, exist_ok=True)
- dat_files = list(input_path.rglob('*.dat')) if args.recursive else list(input_path.glob('*.dat'))
- print('找到', len(dat_files), '个.dat文件')
复制代码
单文件处理由 process_single_file 协调解密、格式确定、文件名冲突处理和保存。original 时直接 write 解密后的二进制,最快且无损;png/jpeg 统一转换时用 Pillow 打开再 save。JPEG 不支持透明通道,遇到 RGBA、LA、P 模式要先 convert('RGB'),否则保存可能报错。文件名用 Path.stem 去掉 .dat,冲突时追加 _1、_2。
- from pathlib import Path
- def process_single_file(dat_file_path, output_dir, output_format='original'):
- image_data, detected_format = decode_dat_image(dat_file_path)
- if image_data is None:
- print('[失败] 跳过', dat_file_path.name, ':无法解密或非图片文件')
- return False
- if output_format == 'original':
- final_format = detected_format
- file_ext = 'jpg' if final_format == 'jpeg' else final_format
- else:
- final_format = output_format
- file_ext = 'png' if output_format == 'png' else 'jpg'
- stem_name = dat_file_path.stem
- output_filename = stem_name + '.' + file_ext
- output_file_path = output_dir / output_filename
- counter = 1
- while output_file_path.exists():
- output_filename = stem_name + '_' + str(counter) + '.' + file_ext
- output_file_path = output_dir / output_filename
- counter += 1
- try:
- if output_format == 'original' and final_format in ['jpeg', 'jpg', 'png', 'gif']:
- with open(output_file_path, 'wb') as f:
- f.write(image_data)
- else:
- img = Image.open(io.BytesIO(image_data))
- if final_format.lower() == 'png':
- img.save(output_file_path, 'PNG')
- elif final_format.lower() in ['jpeg', 'jpg']:
- if img.mode in ('RGBA', 'LA', 'P'):
- img = img.convert('RGB')
- img.save(output_file_path, 'JPEG', quality=95)
- else:
- img.save(output_file_path)
- print('[成功]', dat_file_path.name, '->', output_file_path.name, '(' + final_format + ')')
- return True
- except Exception as e:
- print('[错误] 保存失败', output_file_path.name, e)
- return False
复制代码
使用示例:- python wechat_dat_decoder.py "D:/WeChat Files/YourWeChatID/FileStorage/Image/2024-10"
- python wechat_dat_decoder.py "D:/WeChat Files/YourWeChatID/FileStorage/Image" -r
- python wechat_dat_decoder.py "D:/dat_files" -o "D:/output_pics" -f png -r
复制代码
运行后会看到类似日志:在目标目录中找到 150 个 .dat 文件,逐个输出成功或失败信息,最后统计成功数量并给出输出目录。
四、常见问题排查
1. 解密后图片打不开:先核对 key = 0xFF ^ first_byte;打印原始头2字节、计算出的密钥、解密后头2字节;解密后 JPEG 应为 ff d8,PNG 应为 89 50 4e 47。若不对,可能 .dat 不是图片缓存,或源文件已损坏。FileStorage 下还有其他 .dat 缓存,Pillow 识别失败后跳过是正常设计。
2. 大量文件处理速度慢:original 直接写二进制最快;解密循环是纯 Python,对几 KB 到几 MB 图片可接受。需要极致速度可用 NumPy 向量化 XOR:np.frombuffer、np.bitwise_xor、tobytes,但会增加依赖;也可用 multiprocessing 多进程,但要注意输出文件冲突。
3. 输出全是 JPEG 但有些应是 PNG:检查 decode_dat_image 返回的 detected_format,检查 process_single_file 中 final_format 是否被错误覆盖,检查是否误设 -f jpeg。Pillow 识别通常比文件头兜底更可靠,可临时注释兜底逻辑验证。
4. 只处理 PNG/GIF:在保存前加过滤,if final_format not in ['png','gif']: return False;或扩展 argparse 增加 --filter-format 参数,让用户指定格式列表。
五、健壮性与扩展
用 logging 替换 print,设置级别和格式,便于记录成功、失败、跳过原因。若要保留微信原始日期目录,可在 process_single_file 中计算 relative_path = dat_file_path.relative_to(input_path).parent,创建 output_dir/relative_path 后再写文件,避免所有图片扁平化。处理其他类型 .dat 时不要直接套用本密钥,先确认文件头特征和加密方式。
本文脚本适合从备份中批量还原微信图片缓存,也可作为理解 XOR、文件头识别、Pillow 图像处理和 CLI 脚本设计的练习。处理前备份原始数据,注意隐私合规。 |