学习笔记:用python3实现全手工解压zip文件,包含所有实现的源代码
目录
一、引子
前些日子,因为自己手写了一个base64编码和解码,有点小开心,随口说了句有空挑战一下用python3手写解压zip文件的代码。前几天有空,在网上搜索了一下,发现一篇很好的文章《ZIP压缩算法详细分析及解压实例解释》,原文地址:
https://www.cnblogs.com/esingchan/p/3958962.html
这篇文章对ZIP压缩算法进行了详细的分析并用一个解压文件实例进行了字节和二进制位的逐步解码和解释,非常感谢作者的讲解,我从中学到了很多知识!
郑重声明:本文引用了《ZIP压缩算法详细分析及解压实例解释》中的部分图片和文字信息,本人在此郑重声明,所有这些图片和文字的版权均归原文作者所有。如有侵权,请告知我,我会立即删除!
这篇文章大概两万多字,要想全部弄懂弄通确实要动动脑筋,说实话我看了好几遍,终于看懂了,当然后期在编写代码过程中又多次翻看!经过好几天的编写,经历了N次进坑出坑,终于在我疯之前实现了zip文件的解压工作。
附上python3全手工解压zip文件源代码地址(编码不易,收费4.99元,以资鼓励😂):
https://download.csdn.net/download/weixin_37581211/16175968
(目前只写了单文件的解压算法,多文件和带目录部分还没有写,因为这几天我的工作比较忙,等有空了再完善一下)。
废话太多,先上解压效果图(本图为解压了一个玫瑰花.jpg文件):
接下来我就把我的学习和编码过程带给大家,为了简化,我和《原文》例子一样,解压的是一个不带目录的压缩了单个文件的zip文件。因为本人算法比较弱,而且语文也很一般,写文章是我的弱项!所以难免很多疏忽和错误,有些部分也存在很大的优化空间,希望大家批评指正。另外,因为是全手工编码,所以内容比较多,文章有点长。
二、说干就干,七步解码ZIP
《ZIP压缩算法详细分析及解压实例解释》(以下简称《原文》)中讲的很详细,我在这里不再啰嗦,有兴趣可以去原文看分析过程。这里只列出原文中我需要用到的部分。
既然是说干就干,那么看懂了《原文》以后我就开工了。首先我们要明确《原文》的编码流程图如下:
这个图是动态哈夫曼算法的整个编码的流程,如果是静态哈夫曼和非压缩数据要简单得多,下面会给出所有算法的解码方法。
我们既然是要解码,那么就要从下向上反着来解码!原文举了一个压缩文件的例子,并且一步一步的把zip文件中的字节及二进制数据拆开进行逐步解压展示,非常直观,最后又总结了动态哈夫曼算法的译码过程如下(以下粗体字引用自《原文》):
1、根据HCLEN得到截尾信息,并参照固定置换表,根据CCL比特流得到CCL整数序列;
2、根据CCL整数序列构造出等价于CCL的二级Huffman码表3;
3、根据二级Huffman码表3对CL1、CL2比特流进行解码,得到SQ1整数序列,SQ2整数序列;
4、根据SQ1整数序列,SQ2整数序列,利用游程编码规则得到等价的CL1整数序列、CL2整数序列;
5、根据CL1整数序列、CL2整数序列分别构造两个一级Huffman码表:literal/length码表、distance码表;
6、根据两个一级Huffman码表对后面的LZ压缩数据进行解码得到literal/length/distance流;
7、根据literal/length/distance流按照LZ规则进行解码。
接下来我们就一步一个脚印的来解码!
0、先读取数据区数据
进入上面的步骤有个前提,就是需要先把zip文件中的数据区读出来,那么我们先用16进制编辑器看一下《原文》的zip解压例子文件内容:
我们要读出的数据区是从红线之后到蓝线之前,当然我们实际读取的时候需要先找到这个位置,我们可以利用zip文件头的结构以及其中的信息找到这个位置。《原文》中有文件结构的详细信息,这里不再列举,我们只要跳过头部的固定字节数,再跳过紧接着的文件名的字节数(这个值不固定,但是记录这个文件名所占字节数的位置是固定的第26和第27字节处),再跳过扩展区的字节数(与文件名同理,在第28和第29字节处)就可以直接找到数据区的开始了,数据区的结束在哪呢?我们是单文件的解压,而蓝线部分(central directory)是以0x50、0x4B、0x01、0x02开头的,central directory的开头也就是数据区的结束,那么我们只要找到0x50、0x4B、0x01、0x02,也就找到数据区的结束了。获取数据区的主要代码如下:
# 在列表中查找匹配的一串数值的位置
def findvalue(content,subdata):
sublen=len(subdata)
if content==0 or sublen==0: # 搜索数据为空!
return(-1)
pos=0
catch=0
while pos<len(content):
while content[pos]==subdata[catch]:
pos+=1
catch+=1
if catch==sublen:
return(pos-sublen)
catch=0
pos+=1
return(-1)
if __name__=="__main__":
# 以二进制读取zip文件内容
zipfile=open(sys.path[0]+r"/Test.zip", mode='rb')
content = zipfile.read() # 文件内容(二进制),这里采用一次性读入
zipfile.close()
# zip文件的头四位是固定的(简单判断一下):0x04034b50(注意是小端序,所以实际存储的字节是反序的!)
if content[0]==0x50 and content[1]==0x4b and content[2]==0x03 and content[3]==0x04:
# print("zip file!")
# 第4、5位,解压文件所需最低版本
least_version=str(content[5])+str(content[4])
# 第8、9位,压缩方法(本例是 08 00)
compress_method=str(content[9])+str(content[8])
# 第26、27字节,这里记录的是文件名的长度
filename_len=content[27]*256+content[26]
# 暂时没处理编码兼容问题,如果decode("gb2312")出错,
# 先改用下一句我自己编制的UTF8解码函数
#filename=lzq_lz77.decodeUTF8(content[30:30+filename_len])
filename=content[30:30+filename_len].decode("gb2312")
# 第28、29字节,这里记录的是扩展区的大小
extend_len=content[29]*256+content[28]
# 数据从30+filenamelen+extendlen开始,一直到504B0102
# 这里暂时只解压不带目录的zip文件,呵呵。
dataend=findvalue(content,[0x50,0x4b,0x01,0x02])
if dataend!=-1:
# 到这就取得数据了
data=content[30+filename_len+extend_len:dataend]
……
这样就把zip中的数据区读出来放进data中了,注意data为bytes类型。data数据由一个或多个数据块构成(每个数据块都可以是不同的算法),单个动态哈夫曼算法数据块的结构如《原文》的下图:
注意,这个图是一个动态哈夫曼编码数据块的结构图,不适用静态哈夫曼编码数据块以及非压缩数据块(这是我后期发现的,并且网上相关资料很少,只有官方RFC1951文档中草草提了几句)
这里要着重说一下Header,Header里有两个重要标识,第1位标识本数据块是否为最后一个,“1”就是最后一个,“0”说明后面还有数据块,我就是根据这个位来控制读取data中的多个数据块的循环逻辑的; Headet的第2和第3位用来标识解压的算法,"10"为动态哈夫曼算法,"01"为静态哈夫曼算法,"00"为非压缩算法。下面我就根据三种不同的算法来继续讲解我们的解码过程!
1、动态哈夫曼算法(前6步)
如果读取的数据块是动态哈夫曼算法(根据Header的第2和第3位来区分),我们要进行1~6步骤如下:
0x1、读取CCL
先看代码如下:
isnotend=True # 待解压数据没有结束
bindata=data2bin(data) # 将数据转换为二进制位字符串
curpos=0 # curpos是当前在bindata中的位置
lz77=[] # 待lz77解压列表
unzipdata=[] # 解压后数据
while isnotend: # 多块解压循环体
# Header:3个位,HLIT:5位,HDIST:5位,HCLEN:4位,共17位,所以得先读入四个字节的数据
Header=bindata[curpos:curpos+3]
if Header[0]=="1":
print("Header[0]=",Header[0],"“1说明为最后一个压缩数据块”") ####
else:
print("Header[0]=",Header[0],"“0说明后面还有数据块”") ####
print("压缩算法:",Header[1:3][::-1]) ####
if Header[0]=="1": # 1表示此部分为最后一个压缩数据块
isnotend=False # 下一次跳出循环
if Header[1:3][::-1]=="10": # 因zip中采用的是低位在前的存储方法,所以需要倒序,01倒过来是10,所以是动态哈夫曼
print("zip压缩算法:",Header[1:3][::-1],"动态哈夫曼")
HLIT=int(bindata[curpos+3:curpos+8][::-1],2) # 说明后面存在HLIT+257=259个CL1,CL1即0-258被编码后的长度,其中0-255表示Literal,256表示无效符号,257、258分别表示Length=3、4(length从3开始)
HDIST=int(bindata[curpos+8:curpos+13][::-1],2) # 说明后面存在HDIST+1=11个CL2,CL2即Distance Code=0-10被编码的长度
HCLEN=int((bindata[curpos+13:curpos+17])[::-1],2) # 说明紧接着跟着HCLEN+4=18个CCL(每个ccl为3个位)
ccl=[] # zip文件中最后生成的ccl序列
for k in range(HCLEN+4): # 从二进制数据中取出ccl原始数据
ccl.append(bindata[k*3+17+curpos:k*3+20+curpos])
for l in range(19-len(ccl)): # ccl补齐19个元素(用"000"补),生成ccl序列
ccl.append("000")
注释比较详细,要注意的是,我为了简化,一次性就把所有数据(data)转换为二进制位了,这种方法对于特大文件很耗内存,大家可以根据实际情况进行改进。另外代码中的HLIT、HDIST、HCLEN解释起来非常麻烦,涉及算法原理。如果看不懂注释请看《原文》。
上面代码中用到的转换二进制函数如下:
# 给一个字做按位倒叙
def getbin(highbyte,lowbyte):
mybyte=str(bin(highbyte*256+lowbyte)) # 取出两个字节,小端序
mybyte="%016d"%int(mybyte[2:]) # 二进制数如果不够16位则前面加“0”补齐16位
mybyte=mybyte[::-1] # 倒过来
return(mybyte)
# 将data全部转换为bin字符串
def data2bin(data): # data为二进制list
resultstr=""
lendata=len(data)
if len(data)%2!=0:
lendata-=1
for i in range(0,lendata,2):
resultstr+=getbin(data[i+1],data[i])
if len(data)%2!=0:
resultstr+=getbin(0,data[len(data)-1]) # 如果data不是偶数个字节,在最后补上一个字节的“0”
return(resultstr)
要强调的是,因为zip中存储二进制位时都是反序的,所以在getbin中最后要倒过来!之后经常会遇见这样的操作。还有就是data2bin中要补齐偶数个字节,这是掉坑里后发现的。
读出CCL后,根据算法,按照pk的要求需要交换ccl序列如《原文》的下图:
好,我们进行置换,恢复初始ccl序列:
# 交换ccl,并顺便按二进制取值
def changeccl(ccl):
ccl=[ccl[3],ccl[17],ccl[15],ccl[13],ccl[11],ccl[9],ccl[7],ccl[5],ccl[4],ccl[6],ccl[8],ccl[10],ccl[12],ccl[14],ccl[16],ccl[18],ccl[0],ccl[1],ccl[2]]
for i in range(len(ccl)):
ccl[i]=int(ccl[i][::-1],2) # 还是要倒叙,并取值
return(ccl)
ccl=changeccl(ccl) # 置换ccl
0x2、构造二级Huffman码表3
接着我们根据CCL整数序列构造出等价于CCL的二级Huffman码表3,也就是用ccl序列生成Huffman树3:
deflatetree3=lzq_deflate.mydeflate(ccl) # 返回的是一个字典类型
这里为了方便管理,我专门编了一个动态哈夫曼算法来生成zip专用的哈夫曼树,命名为lzq_deflate.py,内容如下:
# coding : utf-8
# author : 泉中流
# function : 生成zip中的哈夫曼树(定制的huffman树,右边深度优先)
# date : 2021.3.10,最后更新时间:2021.3.13
# 生成zip中的哈夫曼树对应的编码字典
def mydeflate(ccl):
MAX_BITS=max(ccl) # ccl中的最大值
bl_count=[0] # 存储ccl中每个码长值的个数,bl_count的索引值对应码长值!
for i in range(1,MAX_BITS+1): # 不统计ccl中0的个数,因为用不着,上句直接赋0
bl_count.append(ccl.count(i))
#bl_count=[0,0,2,3,0,4] # 统计ccl中各个码长值的个数,例如得到0个1,2个2,3个3,0个4,4个5
#print("bl_count",bl_count)
# 算出下一个码长值的初始编码,例如码长值为5的初始编码为28(11100)
code = 0
bl_count[0]=0
next_code=[0] # 索引值为0的第一个值强制为0,这个编码时用不到!
for bits in range(1,(MAX_BITS+1)):
code = (code + bl_count[bits-1]) << 1 # 下一个编码要给当前编码的个数留出足够空间!
next_code.append(code)
# 从next_code中获取哈夫曼树编码
deflatetree={}
# print("解码得到哈夫曼树编码(定制的huffman树):")
next_code_org=next_code.copy() #备份一下next_code,此句可以不要
for index in range(len(ccl)):
if(ccl[index]!=0): # ccl数据中的0没有用,所以不编码
tmp=str(bin(next_code[ccl[index]]))[2:]
if len(tmp)<ccl[index]:
tmp="0"*(ccl[index]-len(tmp))+tmp # 如果编码位数不够,前面补“0”
deflatetree.update({tmp:index})
next_code[ccl[index]]+=1 # 下一个相同码长值编码要在初始编码值上加1
return(deflatetree)
这段代码我是根据官方文档中的代码片段改写的,核心算法为:code = (code + bl_count[bits-1]) << 1,说实话我基本读懂了,但是我写不出来。
0x3、解码CL1、CL2比特流
继续,根据上面得到的二级Huffman码表3对CL1、CL2比特流进行解码,用以得到SQ1和SQ2整数序列(因sq1和sq2的算法完全相同,所以这里只列出了sq1):
curpos+=17+(HCLEN+4)*3 # curpos当前位置为cl1序列的起始位置
# 读二进制数据,还原sq1
sq1,tmppos=lzq_sqencode.scansqbin(bindata[curpos:],deflatetree3,HLIT+257)
这里调用了我编写的zip专用游程编码算法文件 lzq_sqencode.py,调用的内容如下:
# coding : utf-8
# function: zip中的游程编码的解码
# author : lzq2000 泉中流
# date : 2021.3.7 最后更新日期 2021.3.14
# 注意,zip中的游程编码是用于编码由纯数字(数值为0-15)组成的字符串,目的是压缩连续相同的值
# 待编码: 4,4,4,4,4, 3,3,3, 6,6,6,6,6,6,6,6,6,6,6,6,6, 6,6, 0,0,0,0,0,0, 2,2,2,2, 7
# 编码结果:4,16,1 3,3,3 6,16,3,16,3 6,6 17,3 2,16,0 7
# 用16这个特殊的数表示重复出现3、4、5、6个这样一个游程,分别后面跟着00、01、10、11表示
# 用17和18编码连续出现的“0”,17编码3-10个“0”,18编码11到138个“0”
# 另外:实际存储的时候需要低比特优先存储,需要把比特倒序来存
# 根据哈夫曼树(字典)扫描二进制字符串,分解为游程编码字符串列表(返回字典中的值)
def scansqbin(binstr,dict,cllen): # 注意只还原cllen长度!!!
getlen=0 # 已还原长度,注意是游程编码扩展后长度!
resultlist=[]
spos,epos=0,1 # spos是二进制字符串开始位置,epos是二进制字符串结束位置
while epos<=len(binstr):
if binstr[spos:epos] in dict:
resultlist.append(dict[binstr[spos:epos]])
if dict[binstr[spos:epos]]==17:
resultlist.append(int(binstr[epos:epos+3][::-1],2)) # 需要倒序
getlen+=int(binstr[epos:epos+3][::-1],2)+3 # 0的个数
spos=epos+3
epos=spos+1
elif dict[binstr[spos:epos]]==18:
resultlist.append(int(binstr[epos:epos+7][::-1],2))
getlen+=int(binstr[epos:epos+7][::-1],2)+11 # 0的个数
spos=epos+7
epos=spos+1
elif dict[binstr[spos:epos]]==16: # 重复字符标志
resultlist.append(int(binstr[epos:epos+2][::-1],2))
getlen+=int(binstr[epos:epos+2][::-1],2)+3 # 重复字符的个数
spos=epos+2
epos=spos+1
else:
spos=epos
epos=spos+1
getlen+=1
if getlen==cllen: # 还原了cllen个字符,返回结果和下一个待解压二进制字符的位置
return(resultlist,spos)
elif getlen>cllen:
print("zip文件格式错误1!")
else:
epos+=1
print("zip文件格式错误2!")
###return(resultlist,epos) # 应该不会运行到这里
# 功能:游程解码
def sqdecode(orglist):
j=0 # 计数器,用于计数编码是否取完整
resultlist=[] # 最终结果列表
tmplist=[] # 待解析单编码列表
while j<len(orglist):
if(orglist[j]<16): # 普通字符
i=j
sum=1
tmpstr=orglist[j]
tmplist=[]
while i<len(orglist):
if(i>=len(orglist)-1):
break
if(orglist[i+1]==16): # 重复的非0字符串
i+=2
sum+=orglist[i]+3
else:
break
tmplist+=[tmpstr]*sum
j=i+1
elif(orglist[j]==17):
tmplist=[0]*(orglist[j+1]+3)
j+=2
elif(orglist[j]==18):
tmplist=[0]*(orglist[j+1]+11)
j+=2
else:
return([-1])
resultlist+=tmplist
return(resultlist)
这样就得到了SQ1和SQ2整数序列。
0x4、解码SQ1、SQ2整数序列
根据上面得到的SQ1和SQ2整数序列,我们利用游程编码规则得到等价的CL1整数序列、CL2整数序列,代码如下:
cl1=lzq_sqencode.sqdecode(sq1) # 还原cl2序列
这里同样调用了上面算法文件lzq_sqencode.py中的函数,而且因为算法相同,只列举了cl1,这样就完成了cl1和cl2整数序列的解码工作。
0x5、构造两个一级哈夫曼码表
这两个一级哈夫曼表就是literal/length码表和distance码表,《原文》中有详细的讲解!
deflatetree1=lzq_deflate.mydeflate(cl1) # 还原哈夫曼树1,存储的是literal/length码表
deflatetree2=lzq_deflate.mydeflate(cl2)# 还原哈夫曼树2,存储的是Distance码表
这里调用的同样是前面我写的lzq_deflate.py算法中的mydeflate函数。
这样我们就还原了两个哈夫曼树,也就是说,我们成功地解码出来了literal/length码表和Distance码表。
0x6、解码LZ压缩数据
再继续,我们根据两个哈夫曼树(huffman)码表解码LIT bits和DIST bits数据,生成lz77编码流(即literal/length/distance编码流)。
# 根据两个哈夫曼树(huffman)码表解码LIT bits和DIST bits数据,生成lz77编码
def decodeLIT_DIST(binstr,deflatetree1,deflatetree2):
minlen=len(list(deflatetree1.keys())[0])
for item in deflatetree1.keys(): # list(deflatetree1.keys())
if len(item)<minlen:
minlen=len(item) #得到哈夫曼树1的最短字符串的长度
disminlen=len(list(deflatetree2.keys())[0])
for item in deflatetree2.keys(): # list(deflatetree2.keys())
if len(item)<disminlen:
disminlen=len(item) #得到哈夫曼树2的最短字符串的长度
resultlist=[]
spos,epos=0,minlen # spos是二进制字符串开始位置,epos是二进制字符串结束位置
while epos<=len(binstr):
mykey=binstr[spos:epos]
if mykey in deflatetree1:
myvalue=deflatetree1[mykey] # key对应的数值
if myvalue==256: # 256则解码结束,返回结果resultlist
return(resultlist,epos)
if myvalue<256:
resultlist.append(chr(myvalue)) # 数值转为字符存入结果列表
if myvalue>256:
extendbits,lengthbase=lengthextend(myvalue)
#print(extendbits,lengthbase)
if extendbits>0:
mylength=int(binstr[epos:epos+extendbits][::-1],2)+lengthbase # 需要倒序
epos+=extendbits
else:
mylength=lengthbase
spos=epos
epos+=disminlen # deflatetree2中最短搜这么长
while epos<=len(binstr):
mydiskey=binstr[spos:epos]
if mydiskey in deflatetree2:
mydisvalue=deflatetree2[mydiskey]
extendbits2,distancebase=distanceextend(mydisvalue)
if extendbits2>0:
mydistance=int(binstr[epos:epos+extendbits2][::-1],2)+distancebase
epos+=extendbits2
else:
mydistance=distancebase
break
else:
epos+=1
resultlist+=["("+str(mydistance)+","+str(mylength)+")"]
spos=epos
epos=spos+minlen
else:
epos+=1
解码得到literal/length/distance流,上面代码中涉及到两个码表的创建如下面两图(下面两图引用自《原文》,文中有详细讲解):
literal/length图,代码如下:
# 计算length扩展位数,进而求出对应length值
def lengthextend(code): # 输入的code必须大于256,小于286
extendbits=-1 # code输入错误
#baselist=[11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227] #length的起始值
lengthbase=0
baselist =[11]
for i in range(1,20):
baselist.append(baselist[i-1]+(2**((i-1)//4+1))) # 从baselist中的第二个元素开始,都是从上一个元素计算而来
if 256<code<286:
extendbits=0
lengthbase=code-254
if 264<code<285:
extendbits=(code-265)//4+1
lengthbase=baselist[code-265] #
if code==285:
lengthbase=258
return(extendbits,lengthbase) # extendbits是扩展位数,lengthbase是length的起始值
distance图,代码如下:
# 计算distance扩展位数,进而求出对应distance值
def distanceextend(code): # 输入的code必须大于等于0,小于30
extendbits=-1 # code输入错误
#baselist=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577]
distancebase=0
if 0<=code<30:
extendbits=0
distancebase=code+1
if 3<code<30:
extendbits=(code-4)//2+1
distancebase=(2**(extendbits+1))+(2**extendbits)*(code%2)+1
return(extendbits,distancebase)
到这里我们就只差最后一步,第7步的lz77解码工作了!
2、静态哈夫曼算法
有的数据块会是静态哈夫曼算法,这时Header中的算法标识是"01",我们也得进行解码,但因为静态哈夫曼算法是官方给定的编码表,不需要在zip文件的数据块中存储任何码表,所以对应的数据块结构不再是上面那个《原文》的数据块结构图了,而是Header的三个二进制位后面紧跟着就是LIT或者DIST编码流。
当然,构造静态哈夫曼树也非常简单,我们不需要像动态哈夫曼算法的1~5步,我们只需要下面这一步即可,根据官方文档编写代码如下:
# 生成静态哈夫曼的哈夫曼树,两个
def staticdeflatetree():
deflatetree1={} #静态哈夫曼的哈夫曼树1
for item in range(144):
key="%08d"%(int(bin(item+0b110000)[2:]))
deflatetree1.update({key:item})
for item in range(256-144):
key="%09d"%(int(bin(item+0b110010000)[2:]))
deflatetree1.update({key:item+144})
for item in range(280-256):
key="%07d"%(int(bin(item+0b0)[2:]))
deflatetree1.update({key:item+256})
for item in range(288-280):
key="%08d"%(int(bin(item+0b11000000)[2:]))
deflatetree1.update({key:item+280})
deflatetree2={} #静态哈夫曼的哈夫曼树2
for item in range(32):
key="%05d"%(int(bin(item)[2:]))
deflatetree2.update({key:item})
return(deflatetree1,deflatetree2)
构造好静态哈夫曼树后,接下来就该进入到和动态哈夫曼算法相同的第6步了!!
3、非压缩数据算法
当Header中的算法标识为"00"时,就说明此数据块里存储的是非压缩数据,数据块的结构又不同了,官方文档中说:Header所在的字节后面紧跟着的就是记录非压缩数据所占的字节数(LEN)的两个字节,再之后是LEN的补码(NLEN),也是两个字节,然后跟着的就是非压缩数据了。解码如下:
elif Header[1:3][::-1]=="00": # "00",是非压缩数据
print("zip压缩算法:",Header[1:3][::-1],"非压缩数据!")
# 非压缩部分
# 本部分位字节操作,要算出当前位置curpos在二进制序列中的位置对应在一个字节中的第几位
mypos=curpos%8 # mypos是当前位在一个字节中的位置
#print("mypos: ",mypos)
if mypos>5: # 因Header占三位,所以当前位置如果大于5,又夸了一个字节
curpos+=16-mypos
else:
curpos+=8-mypos # 越过Header所在字节
# datalen是非压缩数据的字节数,即RFC1951文档中的LEN
datalen=int(bindata[curpos:curpos+8][::-1],2)+int(bindata[curpos+8:curpos+16][::-1],2)*256
curpos+=32 # 越过LEN和NLEN
for x in range(datalen):
lz77.append(chr(int(bindata[curpos+8*x:curpos+8*x+8][::-1],2)))
curpos+=datalen*8 # 继续进入下个数据块
这里解释一下,因为非压缩数据是原样存储进来的,那么本来数据应该从data字节序列中读取,但是因为实现的比较仓促,我就直接从bindata二进制序列中读取了,而且因为bindata当初先把二进制倒序了,这里还需要重新倒序回去,还要每8位一读,再取值,再转字符串,这太麻烦太低效了,这里只是为了实现解压zip的示例,大家记得要改成直接从data中读取!!
另外之前我也说了,这种data一次性读入所有数据的方式和一次性转换为bindata二进制位的方式也非常不科学,对于大文件非常不好,大家如果想优化,可以尝试从zip文件中每次读入足够的字节逐步进行解码即可!
好了,非压缩数据还原出来了。这个数据将和其他算法得出的数据一起进入第7步,也是最后一步,加油,卡忙!
4、0x7,第7步lz77解码
上面的工作将三种算法的所有解码数据汇总为一个lz77编码流,我们最终要对这个编码流进行解码!我写了一个lz77的解码算法,其中包括utf-8解码,如果zip文件中压缩了包含中文等编码的字符,需要进行utf-8解码,否则会乱码,算法内容如下:
# encoding:utf-8
# 功能 : lz77压缩算法和解压算法编码(支持中文等编码)
# author: lzq2000 泉中流
# date : 2021.2.20,最后更新时间:2021.3.23
# lz77的压缩思想为:向前寻找与本字符串相同的字符串的位置,然后用相对距离和长度(相同的字符数)来简化存储!
# 为了提高效率,用到滑动窗口的方法,就是说向前搜索的范围是一个有限范围,大小固定,但是会不断向后滑动!
# 滑动窗口字节宽度,一般为4KB
swlen=32768 #8 #32768
# 前向搜索缓冲字节宽度,一般不超过100B,可以为32B
abuf=32 #4 #32
# 在滑动窗口(mystr)中按位置和长度取子字符串
def searchstr(mystr, mypos, mylen):
if mypos>0:
if mylen==mypos: # 长度等于距离,切片后面不能写数字!!!
return mystr[-1*mypos:]
elif mypos<mylen: # 长度大于距离。另外:对应lz77编码也要加上这段对应方法!lzq2000 20210315
return mystr[-1*mypos:]*(mylen//mypos)+mystr[-1*mypos:][0:(mylen%mypos)] # 循环重复距离到最后的几位
else:
return mystr[-1*mypos:mylen-mypos]
# lz77解压算法,也可以解压lz
def unlz77(lzlist):
result = [] # 解码结果
winside = 0 # 滑动窗口右边界
slidewin = "" # 滑动窗口,注意我这里的滑动窗口一开始的宽度为“0”!!!
for item in lzlist:
if len(item)==1:
result+=[ord(item)]# 转换为数值
winside+=1 # 滑动窗口右边界加1
if winside>=swlen:
slidewin = result[winside-swlen:winside] # 滑动窗口向右滑动一个字符
else:
slidewin = result[0:winside] # 增加滑动窗口
elif item[0]=="(" and item[-1]==")" and item.count(",")==1: # 有重复字符串,但最后没有字符,所以是二元组
myitem=item[1:len(item)-1].split(",")
result+=searchstr(slidewin, int(myitem[0]), int(myitem[1])) # myitem[0]是重复字符串距离,myitem[1]是长度
winside+=int(myitem[1])
if winside>=swlen:
slidewin = result[winside-swlen:winside] # 滑动窗口向右滑动一个字符
else:
slidewin = result[0:winside] # 增加滑动窗口
elif item[0]=="(" and item[-1]==")" and item.count(",")==2: # 有重复字符串,且最后有字符,所以是三元组
myitem=item[1:len(item)-1].split(",")
result+=searchstr(slidewin, int(myitem[0]), int(myitem[1]))+[ord(myitem[2])] # myitem[2]是下一个字符
winside+=int(myitem[1])+1 # 滑动窗口右边界加上重复字符串长度再加上下一个字符的长度(1)
if winside>=swlen:
slidewin = result[winside-swlen:winside] # 滑动窗口向右滑动一个字符
else:
slidewin = result[0:winside] # 增加滑动窗口
else:
return("lz77压缩列表错误,无法解压!")
return(result)
最后我们将解压出来的数据以字节的方式存储到文件中:
unzipdata+=lzq_lz77.unlz77(lz77) # 循环加入已解压数据
file=open(sys.path[0]+"/"+filename,"wb") # 压缩文件的文件名!
file.write(bytes(unzipdata))
file.close()
到此为止,我们就完成了这个zip文件的全部解压工作,感谢您耐心的观看!
三、总结
本文解压zip文件一共用到四个源代码文件如下:
lzq_zip.py # 解压主文件
lzq_deflate.py # 用于还原哈夫曼树的码表
lzq_sqencode.py # 用于解码zip专用的游程编码
lzq_lz77.py # 用于解码lz77压缩编码
这四个文件共同完成了整个zip文件(基于lz77)的解压工作,程序可以解码动态哈夫曼算法、静态哈夫曼算法和非压缩数据,可以解压包含中文等编码的utf-8字符,单目前不支持多压缩文件以及包括目录的解压。
缺点是解码数据流时的一次性读取所有数据是一个缺陷,而且一次性转换为二进制位数据流更是不可取,如果大家需要用到这些代码,请按自己的需求进行改进!另外,程序的健壮性很差,本程序是本人的学习笔记,只是起到抛砖引玉的作用。本人语文很一般😂,写文章是我的弱项,最后十分感谢大家的围观和支持!谢谢!!!
附上python3全手工解压zip文件源代码地址(编码不易,收费4.99元,以资鼓励😂):
https://download.csdn.net/download/weixin_37581211/16175968
(最后编辑于:2021.3.29,by 泉中流(lzq2000) )