Python的zipfile模块无法解压带有中文字符的文件名

9
我正在尝试使用Python脚本从中国服务提供商下载文件(我自己不是来自中国的)。该提供商提供给我一个.zip文件,其中包含一个似乎在名称中具有中文字符的文件。这似乎导致zipfile模块出现错误。

代码:

import zipfile

f = "/path/to/zip_file.zip"

if zipfile.is_zipfile(f):
    fz = zipfile.ZipFile(f, 'r')

这个zip文件本身不包含任何非ASCII字符,但是里面的文件包含。当我运行上面的脚本时,会出现以下异常:

Traceback (most recent call last):   File "./temp.py", line 9, in <module>
    fz = zipfile.ZipFile(f, 'r')   File "/usr/lib/python2.7/zipfile.py", line 770, in __init__
    self._RealGetContents()   File "/usr/lib/python2.7/zipfile.py", line 859, in _RealGetContents
    x.filename = x._decodeFilename()   File "/usr/lib/python2.7/zipfile.py", line 379, in _decodeFilename
    return self.filename.decode('utf-8')   File "/usr/lib/python2.7/encodings/utf_8.py", line 16, in decode
    return codecs.utf_8_decode(input, errors, True) UnicodeDecodeError: 'utf8' codec can't decode byte 0xbd in position 30: invalid start byte

我尝试查看了许多类似问题的答案:

请纠正我如果我错了,但看起来像是zipfile 模块存在一个已知问题

我该如何解决这个问题?是否有任何替代模块可用于处理 zip 文件?还是有其他解决方案?

TIA。

编辑: 我可以使用 Linux 命令行实用程序“unzip”完美地访问/解压缩相同的文件。

6个回答

14

Python 2.x(2.7)和Python 3.x处理模块zipfile中非utf-8编码文件名的方式略有不同。

首先,它们都会检查文件的ZipInfo.flag_bits,如果ZipInfo.flag_bits & 0x800,则使用utf-8解码文件名。

如果上述检查为False,在Python 2.x中,将返回名称的字节字符串;在Python 3.x中,模块将使用cp437编码解码文件并返回解码结果。当然,该模块不知道两个Python版本中文件名的真实编码。

因此,假设您从ZipInfo对象或zipfile.namelist方法获取了文件名,并且已经知道该文件名是用XXX编码的。以下是获取正确的Unicode文件名的方法:

# in python 2.x
filename = filename.decode('XXX')


# in python 3.x
filename = filename.encode('cp437').decode('XXX')

3
要了解哪个代码 XXX 对应于您的语言,请查看这里是 Python 2.4这里是 Python 3.x - maxhaz

7

最近我遇到了同样的问题。以下是我的解决方案,希望对你有用。

import shutil
import zipfile
f = zipfile.ZipFile('/path/to/zip_file.zip', 'r')
for fileinfo in f.infolist():
    filename = fileinfo.filename.encode('cp437').decode('gbk')
    outputfile = open(filename, "wb")
    shutil.copyfileobj(f.open(fileinfo.filename), outputfile)
    outputfile.close()
f.close()

更新:您可以使用以下更简单的解决方案,使用pathlib

from pathlib import Path
import zipfile

with zipfile.ZipFile('/path/to/zip_file.zip', 'r') as f:
    for fn in f.namelist():
        extracted_path = Path(f.extract(fn))
        extracted_path.rename(fn.encode('cp437').decode('gbk'))

1

@Mr.Ham的解决方案完美地解决了我的问题。我正在使用中文版Win10,其中文件系统的默认编码是GBK。

我认为对于其他语言的用户,只需将解码从GBK更改为其系统默认编码即可。而Python可以自动获取默认系统编码。

因此,修补后的代码如下:

import zipfile
import locale

default_encoding = locale.getpreferredencoding()

with zipfile.ZipFile("/path/to/zip_file.zip") as f:
    zipinfo = f.infolist()
    for member in zipinfo:
        member.filename = member.filename.encode('cp437').decode(default_encoding)
        # The second argument could make the extracted filese to the same dir as the zip file, or leave it blank to your work dir.
        f.extract(member, "/path/to/zip_file")

1

这已经晚了近6年,但是随着metadata_encoding参数的添加,Python 3.11终于解决了这个问题。我仍然在此发布答案,以帮助其他遇到类似问题的人。

import zipfile

f = "your/zip/file.zip"
t = "the/dir/where/you/want/to/extract/it/all"

with zipfile.ZipFile(f, "r", metadata_encoding = "utf-8") as zf:
    zf.extractall(t)

1
ZIP文件无效。它有一个标志,表明其中的文件名被编码为UTF-8,但实际上它们不是;它们包含的字节序列不是有效的UTF-8。也许它们是GBK?也许是其他东西?也许是一些混乱不一致的混合物?不幸的是,野外中的ZIP工具在处理非ASCII文件名方面非常非常差。一个快速的解决方法可能是替换解码文件名的库函数。这是一个猴子补丁,因为没有简单的方法将自己的ZipInfo类注入到ZipFile中,但:
zipfile.ZipInfo._decodeFilename = lambda self: self.filename

这将禁用对文件名的解码尝试,并始终返回一个ZipInfo对象,其中包含一个字节字符串filename属性,您可以手动解码/处理它,以适当的方式进行处理。


它有一个标志,表示其中的文件名编码为UTF-8。我从未听说过这个标志。在哪里可以找到它? - user824425
抱歉在我的问题中没有提到,但我可以使用Linux命令行实用程序“unzip”完美地访问/解压缩它。因此,我怀疑这是否是文件本身的问题。 - hyperwiser
1
@Rhymoid:文件属性标志字的第11位,请参见 PKware appnote 第4.1.4节:“如果设置了此标志位,则必须使用UTF-8编码对该文件的文件名和注释字段进行编码”。hyperwiser:通过任何一个工具来判断文件的有效性都是不明智的。确实,早于UTF-8标志的工具将会忽略它,同样,一个不关心编码的面向字节的工具也会忽略它。与ZIP的各种松散定义的边缘情况产生的工具的极端差异反应完全无关。 - bobince

1

这段代码怎么样?

import zipfile

with zipfile.ZipFile('/path/to/zip_file.zip', 'r') as f:
    zipInfo = f.infolist()
    for member in zipInfo:
        member.filename = member.filename.encode('cp437').decode('gbk')
        f.extract(member)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接