我们最近有遇到了一些这样的一个问题,有些.zip压缩包在Windows下直接打开是空的,但是通过一些第三方的压缩软件打开(比如7zip等),则是完全正常的。
通过一番调研和实验,我们发现,对于.zip这样如此常见的文件,在它数十年的发展历程中除了经历过前苏联的阶梯,伊拉克战争,.com的泡沫,次贷危机,互联网的繁荣与新冠疫情之外,自身的标准和实现也有过很多的变化,不同时代第三方软件也各自以自己对当时标准的理解做了不同的实现,所以滚滚的历史洪流依然可以给生活在今天我们冲来看似相同实则实现方式各种各样的.zip文件。而Windows自然是不屑支持这各种各样的实现的,所以也就让我们看到了这些空空如也的压缩包。
如前所述导致Windows下打开为空.zip包的原因并不唯一,下面列举一下我们在调研中发现的两种。
以/开头的路径
在zip的标准中,/
是不可以作为路径的开头的,但是这并不意味着我们不能把/
作为路径的前缀写到.zip文件中。如果文件路径是以/
开头的,那么在Windows中打开这个.zip文件时,就无法显示这个路径。
我们可以通过下面的方式,生成一个路径以/
开头的.zip文件
import zipfile
with zipfile.ZipFile('slash_prefix.zip', 'w') as zip_handle:
zip_handle.writestr('/test.txt', b'')
注意,即使使用7zip打开这个文件,7zip会认为这个文件的路径是一个以空的目录。
文件路径中的特殊字符
如果.zip包,文件路径中包含中文等CJK字符,可以通过UTF-8对这些文件路径编码,而以UTF-8编码则需要通过flag bits中的第11位设置为1来声明。
但是一些软件还是会生成一些.zip包,其中的一些文件没有对flag bits进行合适的设置,但是文件路径中依然包含中文字符等。这样就会导致Windows在打开.zip包时,把文件名解析成乱码,而如果这些乱码包含一些特殊字符,就会进一步导致Windows不能显示这些路径,从而导致空.zip包的出现。
我们可以通过如下的例子,以这种方式生成一个在Windows打开为空的文件:
import zipfile
def _encodeFilenameFlags(self):
return self.filename, self.flag_bits
zipfile.ZipInfo._encodeFilenameFlags = _encodeFilenameFlags
zip_handle = zipfile.ZipFile('encoding.zip', 'w')
info = zipfile.ZipInfo()
info.flag_bits = 0
info.filename = '包.txt'.encode('utf-8')
zip_handle.writestr(info, b'')
zip_handle.close()
可以看到,其实上面的代码生成的文件中,主要就是这个“包”字对应的编码造成了空文件夹的展示。
文件转换
对于存在上述的问题的文件,解决方法实际上也就是针对各自的文件进行转换,把转换好的文件重新存入一个新的zip包就可以了。一些zip包修复工具,实际上也是采取类似的方法来处理的。
比如,我们可以通过如下的Python函数对文件进行转换,就可以在Windows中正常打开了:
import zipfile
def convert(filename, new_filename):
f = zipfile.ZipFile(filename)
nf = zipfile.ZipFile(new_filename, mode='w')
for zip_info in f.infolist():
if not zip_info.flag_bits & 0x800:
filename = zip_info.filename.encode('cp437').decode('utf-8')
else:
filename = zip_info.filename
content = f.read(zip_info.filename)
new_info = zipfile.ZipInfo()
new_info.filename = filename.lstrip('/')
new_info.date_time = zip_info.date_time
new_info.flag_bits = zip_info.flag_bits | 0x800
new_info.extra = zip_info.extra
new_info.internal_attr = zip_info.internal_attr
new_info.external_attr = zip_info.external_attr
new_info.file_size = len(content)
nf.writestr(new_info, content)
f.close()
nf.close()