zipfile 优雅地正确解压压缩包内中文文件名文件

背景

使用 Python 标准库 zipfile 解压压缩包,但压缩包中含中文文件名文件。

此时无论使用 ZipFile.extract 还是 ZipFile.extractall 解压都会导致解压出来的中文文件名变成 乱码

解决方案

通过查看 ZipFile 源码可以发现其是在 __init___RealGetContents 方法对编码进行了处理

摘抄该部分代码出来如下:

if flags & 0x800:
    # UTF-8 file names extension
    filename = filename.decode('utf-8')
else:
    # Historical ZIP filename encoding
    filename = filename.decode('cp437')
# Create ZipInfo instance to store file information
x = ZipInfo(filename)
x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH])
x.comment = fp.read(centdir[_CD_COMMENT_LENGTH])
x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET]
(x.create_version, x.create_system, x.extract_version, x.reserved,
 x.flag_bits, x.compress_type, t, d,
 x.CRC, x.compress_size, x.file_size) = centdir[1:12]
if x.extract_version > MAX_EXTRACT_VERSION:
    raise NotImplementedError("zip file version %.1f" %
                              (x.extract_version / 10))
x.volume, x.internal_attr, x.external_attr = centdir[15:18]
# Convert date/time code to (year, month, day, hour, min, sec)
x._raw_time = t
x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F,
                t>>11, (t>>5)&0x3F, (t&0x1F) * 2 )

x._decodeExtra()
x.header_offset = x.header_offset + concat
self.filelist.append(x)
self.NameToInfo[x.filename] = x

显然,要处理中文乱码只需要将 cp437 编码转换为 gbk 编码即可

然后我们查看 ZipFile.extract 还是 ZipFile.extractall 的源码, debug 发现跟进去发现其就是利用 filename 这个字段解压的

综上,解决中文乱码的思路揪出来了,在不改变 zipfile 源码的情况,我们可以选择创建一个新的 class 重写 zipfile 指定地方源码或者

使用补丁的方式更改 zipfile.ZipFile 实例指定属性

在这里,我选择的是定义一个函数对 zipfile.ZipFile 实例进行打补丁

源码如下:

import zipfile

def zipfile_support_gbk(zip_file: zipfile.ZipFile):
    """
    补丁函数,使得 zipfile 支持中文 gbk 编码
    :param zip_file:
    :return:
    """
    name_to_info = zip_file.NameToInfo
    # 这里 list 相当于深拷贝了 name_to_info 字典的 key
    # 避免了遍历字典时操作字典情况
    voice_li = list(name_to_info.keys())
    for voice_name in voice_li:
        real_voice_name = voice_name.encode('cp437').decode('gbk')
        info = name_to_info.pop(voice_name)
        info.filename = real_voice_name
        name_to_info[real_voice_name] = info
Table of Contents