此前接单做了一个批量解压缩小工具后脑洞大开,打算给运营小姐姐们做一个小工具,解放一下她们时间,同时享受一下小姐姐们的崇拜的眼神......好吧,话不多说,咱们直接开始:
使用场景与需求分析 解释一下使用场景:公司为了数据安全,给所有下载的数据压缩包设置了一个6位数字密码,密码使用短信发送到下载人指定手机上。如果一次下载单个文件,那解压缩很简单,但是如果下载的文件数量较多,一个一个压缩包与密码短信进行配对那就有点让人烦躁了。
因此,小工具要实现的需求就是:能够批量的将压缩包与密码短信文本进行自动匹配,将匹配到的压缩包进行解压缩;同时,要实现一个交互界面,并打包成可执行文件。
分步实现 有了需求,那么就是一步一步实现它:
要打包成exe可执行文件,可以使用pyinstaller进行打包,若要打包文件最小,需要创建一个虚拟环境,安装最少的第三方包,因此,使用pycharm进行虚拟环境创建,并安装需要使用到的包。 pycharm创建虚拟环境 安装需要使用到的包:pyinstaller、pyzipper(zipfile不支持我司的压缩包格式解压)
安装需要的包 # -*- coding: utf-8 -*- # @author: Lin Wei # @contact: 580813@qq.com # @time: 2021/11/23 10:35 # @file: unzip_file.py # @desc: import tkinter as tkfrom tkinter import filedialogfrom extract_files import ExtractFiles # 需要自行实现的方法 # 定义一个tk窗口 wd = tk.Tk() wd.title('解压小程序V1.0' ) wd.geometry('800x500' )# 设置选择解压缩文件包所在路径的按钮及功能 # 定义选择压缩包的路径为tk的字符串变量 unzip_files_path = tk.StringVar()def get_files_path () : """ 运行函数将设置路径 """ unzip_files_path.set(filedialog.askdirectory())# 定义压缩包路径的标签,并设置所在位置 files_path_label = tk.Label(wd, text='需要批量解压的文件路径:' ) files_path_label.grid(row=0 , column=0 )# 定义压缩包路径的输入框,并设置所在位置,其中输入框所显示内容为tk变量unzip_files_path files_path_entry = tk.Entry(wd, textvariable=unzip_files_path, width=80 ) files_path_entry.grid(row=0 , column=1 )# 定义压缩包路径的选择按钮,并设置所在位置,其中按钮点击的命令为get_unzip_files_path files_path_button = tk.Button(wd, text='选择路径' , command=get_files_path) files_path_button.grid(row=0 , column=2 )# 设置解压缩密码文本的text框,同时实现可输入或选择txt文件进行读取的功能 # 定义选择解压缩密码文本文件的函数 def choose_password_text () : password_text_path = filedialog.askopenfilename( title='请选择解压缩的密码文本' , initialdir='/' , filetypes=[('文本文档' , '*.txt' )] ) if password_text_path: text = 'TXT文件错误或编码非utf-8和ansi,请在文本框中直接输入密码文本' for code in ['utf-8' , 'ansi' ]: try : with open(password_text_path, 'r' , encoding=code) as fin: text = fin.read() break except Exception: pass password_text_text.delete(1.0 , 'end' ) password_text_text.insert(1.0 , text) info_mes = """这里写上短信文本的示例,作为提示""" # 定义密码文本的标签,并设置所在位置 password_text_label = tk.Label(wd, text='解压缩文件的密码短信文本:' ) password_text_label.grid(row=1
, column=0 )# 定义密码文本的输入框,并设置所在位置,其中输入框所显示内容为tk变量password_text password_text_text = tk.Text(wd, height=15 ) password_text_text.insert(1.0 , f'直接在此粘贴短信文本,示例如下:\n{info_mes} \n(可以多条)\n也可以点击左侧按钮读取存放在TXT文件中的短信文本' ) password_text_text.grid(row=1 , column=1 , sticky=tk.E+tk.W+tk.N)# 定义压缩包路径的选择按钮,并设置所在位置,其中按钮点击的命令为choose_path1 password_text_button = tk.Button(wd, text='选择密码文件' , command=choose_password_text) password_text_button.grid(row=1 , column=2 )# 设置解压运行按钮以及解压缩的方法 def run_unzip () : # 从路径选择框里获取路径文本 root_path = files_path_entry.get() # 从密码输入框里获取密码文本 password_text = password_text_text.get(1.0 , 'end' ) # 清除执行情况框中的全部内容 run_message_text.delete(1.0 , tk.END) # 运行解压缩程序 obj = ExtractFiles(root_path, password_text) obj.get_all_zip_files() obj.parse_password() obj.matching() obj.do_extract() # 往执行情况框里添加内容 # 添加成功解压缩的记录 run_message_text.insert(tk.END, '\n' .join([f'成功解压{x} ' for x in obj.success_files_list]) + '\n' ) # 添加解压失败的记录(注:有匹配到密码,但解压缩失败) run_message_text.insert(tk.END, '\n' .join([f'解压失败{x} ' for x in obj.fail_files_list])+'\n' ) # 添加未匹配到压缩包的文件记录 run_message_text.insert(tk.END, '未匹配到密码的压缩包有:\n' + '\n' .join([f'{x} ' for x in obj.not_found_pwd_files])+'\n' ) # 添加整体运行情况统计结果 run_message_text.insert(tk.END, f""" 共找到{obj.zip_file_count} 个压缩包,其中{len(obj.success_files_list)} 个解压成功,{len(obj.fail_files_list)} 个解压失败, 有{len(obj.not_found_pwd_files)} 个压缩包未匹配到密码,有{len(obj.not_found_file_password)} 个密码未匹配到压缩包文件。""" ) submit_button = tk.Button(wd, text='提交运行' , command=run_unzip, bg='yellow' ) submit_button.grid(row=2 , column=2 )# 解压缩文件的执行情况 run_message_label = tk.Label(wd, text='解压情况:' ) run_message_label.grid(row=5 , column=0 )# 定义密码文本的输入框,并设置所在位置,其中输入框所显示内容为tk变量password_text run_message_text = tk.Text(wd, height=15 ) run_message_text.grid(row=5 , column=1 , sticky=tk.E+tk.W+tk.N)# 添加一个说明标签 info_label = tk.Label(wd, text='本小程序为学习Python程序进行开发,仅用于方便工作使用,不得用于任何违法事宜。Power By Wei Lin' ) info_label.grid(row=6 , column=1 )# 使窗口保持刷新 wd.mainloop() 思路解析:很基础的页面,不过记得哪个大佬说过,先实现核心的功能,其他的(美化)以后再说。此外,代码中已经把后续写的一些代码贴上了,在实际编写过程中实际上是先预留空位,后续代码实现后补充的。
有了界面,要实现功能 3.1先实现单个文件解压缩的功能 import pathlibimport pyzipper as zpclass ExtractFile : def __init__ (self, file_path, pwd=None, out_path=None) : """ 初始化解压文件类的信息 :param file_path: 需解压文件的完整路径 :param pwd: 解压的密码,默认为None :param out_path: 解压出来的文件存放的路径,默认为None,即解压缩到压缩文件的相同目录下 """ self.file_path = file_path self.pwd = pwd # 判断一下文件是否是zip压缩包,给self.is_zip_file赋值 if zp.is_zipfile(self.file_path): self.is_zip_file = True else : self.is_zip_file = False print(f'{self.file_path} 文件并不是压缩文件' ) # 判断输出地址是否为正确路径,如正确则定义为解压缩后的输出路径,如果错误,则以压缩包所在路径为准 if out_path is not None : out_path = pathlib.Path(out_path) # 将out_path转成path对象 if out_path.is_dir(): # 根据out_path对象的is_dir方法判断是否文件夹 self.out_path = out_path
else : # 如果不是文件夹则将文件所在的路径作为输出文件夹 self.out_path = pathlib.Path(self.file_path).parent else : # 如果不是文件夹则将文件所在的路径作为输出文件夹 self.out_path = pathlib.Path(self.file_path).parent def extract_all (self, pwd, out_path) : """ 解压缩所有文件 :return: bool """ if self.is_zip_file: try : with zp.AESZipFile(self.file_path, mode='r' ) as zip_file: zip_file.extractall(path=out_path, pwd=pwd.encode('utf-8' )) return True except Exception as e: print(f'解压缩文件错误,错误信息为: {e} ' ) return False else : print(f'{self.file_path} 文件并不是压缩文件' ) return False if __name__ == '__main__' : file_path = 'd:/测试文件.zip' obj = ExtractFile(file_path) obj.run_extract()思路解析:上面的代码都有注释就不一一解释了,需要提一下的是,单独建了一个py文件存放这个代码,以便调试以及后续更新功能。
3.2再实现批量解压缩文件的功能
import pathlibimport refrom extract_single_file import ExtractFileclass ExtractFiles : def __init__ (self, root_path, password_text) : # 需要解压缩文件所在的路径 self.root_path = root_path # 初始化zip文件的个数 self.zip_file_count = 0 # 初始化未找到匹配密码的文件list self.not_found_pwd_files = [] # 根目录中所有的zip文件字典,其中key为去除扩展名后的文件名,value为list格式,为zip文件的完整路径 self.zip_file_dict = {} # 密码文本 self.password_text = password_text # 初始化密码字典,其中key为短信中任务名称(与压缩包名称对应),value为set集合格式,为六位数字密码 self.password_dict = {} # 初始化密码信息的个数 self.password_count = 0 # 初始化解压缩的dict,key为需要解压缩文件的完整路径名称,value为匹配到的密码(list格式) self.unzip_dict = {} # 初始化未找到匹配文件的密码list self.not_found_file_password = [] # 初始化成功解压缩的文件list self.success_files_list = [] # 初始化解压缩失败的文件list self.fail_files_list = [] def get_all_zip_files (self) : """ 为防止模块报毒,使用pathlib模块遍历根目录下的所有zip文件,生成zip_file_dict """ for file in pathlib.Path(self.root_path).rglob('*.zip' ): if not file.name.startswith('~' ): self.zip_file_count += 1 full_path = str(file) # 完整路径是file对象的str格式 filename = file.stem # file对象的stem是去除扩展名后的文件名 if filename in self.zip_file_dict.keys(): self.zip_file_dict[filename].append(full_path) else : self.zip_file_dict[filename] = [full_path] def parse_password (self) : """ 使用正则解析密码字符串,生成字典 """ if self.password_text is not None : res = re.findall(r'任务名为(.*?) 的压缩包,解压密码为(\d{6}),' , self.password_text) if len(res) > 0 : for key, value in res: self.password_count += 1 if key in self.password_dict.keys(): self.password_dict[key].add(value) else : self.password_dict[key] = {value} def matching (self) : """ 给压缩包与密码配对 """ # 创建文件字典、密码字典的副本以便后续进行修改操作 file_dict = self.zip_file_dict.copy() password_dict = self.password_dict.copy() # 遍历匹配 for i in range(len(file_dict)): # 根据file_dict的长度,确定循环次数 # 每一次从file_dict中取出一对键值对,分别为不含扩展名的文件名与文件绝对路径list file, full_path_list = file_dict.popitem() if
file in password_dict.keys(): # 如果压缩文件名在密码字典的key中 # 取出密码的list pwd_list = password_dict.pop(file) # 将绝对路径与密码进行配对放入待解压的文件字典中 for full_path in full_path_list: self.unzip_dict[full_path] = pwd_list else : # 否则将文件绝对路径放入未找到密码的文件list中 for full_path in full_path_list: self.not_found_pwd_files.append(full_path) if len(password_dict) > 0 : # 如果匹配结束后,密码字典中还有剩余的键值对,把它们放入未找到文件的密码list中 for i in range(len(password_dict)): key, pwd_list = password_dict.popitem() for pwd in pwd_list: self.not_found_file_password.append((key, pwd)) def do_extract (self) : """ 运行批量解压 """ for file, pwd_list in self.unzip_dict.items(): # 遍历待解压的文件字典 add_to_fail = True # 将加入失败list初始化为True for pwd in pwd_list: # 遍历密码list进行解压 extract_obj = ExtractFile(file, pwd) res = extract_obj.extract_all(pwd=extract_obj.pwd, out_path=extract_obj.out_path) if res: # 如果成功 self.success_files_list.append(file) add_to_fail = False break if add_to_fail: # 如果值为True,说明所有密码均没有成功解压缩文件,因此加入有密码但解压失败的文件list中 self.fail_files_list.append(file)思路解析:这部分代码主要实现读取出所有zip文件,解析短信密码文本,并且进行匹配解压的过程,考虑到方便使用者,因此将文件与密码的匹配设计为多对多的方式,即目录下可能存在相同文件名的压缩包(同名文件可能在子文件夹中),密码文本可能存在重复或多条短信密码均为相同文件名的情形。 此外,解析密码文本是,需要用到正则表达式,找出文本中的文件名与密码的键值对,我使用的是“r'任务名为(.*?) 的压缩包,解压密码为(\d{6}),'”,具体情形需要具体修改。
打包成exe 5.1 打开cmd,进入到虚拟环境的路径中:我的是"D:/work/pythonproject/venv/scripts/activate.bat" 5.2 在cmd中,进入到存放py文件的路径,运行pyinstaller命令,我的是“pyinstaller -Fw unzip_file.py”(命令就不具体解释了,详见pyinstaller的教程) 打包结束后,目录下会出现一个dist目录,里面就是打包好的exe文件了,测试运行了一下,一切正常。
只有11M 特别说明: 如果在代码中使用os模块,则在后续打包成exe文件时,会被杀毒软件识别为病毒,所以,一开始就要避免使用os模块。(此前版本代码中使用os.walk进行文件遍历,就踩坑了,还好使用的地方不多,都改成pathlib了)
加个彩蛋 前面那些只是正常的解压缩步骤,适用的场景很小,不能体现咱的水平,咱得憋个大招:搞个暴力破解压缩包密码的功能,要是没收到密码短信或者误删短信了,还是能够解压缩文件,这才牛^_^(后来想了想,似乎这个功能也没什么软用,但就是要折腾,脑洞继续开)。
暴力破解功能实现 暴力破解只针对单个文件,那么直接在前面单个解压文件的程序中增加这个功能即可,由于密码为6位数字,一共有100万种组合,因此单线程肯定是不行的,要上多线程。此外,试验密码的顺序对于破解密码的消耗时间是有巨大影响的,因此,提供一些密码的排序方式供选择“正序、倒序、1优先、2优先等等”,以及一个“随机”方式,如果选择的方式恰好命中了正确密码的范围,那么将大大节约破解时间(有点买彩票的感觉^_^),上代码:
from extract_single_file import ExtractFileimport pyzipper as zpimport queueimport threadingimport timefrom itertools import productfrom random import shuffle comm = False # 定义一个全局变量信号 pwd_queue = queue.Queue() # 定义一个存放待测试密码的queue,用于多线程通信 fail_queue = queue.Queue() # 定义一个存放失败密码的queue method_dict = { '1优先' : 1 , '2优先' : 2 , '3优先' : 3 , '4优先' : 4 , '5优先' : 5 , '6优先' : 6 , '7优先' : 7 , '8优先' : 8 , } # 定义优先级数字字典,0和9分别是正序和倒序,无需特别定义 class CrackExtract (ExtractFile) : def __init__ (self, file_path, pwd_length=6 , method=None, pwd=None, out_path=None) : # 继承父类属性
super(CrackExtract, self).__init__(file_path, pwd, out_path) # 初始化密码长度 self.pwd_length = int(pwd_length) # 初始化暴力破解的顺序 self.method = method # 初始化打开zip文件,后续需要关闭 self.zip_file = zp.AESZipFile(self.file_path, mode='r' ) # 初始化尝试次数 self.try_count = 0 def product_pwd_queue (self) : """ 创建存放待破解密码的queue """ global comm, pwd_queue # 声明全局变量 num_list = list(range(10 )) if self.method == '逆序' : # 如果是逆序方式,则倒转num_list num_list = num_list[::-1 ] elif self.method in method_dict.keys(): # 如果数字优先,则重新组合num_list,将优先数字向两测重新排序 k = method_dict[self.method] + 1 part1 = num_list[:k] part2 = num_list[k:] num_list = [] while part1 or part2: if part1: num_list.append(part1.pop()) if part2: num_list.append(part2.pop(0 )) # 使用itertools的product方法创建6位密码的生成器,拼接后放入pwd_queue中 if self.method == '随机' : # 使用random模块的shuffle随机打散num_list pwd_list = [x for x in product(num_list, repeat=self.pwd_length)] shuffle(pwd_list) for s in pwd_list: if comm: return pwd_queue.put('' .join([str(x) for x in s])) else : for s in product(num_list, repeat=self.pwd_length): if comm: # 如果信号已经转为True,无需继续生产密码 return pwd_queue.put('' .join([str(x) for x in s])) def crack_extract (self) : """ 尝试解压缩程序 """ # 声明全局变量 global comm, pwd_queue, fail_queue while True : # 开启循环 if not pwd_queue.empty(): # pwd_queue不为空时 pwd = pwd_queue.get() # 取出一个密码尝试解压缩是否成功 try : self.zip_file.extractall(path=self.out_path, pwd=pwd.encode('utf-8' )) pwd_queue.queue.clear() # 若成功清除pwd_queue剩余密码 comm = True # 若成功,修改信号为True self.pwd = pwd.encode('utf-8' ) print(f'解压缩成功,密码是{pwd} ' ) except (TypeError, RuntimeError): self.try_count += 1 fail_queue.put(pwd) # 若失败,将失败密码存入失败队列 print(f'正在解压,尝试密码串{pwd} 失败,还剩余{pwd_queue.qsize()} 个密码串待试验' ) pwd_queue.task_done() # 处理完成 elif comm: # 若信号为True,终止循环 break else : # 若pwd_queue为空,等待0.2秒 time.sleep(0.2 ) def run_extract (self) : """ 运行解压程序 """ # 声明全局变量 global comm, pwd_queue, fail_queue comm = False # 将信号初始化为False # 初始化多线程list,并添加一个声明密码queue的线程 threadlist = [threading.Thread(target=self.product_pwd_queue, name='create' )] # 添加10个破解线程 for x in range(10 ): th = threading.Thread(target=self.crack_extract, name=f'extract{x} ' ) threadlist.append(th) start = time.time() # 开始时间 for t in threadlist: # 开启线程 t.start() for t in threadlist: # 等待线程结束 t.join() end = time.time() # 结束时间 cost_time = round(end - start, 4
) # 所用时长(秒) self.zip_file.close() # 关闭zip文件 print(f'解压成功,总用时{cost_time} 秒' ) return self.pwd, self.try_count, cost_timeif __name__ == '__main__' : file = r'D:\test\upzip\测试文件.zip' obj = CrackExtract(file, method='3优先' ) obj.run_extract()暴力破解测试截图 破解功能的界面 由于不想直接体现在程序界面中,因此,破解功能的界面写了一个新的交互界面,代码如下:
import tkinter as tkfrom tkinter import filedialog, messageboxfrom crack_extract import CrackExtract RADIO_DICT = { 0 : '正序' , 9 : '倒序' , 1 : '1优先' , 2 : '2优先' , 3 : '3优先' , 4 : '4优先' , 5 : '5优先' , 6 : '6优先' , 7 : '7优先' , 8 : '8优先' , 10 : '随机' }class PowerExtractWindow : def __init__ (self) : self.wd = tk.Toplevel() self.wd.title('破解密码小程序V1.0' ) self.wd.geometry('600x200' ) self.file_path = tk.StringVar() self.path_button = None self.path_entry = None self.submit_button = None self.mes_window = None self.radio = tk.IntVar() def choose_file (self) : """ 设置选择解压缩文件包所在路径的按钮及功能方法 """ self.file_path.set(filedialog.askopenfilename( title='请选择文件' , initialdir='/' , filetypes=[('压缩文件' , '*.zip' )] )) def power_extract (self) : """ 开启破解的方法 """ # 由于破解耗时较长,弹出一个窗口让用户再次确认是否开始 answer = messagebox.askokcancel('确认是否开始' , '破解密码需要等待较长时间,你确定更要开始吗?' ) if answer: # 确认开始 self.mes_window.delete(1.0 , tk.END) # 清理信息窗口的内容 # 初始化破解的对象 crack_obj = CrackExtract(self.path_entry.get(), method=RADIO_DICT.get(self.radio.get())) # 执行破解程序,并获得正确破解后的密码、尝试次数、用时等信息,并输出到窗口上 pwd, try_count, cost_time = crack_obj.run_extract() mes_text = f'解压文件{self.path_entry.get()} 成功,正确密码是{pwd.decode("utf-8" )} ,' \ f'共尝试了{try_count} 次,总耗时{int(cost_time // 60 )} 分{cost_time % 60 } 秒' self.mes_window.insert(1.0 , mes_text) def layout (self) : """ 对窗口进行布局的方法 """ # 定义压缩包路径的选择按钮,并设置所在位置,其中按钮点击的命令为get_unzip_files_path self.path_button = tk.Button(self.wd, text='请选择破解文件' , command=self.choose_file) self.path_button.grid(row=0 , column=0 ) # 定义压缩包路径的输入框,并设置所在位置,其中输入框所显示内容为tk变量unzip_files_path self.path_entry = tk.Entry(self.wd, textvariable=self.file_path, width=60 ) self.path_entry.grid(row=0 , column=1 , columnspan=10 ) # 定义压缩包路径的输入框,并设置所在位置,其中输入框所显示内容为tk变量unzip_files_path self.submit_button = tk.Button(self.wd, text='开始破解' , command=self.power_extract, bg='red' ) self.submit_button.grid(row=5 , column=0 )
# 定义选择破解密码的排序方式,单选项,并布局 r_label = tk.Label(self.wd, text='请选择破解的密码排序方式:' ) r_label.grid(row=1 , column=0 , rowspan=3 ) col = 0 for k, v in RADIO_DICT.items(): exec(f'tk.Radiobutton(self.wd, text="{v} ", variable=self.radio, value={k} ).grid(row={col//5 +1 } , column={col%5 +1 } )' ) col += 1 # 定义运行消息窗口 self.mes_window = tk.Text(self.wd, height=5 , width=60 ) self.mes_window.grid(row=5 , column=1 , columnspan=10 ) self.wd.mainloop()def open_power_window () : """ 打开破解的窗口函数 """ new_wd = PowerExtractWindow() new_wd.layout()if __name__ == '__main__' : wd = tk.Tk() wd.title('暴力破解testV1.0' ) wd.geometry('400x200' ) submit_button = tk.Button(wd, text='打开二级窗口' , command=open_power_window, bg='red' ) submit_button.pack() wd.mainloop()破解窗口的测试截图 将破解功能加入原界面中(隐藏界面) 给压缩包加密码本身是为了数据保密,而暴力破解密码,某种角度看,似乎有点不合适,因此,直接放在界面中不太合适,咱搞个隐藏界面,必须输入密码口令才能打开,至于口令就用“上上下下左右左右BABA”(致敬一下经典,嘿嘿)。
以下代码在原交互界面上增加 # 引入新的破解界面 from power_unzip import open_power_window def judge (func) : """ 装饰器函数,用于判断是否为特殊口令打开隐藏界面, :param func: 需要增加功能的函数 """ def open_power_wd () : # 获取密码文本的输入框的输入内容,如何符合条件,则运行破解程序 password_text = password_text_text.get(1.0 , 'end' ) if password_text.strip() == '上上下下左右左右BABA' : open_power_window() return func() return open_power_wd将装饰器放置在需要装饰的函数前 完成上述步骤后,再次使用pyinstaller进行打包即可。
成品展示 破解测试 破解程序的测试结果 最终总结 之所以开这么一个脑洞,其实主要是设计了一个场景,为了锻炼一下综合的应用能力,把学到的分项知识进行一次融合。
整个过程中用到的模块有:
itertools.product,求多个可迭代对象的笛卡尔积 在实现过程中,按自己理解进行了分项的拆解实现,以便后续进行功能的迭代;同时,应用了装饰器,在不改变原代码的情况下,增加功能等等。通过这样的过程,加深对各个功能的理解。
好吧,这次的脑洞先开到这里了,等有时间了咱继续开脑洞。
最后,推荐蚂蚁老师的《Python零基础入门到数据分析到办公自动化实战》课程,购买后加老师微信答疑:ant_learn_python