来自ZipFile的namelist()返回具有无效编码的字符串。

9
问题在于对于一些上传到Python应用程序的存档或文件,ZipFilenamelist()返回的字符串解码不正确。
from zip import ZipFile
for name in ZipFile('zipfile.zip').namelist():
    print('Listing zip files: %s' % name)

如何修复该代码,以便始终以Unicode解码文件名(从而支持中文,俄语和其他语言)?

我看过一些Python 2的样例,但由于Python3中字符串的性质已经改变,我不知道如何重新编码它,或者在其上应用chardet。


这个链接https://dev59.com/6krSa4cB1Zd3GeqPXIh1 可能会给你一些答案,特别是第二个回答。 - Kush
无论这里的答案中人们发布了多少解决方法,它们都不是真正可靠的解决方案,并且没有解释其他语言中的程序如何处理该问题。此外,Python软件包开发人员也没有提供任何信息。 - Croll
3个回答

10

如何修复代码,以便始终使用Unicode解码文件名(以支持中文,俄语和其他语言)?

自动化地进行?不可能。基本ZIP文件中的文件名是一系列字节字符串,没有附加的编码信息,因此除非您知道创建ZIP的计算机上使用的编码方式,否则无法可靠地获取可读的文件名。

现代ZIP文件的标志有一个扩展来告诉你文件名是UTF-8编码。但不幸的是,您从Windows用户那里收到的文件通常都没有这个标志,所以您只能使用本质上不可靠的方法(如chardet)猜测。

我看到一些Python 2的示例,但由于Python 3中字符串的性质已经改变,我不知道如何重新编码它或对其应用chardet。

在Python 2中,它只会返回原始字节。而Python 3的新行为是:

  • 如果设置了UTF-8标志,则使用UTF-8解码文件名并将正确的字符串值返回。

  • 否则,它使用DOS代码页437对文件名进行解码,这很可能不是预期的结果。然而,您可以将字符串重新编码回原始字节,然后尝试使用您实际想要的代码页再次解码,例如name.encode('cp437').decode('cp1252')

不幸的是(再次强调,因为涉及ZIP文件时不幸的事情永远不会停止),ZipFile在不告诉你它所做的情况下静默地进行这种解码。因此,如果要切换并仅在文件名可疑时执行转码步骤,则必须复制嗅探UTF-8标志是否设置的逻辑:

ZIP_FILENAME_UTF8_FLAG = 0x800

for info in ZipFile('zipfile.zip').filelist():
    filename = info.filename
    if info.flag_bits & ZIP_FILENAME_UTF8_FLAG == 0:
        filename_bytes = filename.encode('437')
        guessed_encoding = chardet.detect(filename_bytes)['encoding'] or 'cp1252'
        filename = filename_bytes.decode(guessed_encoding, 'replace')
    ...

我想指出,我曾经遇到过来自Mac OS X的ZIP文件,它将文件列表编码为utf-8,但却忘记设置标志。 - beruic
谢谢!当文件名使用cp949编码时,这对我的情况起作用了。只需使用:name.encode('cp437').decode('cp949')就可以了! - Maritza Esparza

7
以下是解码 zipfile.py 中文件名的代码,根据zip规范,只支持cp437和utf-8字符编码
        if flags & 0x800:
            # UTF-8 file names extension
            filename = filename.decode('utf-8')
        else:
            # Historical ZIP filename encoding
            filename = filename.decode('cp437')

如您所见,如果未设置0x800标志,即未在输入的zipfile.zip中使用utf-8,则会使用cp437,因此,“中文、俄语和其他语言”的结果可能不正确。

实际上,ANSI或OEM Windows代码页可能会被用于代替cp437。

如果您知道实际的字符编码,例如,在俄罗斯Windows上可以使用cp866(OEM(控制台)代码页),那么您可以重新编码文件名以获取原始文件名:

filename = corrupted_filename.encode('cp437').decode('cp866')

最佳选项是使用utf-8创建zip归档文件,这样您就可以在同一归档文件中支持多种语言:

c:\> 7z.exe a -tzip -mcu archive.zip <files>..

或者

$ python -mzipfile -c archive.zip <files>..`

这对我很有用: f = filename.encode('cp437').decode('cp866') 反之亦然: arch = archive.open( f.encode('cp866').decode('cp437') ) - Julian
也许可以尝试使用.encode('cp437').decode('gbk')来处理中文姓名。 - idailylife

1

我遇到了同样的问题,但是是在使用俄语时出现的。

  1. Most simple solution is just to convert it with this utility: https://github.com/vlm/zip-fix-filename-encoding For me it works on 98% of archives (failed to run on 317 files from corpus of 11388)

  2. More complex solution: use python module chardet with zipfile. But it depends on python version (2 or 3) you use - it has some differences on zipfile. For python 3 I wrote a code:

    import chardet
    original_name = name
    try:
        name = name.encode('cp437')
    except UnicodeEncodeError:
        name = name.encode('utf8')
    encoding = chardet.detect(name)['encoding']
    name = name.decode(encoding)
    

    This code try to work with old style zips (having encoding CP437 and just has it broken), and if fails, it seems that zip archive is new style (UTF-8). After determining proper encoding, you can extract files by code like:

    from shutil import copyfileobj
    fp = archive.open(original_name)
    fp_out = open(name, 'wb')
    copyfileobj(fp, fp_out)
    
在我的情况下,这解决了最后2%的失败文件。

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