摘要
摘要:本文主要对PC版微信的图片存储加密方式进行了研究,通过探讨图片文件中文件头信息中保存的文件标识字符数据得过程,得出了解密还原为正常图片的方式。并采用UI与业务逻辑分离的模式设计了对非程序员用户友好的GUI界面和完整的解密逻辑设计。
关键词:Python; 解密; 微信; PyQt5;
1. 引言
在Windows版微信中,其会把用户收发的图片加密存储为dat文件,用户无法直接打开dat图片文件。而微信加密存储的dat文件本质是由普通图片文件(如jpg/bmp/png)格式中的二进制数据逐位与随机加密码进行异或运算得到的。计算出正确的加密数字,并对dat文件作异或运算即可还原为可正常打开的普通图片文件。本项目基于以上思路,使用Python语言进行解密逻辑程序设计,使用PyQt等模块,设计出对非程序员友好且便于操作的解密工具GUI程序。
2. 系统结构
该程序设计采用UI与业务逻辑分离的模式进行设计。因此实现系统结构介绍分为逻辑实现设计介绍和UI界面设计介绍。
2.1.系统总体设计
该程序设计采用开发平台为PyCharm,采用程序语言为Python。所使用的Python模块为sys模块、os模块、threading模块、PyQt模块。不同的Python模块分别用于实现不同的程序功能
表1 模块-功能对照表
模块名称 | 程序功能 |
sys, os模块 | 系统相关操作模块与文件、文件目录相关操作模块,用于实现相关文件操作。 |
threadin模块 | 多线程操作模块,实现UI界面与业务逻辑分离操作,并实现多线程解密操作,可能加快程序运行。 |
PyQt模块 | Qt库与Python融合的模块。用于程序主要UI设计,实现UI操作功能。 |
该程序设计的目的是做出对非程序员用户友好的且方便操作的界面,因此,在该系统中,GUI界面为主要模块,通过GUI界面相关操作调用业务模块实现功能。业务模块完成相关操作后,将输出结果返回给GUI模块进行展示。
图1 程序GUI界面
图2 程序模块关系
2.2.逻辑实现设计
在PC版微信图片缓存文件夹(一般为
“C:\Users\用户名\Documents\WeChat Files\微信号\FileStorage\Image\xxxx-xx“)中,存储大量的dat格式文件,这些为微信对聊天中收发到的图片进行二次加密处理后得到的文件。
图3 微信图片缓存文件夹中的dat文件
使用16进制的方式打开这些文件可以看到前两个字节为“0xDF, 0xF8”。与jpg图片格式头信息的“0xFF, 0xD8”不符。对图片字节数据直接修改的加密方式易于联想到“异或法加密”。
图4 以十六进制打开dat文件
用0xDF与0xFF做异或运算,0xF8与0xD8做异或运算。结果为0x20。对多个dat格式文件进行计算后,均得到相同结果。可得结论加密码为0x20。但在实际操作中,每个用户的加密码都不一样,需要在程序中计算出的加密码。
图5 异或运算结果
要解密dat文件,首先要知道dat文件在加密前的文件格式。图片的格式很多,一个图片文件的后缀名并不能说明这个图片的真正格式什么,可以通过读取图片文件的文件头标识得到。因为各种格式的图片的文件头标识不同的,因此可以通过判断文件头的标识来识别图片格式。
这里以Windows位图(bmp)格式作为参考例子:BMP(Bitmap-File)图形文件是Windows采用的图形文件格式,位图文件可看成由4个部分组成:位图文件头(bitmap-file header)、位图信息头(bitmap-information header)、彩色表(color table)和定义位图的字节阵列。 在这里,我们只关心其图像文件头信息即可,图像文件头信息结构如下表:
表2 bmp位图文件头信息
偏移量 | 域的名称 | 大小 | 内容 |
0000h | 文件标识 | 2 bytes | 识别位图的类型 |
0002h | File Size | 1 dword | 用字节表示的整个文件的大小 |
0006h | Reserved | 1 dword | 保留,必须设置为0 |
000Ah | Bitmap Date Offset | 1 dword | 从文件开始到位图数据开始之间的数据(bitmap data)之间的偏移量 |
000Eh | Bitmap Header Size | 1 dword | 位图信息头(Bitmap Info Header)的长度 |
0012h | Width | 1 dword | 位图的宽度 |
0016h | Height | 1 dword | 位图的高度 |
001Ah | Planes | 1 word
| 位图的位面数 |
从表2可以看到在图片格式头信息中,文件标识为信息的前两个字节。为了获得图片格式,这里只需关心文件标识,查阅资料可得其它格式的文件标识如表3所示。
表3 图片格式与头信息对照表
图片格式 | 头信息前两个字节(16进制) |
jpg | 0xff, 0xd8 |
bmp | 0x42, 0x4d |
png | 0x89, 0x50 |
gif | 0x47, 0x49 |
基于上述解密算法思路和信息,进行逻辑实现。步骤如下:
1. 读取dat文件前两个字节的数据。
2. 根据表3所列图片格式的头文件信息,逐个遍历,用其第一个字节与dat文件读取出来的第一个字节数据进行 异或运算。 得到加密码。
3. 使用加密码与dat文件第二个字节数据进行异或运算,得到校验码。
4. 校验码与表3所列图片格式的第二个字节进行校验。
5. 校验通过,则表示加密码正确,使用正确的加密码对整个dat文件的数据进行运算,获取正确的图片数据。
6. 将正确的图片数据写入新创建的文件。解密完成。
图6 解密流程图
2.3.UI界面设计
该程序中的UI设计部分使用PyQt中的designer工具进行设计。
图7 程序主界面
所用控件介绍:
1. label标签,用于提示用户输入
2. lineedit控件,用于获取用户输入文件夹路径
3. button按钮,用于实现用户点击调用功能
4. checkbox控件,用于实现功能选用
5. textedit控件,用于将内容输出展示给用户
6. processbar控件,用于展示解密任务处理进度
在UI实现中,主要采用信号与槽机制实现UI与功能交互。信号与槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。
在该程序中,为防止解密过程线程任务过于繁忙,导致UI界面无法进行相应。采用主线程运行UI界面,子线程运行解密程序的方式运行整个程序。
UI程序的运行逻辑程序框图如下:
图8 程序GUI逻辑流程图
3. 实现代码
该程序设计采用UI与业务逻辑分离的模式进行设计。因此实现代码分为UI代码和逻辑实现代码。
在代码中,注释已经对代码块进行充分的解释。
3.1.逻辑实现代码
import sys, os, threading
from wechatdecode_ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog, QMessageBox
from PyQt5.QtGui import QIcon
from time import time
import inspect
import ctypes
class WechatMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# 标识符属性
self.start_flag = False
self.finish_flag = False
# 统计用属性
self.end_num = 0
self.decode_code = 0
self.file_all_num = 0
self.file_done_num = 0
self.file_fail_num = 0
# 多线程列表
self.threads = []
# 统计时间变量
self.start_time = 0
self.end_time = 0
# 信号与槽链接
self.ui.pic_src_button.clicked.connect(self.onclick_src_button)
self.ui.pic_out_button.clicked.connect(self.onclick_out_button)
self.ui.start_button.clicked.connect(self.onclick_start_button)
self.ui.about_button.clicked.connect(self.show_about_dialog)
self.ui.open_out_button.clicked.connect(self.onclick_open_out_button)
self.ui.open_src_button.clicked.connect(self.onclick_open_src_button)
self.ui.close_button.clicked.connect(self.close)
# 控件初始化
self.ui.text_out_textEdit.append("微信加密图片默认存储于\nC:\\Users\\您的用户名\\Documents\\WeChat "
"Files\\您的微信号\\FileStorage\\Image\\20XX-XX\\")
self.ui.text_out_textEdit.append('\n若默认存储文件夹不存在,请查看\"微信程序->设置->文件管理\"中的设置')
# 重写关闭事件,用于关闭确认
def closeEvent(self, event):
if self.start_flag:
return_code = QMessageBox.warning(self, "确认关闭", '解密程序正在运行,是否确认要关闭?', QMessageBox.Yes | QMessageBox.No)
if return_code == QMessageBox.Yes:
for t in self.threads:
stop_thread(t)
event.accept()
else:
event.ignore()
# 槽
def show_about_dialog(self):
'''
用于展示关于窗口
:return:
'''
QMessageBox.about(self, "关于", '本软件适用于最新版PC微信 V2.9.5.33'
'\n理论上兼容以往版本'
'\n\n本软件仅供学习交流,如作他用所承受的法律责任一概与作者无关'
'\n\n该软件可能存在未知原因崩溃问题,请勿作正规用途'
'\n\n下载使用即代表您同意上述观点'
'\n\n版权 © 2020 Leon. 版权所有,翻录必究。'
'\nCopyright © 2020 Leon. All rights reserved.')
def onclick_open_out_button(self):
'''
用系统文件管理器打开路径
:return:
'''
path = self.ui.pic_out_lineEdit.text()
if not os.path.exists(path):
QMessageBox.critical(self, "文件夹打开错误", "请在输出目录处输入正确的文件夹路径")
else:
os.startfile(path)
def onclick_open_src_button(self):
'''
用系统文件管理器打开路径
:return:
'''
path = self.ui.pic_src_lineEdit.text()
if not os.path.exists(path):
QMessageBox.critical(self, "文件夹打开错误", "请在微信加密图片目录处输入正确的文件夹路径")
else:
os.startfile(path)
def run_time(self):
'''
用于实时数据输出到textEdit并更新processbar进度条
:return:
'''
if self.file_done_num < self.file_all_num:
self.ui.progressBar.setValue(self.file_done_num)
elif not self.finish_flag:
self.end_time = time()
self.finish_flag = True
self.start_flag = False
self.ui.progressBar.setValue(self.file_done_num)
self.ui.text_out_textEdit.append("解密完成,共解密 %d 个文件,成功 %d 个,失败 %d 个" % (
self.file_all_num, self.file_all_num - self.file_fail_num, self.file_fail_num))
self.ui.text_out_textEdit.append("用时 %d 秒" % (self.end_time - self.start_time))
self.ui.text_out_textEdit.moveCursor(self.ui.text_out_textEdit.textCursor().End)
if self.ui.done_after_open_checkbox.checkState() == 2:
# 完成后打开文件夹
path = self.ui.pic_out_lineEdit.text()
if not os.path.exists(path):
pass
else:
os.startfile(path)
# 重新初始化
self.ui.mul_thread_checkbox.setDisabled(False)
self.ui.pic_src_button.setDisabled(False)
self.ui.pic_out_button.setDisabled(False)
self.ui.pic_src_lineEdit.setDisabled(False)
self.ui.pic_out_lineEdit.setDisabled(False)
self.ui.start_button.setText("开始(&S)")
def onclick_src_button(self):
'''
浏览按钮打开文件浏览器,默认路径为微信文件夹
:return:
'''
user_path = os.path.expanduser('~')
wechat_path = user_path + '\\Documents\\WeChat Files'
choose_path = QFileDialog.getExistingDirectory(self, '选择PC版微信图片存储文件夹', wechat_path)
choose_path = choose_path.replace('/', '\\')
if choose_path == '':
choose_path = self.ui.pic_src_lineEdit.text()
self.ui.pic_src_lineEdit.setText(choose_path)
def onclick_out_button(self):
'''
浏览按钮打开文件浏览器,默认路径为图片文件夹
:return:
'''
user_path = os.path.expanduser('~')
output_path = user_path + '\\Pictures'
choose_path = QFileDialog.getExistingDirectory(self, '选择解密后图片存储文件夹', output_path)
choose_path = choose_path.replace('/', '\\')
if choose_path == '':
choose_path = self.ui.pic_out_lineEdit.text()
self.ui.pic_out_lineEdit.setText(choose_path)
# 开始
def onclick_start_button(self):
'''
开始按钮,执行读取文件目录文件、多线程调用等功能
:return:
'''
if not self.start_flag:
self.ui.text_out_textEdit.textChanged.connect(self.run_time)
self.start_time = 0
self.start_time = time()
dir_path = self.ui.pic_src_lineEdit.text()
save_path = self.ui.pic_out_lineEdit.text()
# 判断文件是否存在
if not os.path.exists(dir_path) or not os.path.exists(save_path):
QMessageBox.critical(self, "文件夹目录错误", "请设置正确的微信图片目录或图片保存目录")
else:
# 获取目录下文件列表
files_list = os.listdir(dir_path)
self.file_all_num = len(files_list)
# 初始化操作
self.file_done_num = 0
self.file_fail_num = 0
self.ui.progressBar.setValue(0)
self.ui.progressBar.setMaximum(self.file_all_num)
self.ui.text_out_textEdit.clear()
self.start_flag = True
self.finish_flag = False
self.ui.mul_thread_checkbox.setDisabled(True)
self.ui.pic_src_button.setDisabled(True)
self.ui.pic_out_button.setDisabled(True)
self.ui.pic_src_lineEdit.setDisabled(True)
self.ui.pic_out_lineEdit.setDisabled(True)
self.ui.start_button.setText("取消(&S)")
# 多进程设置
self.threads.clear()
if self.ui.mul_thread_checkbox.checkState() == 2:
self.end_num = len(files_list) // 2
t2 = threading.Thread(target=self.decode_dat, args=(files_list[self.end_num:], "线程2"))
self.threads.append(t2)
else:
self.end_num = len(files_list)
t1 = threading.Thread(target=self.decode_dat, args=(files_list[:self.end_num], "线程1"))
self.threads.append(t1)
for t in self.threads:
# s设置守护进程
t1.setDaemon(True)
t.start()
else:
return_code = QMessageBox.warning(self, "确认取消", '解密程序正在运行,是否确认要取消?', QMessageBox.Yes | QMessageBox.No)
if not return_code == QMessageBox.Yes:
pass
else:
# 取消后解密关闭子进程
for t in self.threads:
stop_thread(t)
self.start_flag = False
self.finish_flag = True
self.ui.mul_thread_checkbox.setDisabled(False)
self.ui.pic_src_button.setDisabled(False)
self.ui.pic_out_button.setDisabled(False)
self.ui.pic_src_lineEdit.setDisabled(False)
self.ui.pic_out_lineEdit.setDisabled(False)
self.ui.start_button.setText("开始(&S)")
# 核心功能
def decode_dat(self, files_list, str):
'''
解密功能核心,解密并生成正确二代图片文件
:param files_list: 获取到微信dat文件目录下的文件
:param str: 进程名称
:return:
'''
# 文件信息头前两个字节
# jpg / png / gif格式 / bmp格式
pic_head = [0xff, 0xd8, 0x89, 0x50, 0x47, 0x49, 0x42, 0x4D]
cant_decode_flag = False
# 遍历读取文件
for file_name in files_list:
file_path = os.path.join(self.ui.pic_src_lineEdit.text(), file_name)
# 判断文件是否dat后缀
if not file_path.endswith(".dat"):
self.ui.text_out_textEdit.append(
"<font color=\"#FF0000\">%s</font>" % (str + " " + file_path + " 解密失败,"
+ "失败原因:非dat格式文件"))
self.file_fail_num = self.file_fail_num + 1
self.file_done_num = self.file_done_num + 1
continue
with open(file_path, "rb") as dat_file:
dat_read = dat_file.read(2)
if len(dat_read) == 0:
self.ui.text_out_textEdit.append(
"<font color=\"#FF0000\">%s</font>" % (str + " " + file_path + " 解密失败,"
+ "失败原因:读取文件失败"))
self.file_fail_num = self.file_fail_num + 1
self.file_done_num = self.file_done_num + 1
continue
head_index = 0
cant_decode_flag = False
# 开始解密
while head_index < len(pic_head):
# 使用第一个头信息字节来计算加密码
# 第二个字节来验证解密码是否正确
code = dat_read[0] ^ pic_head[head_index]
idf_code = dat_read[1] ^ code
head_index = head_index + 1
if idf_code == pic_head[head_index]:
self.decode_code = code
break
else:
cant_decode_flag = True
head_index = head_index + 1
# 根据索引得到文件后缀
if 0 <= head_index <= 1:
exten_name = ".jpg"
elif 2 <= head_index <= 3:
exten_name = ".png"
elif 4 <= head_index <= 5:
exten_name = ".gif"
elif 5 <= head_index <= 6:
exten_name = ".bmp"
elif cant_decode_flag:
self.ui.text_out_textEdit.append("<font color=\"#FF0000\">%s</font>" % (str + " " + file_path + "解密失败,"
+ "失败原因:未找到相应图片格式"))
self.file_fail_num = self.file_fail_num + 1
self.file_done_num = self.file_done_num + 1
continue
pic_name = os.path.join(self.ui.pic_out_lineEdit.text(), file_name)
pic_name = pic_name.split('.')[0]
pic_name = pic_name + exten_name
# 对dat文件所有数据进行解密,并生成正确图片文件
try:
dat_file = open(file_path, "rb")
pic_write = open(pic_name, "wb")
for dat_data in dat_file:
for dat_byte in dat_data:
pic_data = dat_byte ^ self.decode_code
pic_write.write(bytes([pic_data]))
except OSError:
self.ui.text_out_textEdit.append("<font color=\"#FF0000\">%s</font>" % (str + " " + file_path + "解密失败,"
+ "失败原因:系统错误"))
self.file_fail_num = self.file_fail_num + 1
self.file_done_num = self.file_done_num + 1
except IOError:
self.ui.text_out_textEdit.append("<font color=\"#FF0000\">%s</font>" % (str + " " + file_path + "解密失败,"
+ "失败原因:写入或读取错误"))
self.file_fail_num = self.file_fail_num + 1
self.file_done_num = self.file_done_num + 1
else:
self.ui.text_out_textEdit.append(str + " " + pic_name + " " + "解密完成")
self.file_done_num = self.file_done_num + 1
finally:
self.ui.text_out_textEdit.moveCursor(self.ui.text_out_textEdit.textCursor().End)
dat_file.close()
pic_write.close()
# 子进程强制性停止函数,该实现参考于博客
https://www.cnblogs.com/rainduck/archive/2013/03/29/2989810.html
def _async_raise(tid, exctype):
'''
:param tid:子进程进程号
:param exctype:执行类型
:return:无
'''
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def stop_thread(thread):
'''
:param thread:子进程对象
:return:无
'''
_async_raise(thread.ident, SystemExit)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = WechatMainWindow()
window.setWindowTitle("PC版微信图片解密工具 Beta1")
app.setWindowIcon(QIcon('./lemon.ico'))
window.show()
QMessageBox.information(window, "提示", "该软件仅用于学习研究,可能存在未知原因崩溃问题,请勿作正规用途。")
exit(app.exec_())
3.2. UI代码
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'wechatdecode_ui.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
# 初始化
MainWindow.setObjectName("MainWindow")
MainWindow.setEnabled(True)
MainWindow.resize(829, 519)
MainWindow.setMinimumSize(QtCore.QSize(800, 480))
MainWindow.setMaximumSize(QtCore.QSize(1280, 720))
# 主要使用表格布局,并将各控件添加进表格布局中合适的位置
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.pic_out_label = QtWidgets.QLabel(self.centralwidget)
self.pic_out_label.setObjectName("pic_out_label")
self.gridLayout.addWidget(self.pic_out_label, 2, 0, 1, 1)
self.pic_out_lineEdit = QtWidgets.QLineEdit(self.centralwidget)
self.pic_out_lineEdit.setObjectName("pic_out_lineEdit")
self.gridLayout.addWidget(self.pic_out_lineEdit, 2, 1, 1, 3)
self.close_button = QtWidgets.QPushButton(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.close_button.sizePolicy().hasHeightForWidth())
self.close_button.setSizePolicy(sizePolicy)
self.close_button.setObjectName("close_button")
self.gridLayout.addWidget(self.close_button, 7, 4, 1, 1)
self.pic_src_button = QtWidgets.QPushButton(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.pic_src_button.sizePolicy().hasHeightForWidth())
self.pic_src_button.setSizePolicy(sizePolicy)
self.pic_src_button.setObjectName("pic_src_button")
self.gridLayout.addWidget(self.pic_src_button, 1, 4, 1, 1)
self.start_button = QtWidgets.QPushButton(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.start_button.sizePolicy().hasHeightForWidth())
self.start_button.setSizePolicy(sizePolicy)
self.start_button.setObjectName("start_button")
self.gridLayout.addWidget(self.start_button, 7, 5, 1, 1)
self.pic_src_lineEdit = QtWidgets.QLineEdit(self.centralwidget)
self.pic_src_lineEdit.setObjectName("pic_src_lineEdit")
self.gridLayout.addWidget(self.pic_src_lineEdit, 1, 1, 1, 3)
self.pic_out_button = QtWidgets.QPushButton(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.pic_out_button.sizePolicy().hasHeightForWidth())
self.pic_out_button.setSizePolicy(sizePolicy)
self.pic_out_button.setObjectName("pic_out_button")
self.gridLayout.addWidget(self.pic_out_button, 2, 4, 1, 1)
self.about_button = QtWidgets.QPushButton(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.about_button.sizePolicy().hasHeightForWidth())
self.about_button.setSizePolicy(sizePolicy)
self.about_button.setObjectName("about_button")
self.gridLayout.addWidget(self.about_button, 7, 0, 1, 1)
self.open_src_button = QtWidgets.QPushButton(self.centralwidget)
self.open_src_button.setObjectName("open_src_button")
self.gridLayout.addWidget(self.open_src_button, 1, 5, 1, 1)
self.text_out_textEdit = QtWidgets.QTextEdit(self.centralwidget)
self.text_out_textEdit.setEnabled(True)
self.text_out_textEdit.setReadOnly(True)
self.text_out_textEdit.setObjectName("text_out_textEdit")
self.gridLayout.addWidget(self.text_out_textEdit, 4, 0, 2, 6)
self.open_out_button = QtWidgets.QPushButton(self.centralwidget)
self.open_out_button.setObjectName("open_out_button")
self.gridLayout.addWidget(self.open_out_button, 2, 5, 1, 1)
self.progressBar = QtWidgets.QProgressBar(self.centralwidget)
self.progressBar.setMinimumSize(QtCore.QSize(0, 1))
self.progressBar.setProperty("value", 0)
self.progressBar.setObjectName("progressBar")
self.gridLayout.addWidget(self.progressBar, 6, 0, 1, 6)
self.pic_src_label = QtWidgets.QLabel(self.centralwidget)
self.pic_src_label.setObjectName("pic_src_label")
self.gridLayout.addWidget(self.pic_src_label, 1, 0, 1, 1)
self.mul_thread_checkbox = QtWidgets.QCheckBox(self.centralwidget)
self.mul_thread_checkbox.setObjectName("mul_thread_checkbox")
self.gridLayout.addWidget(self.mul_thread_checkbox, 3, 0, 1, 1)
self.done_after_open_checkbox = QtWidgets.QCheckBox(self.centralwidget)
self.done_after_open_checkbox.setChecked(True)
self.done_after_open_checkbox.setObjectName("done_after_open_checkbox")
self.gridLayout.addWidget(self.done_after_open_checkbox, 3, 1, 1, 1)
self.horizontalLayout.addLayout(self.gridLayout)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 829, 22))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.pic_out_label.setBuddy(self.pic_out_lineEdit)
self.pic_src_label.setBuddy(self.pic_src_lineEdit)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
# 设置控件提示符或设置控件字符
self.pic_out_label.setToolTip(_translate("MainWindow", "<html><head/><body><p>设置解密后图片保存目录</p></body></html>"))
self.pic_out_label.setText(_translate("MainWindow", "解密后图片保存目录:"))
self.pic_out_lineEdit.setToolTip(_translate("MainWindow", "<html><head/><body><p>设置解密后图片保存目录</p></body></html>"))
self.close_button.setText(_translate("MainWindow", "关闭(&C)"))
self.pic_src_button.setToolTip(_translate("MainWindow", "<html><head/><body><p>微信加密图片默认存储于</p><p><span style=\" text-decoration: underline;\">"C:\\Users\\您的用户名\\Documents\\WeChat Files\\您的微信号\\FileStorage\\Image\\20XX-XX"</span></p></body></html>"))
self.pic_src_button.setText(_translate("MainWindow", "浏览.."))
self.start_button.setText(_translate("MainWindow", "开始(&S)"))
self.pic_src_lineEdit.setToolTip(_translate("MainWindow", "<html><head/><body><p>微信加密图片默认存储于</p><p><span style=\" text-decoration: underline;\">"C:\\Users\\您的用户名\\Documents\\WeChat Files\\您的微信号\\FileStorage\\Image\\20XX-XX"</span></p></body></html>"))
self.pic_out_button.setToolTip(_translate("MainWindow", "<html><head/><body><p>设置解密后图片保存目录</p></body></html>"))
self.pic_out_button.setText(_translate("MainWindow", "浏览.."))
self.about_button.setText(_translate("MainWindow", "关于(&A)"))
self.open_src_button.setText(_translate("MainWindow", "打开"))
self.open_out_button.setText(_translate("MainWindow", "打开"))
self.pic_src_label.setToolTip(_translate("MainWindow", "<html><head/><body><p>微信加密图片默认存储于</p><p><span style=\" text-decoration: underline;\">"C:\\Users\\您的用户名\\Documents\\WeChat Files\\您的微信号\\FileStorage\\Image\\20XX-XX"</span></p></body></html>"))
self.pic_src_label.setText(_translate("MainWindow", "微信图片的缓存目录:"))
self.mul_thread_checkbox.setToolTip(_translate("MainWindow", "<html><head/><body><p>启用双线程可能会加快解密速度,但也会占用过多的CPU资源</p></body></html>"))
self.mul_thread_checkbox.setText(_translate("MainWindow", "启动双线程"))
self.done_after_open_checkbox.setText(_translate("MainWindow", "完成后打开文件夹"))
4. 实验
图9 程序运行主界面
图10 程序解密运行时
图11 防止用户误操作
图12 程序关于页面
图12 解密前的dat文件
图13 解密后的生成的可正常识别的图片
5. 总结与展望
本次程序设计,旨在以软件开发者的身份出发,为用户考虑需求,做出对普通用户友好、易用的图形化解密小工具。通过这次设计对Python的文件读写操作有了更深的理解和认识,对异常捕捉更为熟悉。对异或法加密文件的方式也有了新的理解。同时也学会了PyQt的使用。也初步尝试了Python的多线程编程。但尽管做了防止用户误操作的措施和文件读写时可能出来的异常进行捕捉,该程序仍有低概率会在解密过程时出现崩溃。可能是因为我对多线程编程尚未熟悉,操作失当导致。未来会力求排查出原因并持续对该程序进行维护改进。
6. 参考文献
[1] python中threading方式创建的线程的终止
https://www.cnblogs.com/rainduck/archive/2013/03/29/2989810.html
[2] BMP文件格式详解
https://blog.youkuaiyun.com/o_sun_o/article/details/8351037