namelist() 返回编码无效的字符串

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

问题是,对于上传到 python 应用程序的某些档案或文件,

ZipFile
namelist()
返回错误解码的字符串。

from zip import ZipFile
for name in ZipFile('zipfile.zip').namelist():
    print('Listing zip files: %s' % name)

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

我已经看到了Python 2的一些示例,但是由于python3中字符串的性质发生了变化,我不知道如何重新编码它,或者在它上面应用chardet。

string python-3.x unicode encoding chardet
4个回答
10
投票

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

自动?你不能。基本 ZIP 文件中的文件名是没有附加编码信息的字节字符串,因此除非您知道创建 ZIP 的计算机上的编码是什么,否则您无法可靠地获取人类可读的文件名。

现代 ZIP 文件上有一个标志扩展名,告诉您文件名是 UTF-8。不幸的是,您从 Windows 用户收到的文件通常没有它,因此您只能使用 chardet 等本质上不可靠的方法进行猜测。

我已经看到了Python 2的一些示例,但是由于python3中字符串的性质发生了变化,我不知道如何重新编码它,或者在它上面应用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')
    ...

7
投票

这是根据 仅支持 cp437 和 utf-8 字符编码的 zip 规范

解码 
zipfile.py 中的文件名的代码:

        if flags & 0x800:
            # UTF-8 file names extension
            filename = filename.decode('utf-8')
        else:
            # Historical ZIP filename encoding
            filename = filename.decode('cp437')

如您所见,如果未设置

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

实际上,可以使用 ANSI 或 OEM Windows 代码页代替 cp437。

如果您知道实际的字符编码,例如,

cp866
(OEM(控制台)代码页)可能在俄语 Windows 上使用,那么您可以重新编码文件名以获得原始文件名:

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>..`

2
投票

遇到同样的问题,但是使用定义的语言(俄语)。

  1. 最简单的解决方案就是使用此实用程序进行转换:https://github.com/vlm/zip-fix-filename-encoding 对我来说,它适用于 98% 的档案(无法运行 11388 个语料库中的 317 个文件)

  2. 更复杂的解决方案:使用 python 模块 chardet 和 zipfile。但这取决于您使用的 python 版本(2 或 3) - 它在 zipfile 上有一些差异。对于 python 3 我写了一段代码:

    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)
    

    此代码尝试使用旧式 zip(具有编码 CP437,只是已损坏),如果失败,则 zip 存档似乎是新式(UTF-8)。确定正确的编码后,您可以通过以下代码提取文件:

    from shutil import copyfileobj
    fp = archive.open(original_name)
    fp_out = open(name, 'wb')
    copyfileobj(fp, fp_out)
    

就我而言,这解决了最后 2% 的失败文件。


0
投票

从 Python 3.11 开始,只需在创建

metadata_encoding='utf-8'
时传递
ZipFile
:

with ZipFile('some-archive.zip', metadata_encoding='utf-8') as zip:
    print(zip.namelist())

如 zipfile 源代码中所示,这将跳过整个旧版

cp437
解码: https://github.com/python/cpython/blob/3.11/Lib/zipfile.py#L1420 https://github.com/python/cpython/blob/3.11/Lib/zipfile.py#L1603

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