我正在尝试使用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 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')
最近我也遇到了同样的问题。这是我的解决方案。我希望它对你有用。
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'))
这段代码怎么样?
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)
这几乎晚了 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)
ZIP 文件无效。它有一个标志,表明其中的文件名被编码为 UTF-8,但实际上并非如此;它们包含作为 UTF-8 无效的字节序列。也许他们是GBK?也许还有别的事?也许是一些邪恶的不一致的混合物?不幸的是,野外的 ZIP 工具在一致地处理非 ASCII 文件名方面非常非常差。
一个快速的解决方法可能是替换解码文件名的库函数。这是一个猴子补丁,因为没有一种简单的方法可以将您自己的 ZipInfo 类注入 ZipFile,但是:
zipfile.ZipInfo._decodeFilename = lambda self: self.filename
将禁用解码文件名的尝试,并始终返回带有字节字符串
filename
属性的 ZipInfo,您可以继续以任何适当的方式手动解码/处理。
@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")
之前所有的解决方案都是狗屎。这是一个更好的:
改变
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