问题是,对于上传到 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。
如何修复该代码,以便我始终以 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')
...
这是根据 仅支持 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>..`
遇到同样的问题,但是使用定义的语言(俄语)。
最简单的解决方案就是使用此实用程序进行转换:https://github.com/vlm/zip-fix-filename-encoding 对我来说,它适用于 98% 的档案(无法运行 11388 个语料库中的 317 个文件)
更复杂的解决方案:使用 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% 的失败文件。
从 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