Python zipfile 模块无法提取含有汉字的文件名

问题描述 投票:0回答:7

我正在尝试使用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”完美地访问/解压缩同一文件。

python python-2.7 unicode zip
7个回答
15
投票

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')

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'))

2
投票

这段代码怎么样?

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)

2
投票

这几乎晚了 6 年,但最终在 Python 3.11 中通过添加

metadata_encoding
参数修复了这个问题。无论如何,我在这里发布了这个答案,以帮助其他有类似问题的人。

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

将禁用解码文件名的尝试,并始终返回带有字节字符串

filename
属性的 ZipInfo,您可以继续以任何适当的方式手动解码/处理。


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")

0
投票

之前所有的解决方案都是狗屎。这是一个更好的:

改变

        with zipfile.ZipFile(file_path, "r") as zipobj:
            zipobj.extractall(path=dest_dir)
            print("Successfully extracted zip archive to {}".format(dest_dir))

至:

        with zipfile.ZipFile(file_path, "r") as zipobj:
            zipobj._extract_member = lambda a,b,c: _extract_member_new(zipobj, a,b,c)
            zipobj.extractall(path=dest_dir)
            print("Successfully extracted zip archive to {}".format(dest_dir))

其中

_extract_member_new
是:

def _extract_member(self, member, targetpath, pwd):
    """Extract the ZipInfo object 'member' to a physical
        file on the path targetpath.
    """
    import zipfile
    if not isinstance(member, zipfile.ZipInfo):
        member = self.getinfo(member)

    # build the destination pathname, replacing
    # forward slashes to platform specific separators.
    arcname = member.filename.replace('/', os.path.sep)
    arcname = arcname.encode('cp437', errors='replace').decode('gbk', errors='replace')

    if os.path.altsep:
        arcname = arcname.replace(os.path.altsep, os.path.sep)
    # interpret absolute pathname as relative, remove drive letter or
    # UNC path, redundant separators, "." and ".." components.
    arcname = os.path.splitdrive(arcname)[1]
    invalid_path_parts = ('', os.path.curdir, os.path.pardir)
    arcname = os.path.sep.join(x for x in arcname.split(os.path.sep)
                                if x not in invalid_path_parts)
    if os.path.sep == '\\':
        # filter illegal characters on Windows
        arcname = self._sanitize_windows_name(arcname, os.path.sep)

    targetpath = os.path.join(targetpath, arcname)
    targetpath = os.path.normpath(targetpath)

    # Create all upper directories if necessary.
    upperdirs = os.path.dirname(targetpath)
    if upperdirs and not os.path.exists(upperdirs):
        os.makedirs(upperdirs)

    if member.is_dir():
        if not os.path.isdir(targetpath):
            os.mkdir(targetpath)
        return targetpath

    with self.open(member, pwd=pwd) as source, \
            open(targetpath, "wb") as target:
        shutil.copyfileobj(source, target)

    return targetpath
© www.soinside.com 2019 - 2024. All rights reserved.