布景

写了一段代码,需求调用python的shutil规范库解压zip紧缩包,详细代码如下

import shutil
​
​
def unzip(self, src_path: str, dst_path: str):
  # shutil.unpack_archive("../README.md.zip", "../")
  # shutil.unpack_archive("../docxx.zip", "../")
  shutil.unpack_archive(src_path, dst_path)

成果发现解压后的文件名称呈现了乱码,可是文件内容是正常的,没有呈现乱码

三行python代码,永久消除linux解压zip包乱码

python代码的运转环境是ubnutu,发现运用unzip命令解压,也会呈现这个问题

$ unzip HEAP.zip
Archive:  HEAP.zip
  inflating: 20230329-SOC│╡╘╞╥╗╠╗▒-1.pptx 
  inflating: 20230330-EEA-╒√│╡╡╫╙╡╞╝▄╣╣ v1.pptx 
  inflating: 20230412-╬╩╠╨▐╕─.md 

而在windows环境下,就不会呈现这种问题,代码是正常的,而巧合的是这个紧缩包也是windows下打的,所以根本能够清晰,这是由于不同操作体系的默许编码不同导致的

问题处理

shutil规范库解压zip包,调用的是zipfile规范库,调用代码如下

def _unpack_zipfile(filename, extract_dir):
  """Unpack zip `filename` to `extract_dir`
   """
  import zipfile # late import for breaking circular dependencyif not zipfile.is_zipfile(filename):
    raise ReadError("%s is not a zip file" % filename)
​
  zip = zipfile.ZipFile(filename)
  try:
    for info in zip.infolist():
      name = info.filename
​
      # don't extract absolute paths or ones with .. in them
      if name.startswith('/') or '..' in name:
        continue
​
      target = os.path.join(extract_dir, *name.split('/'))
      if not target:
        continue
​
      _ensure_directory(target)
      if not name.endswith('/'):
        # file
        data = zip.read(info.filename)
        f = open(target, 'wb')
        try:
          f.write(data)
        finally:
          f.close()
          del data
  finally:
    zip.close()

其间问题就呈现在这行代码

      name = info.filename

读取了文件名,可是没有依照正确的编码格局进行解码,只需如下处理就能够处理这个问题

      if info.flag_bits & 0x800: # #utf-8 #编码
        name = info.filename
      else:
        try:
          # zipfile 默许运用 #cp437 编码 & #utf-8 编码
          name = info.filename.encode('cp437').decode('gbk') # gbk编码兼容ASCII
        except UnicodeDecodeError as e:
          name = info.filename

问题原因也很简单,获取文件名之后,没有依照windows下的编码格局,而是运用了cp437,所以呈现了乱码

info.flag_bits是一个标志位,其间的一位是用于判别是否运用utf-8编码,详解见下末节。

有的教程会教我们如何修正python规范库的源码,以处理这个问题,可是这是一种很危险的操作,不建议如此。

我选用的方案是经过shutil.unregister_unpack_format()shutil.register_unpack_format()办法动态的替换运转时解压zip包的函数。

show me code

完好代码如下

def _unpack_zipfile(filename, extract_dir):
  """Unpack zip `filename` to `extract_dir`
   """
  import zipfile # late import for breaking circular dependencyif not zipfile.is_zipfile(filename):
    raise shutil.ReadError("%s is not a zip file" % filename)
​
  zip = zipfile.ZipFile(filename)
  try:
    for info in zip.infolist():
      # name = info.filename# 支撑windows下的打得zip包 不会乱码 ==========================
      if info.flag_bits & 0x800: # #utf-8 #编码
        name = info.filename
      else:
        try:
          # zipfile 默许运用 #cp437 编码 & #utf-8 编码
          name = info.filename.encode('cp437').decode('gbk') # gbk编码兼容ASCII
        except UnicodeDecodeError as e:
          name = info.filename
      # ========================================================# don't extract absolute paths or ones with .. in them
      if name.startswith('/') or '..' in name:
        continue
​
      target = os.path.join(extract_dir, *name.split('/'))
      if not target:
        continue
​
      ensure_dir(target)
      if not name.endswith('/'):
        # file
        data = zip.read(info.filename)
        f = open(target, 'wb')
        try:
          f.write(data)
        finally:
          f.close()
          del data
  finally:
    zip.close()
​
​
shutil.unregister_unpack_format('zip')
shutil.register_unpack_format('zip', ['.zip'], _unpack_zipfile, [], "ZIP file")

原因剖析

知其然,还要知其所以然

zip(紧缩文件格局)是一种古老的规范,最早呈现在ibm的dos体系下,zip属于当前几种干流的紧缩格局之一。当年的dos不能像今日这样支撑unicode和utf-8编码,不同国家的电脑需求装置不同的代码页(code page),并只能兼容当地(国家/地区)的文字。在这种情况下,zip和dos相同,设计初期并没有考虑unicode统一编码的问题,所以紧缩时分会依照各个操作体系默许编码存储文件

现如今,跟着新的unicode和utf-8编码的兴盛,越来越多的体系开端支撑utf-8规范(这是一种能够支撑全球一切文字的编码方法)。zip中也增加了新的标志位,用来表示zip文件的紧缩编码是否是utf-8。但是,干流操作体系针对zip的紧缩功能代码年久失修,很多功能都没有遵从最新的zip规范,不同操作体系的文件体系对编码格局支撑不统一。如linux下默许不支撑gbk编码;windows操作体系的中文默许编码为gbk,而且至今windows 10依旧选用兼容代码页(code page)的方法判别体系语言,因此windows的zip紧缩会运用本地码紧缩(默许是gbk编码),而不会敞开utf-8标志位,但会运用zip一个特别的功能“zip拓展文件名字段”,并在拓展字段里运用“utf-8”编码的文件名;而macos操作体系虽然选用中文默许编码utf-8,但因为mac的代码页(code page)便是utf-8,所以紧缩的时分依照utf-8紧缩,且不会敞开utf-8标志位。而且,不同操作体系对大/小写文件名识别的方法也不一致,如linux下区分大小写,mac、windows下默许不区分大小写。

由于文件识别体系无法获悉即将解码的zip文件是由哪种体系编码的,也就无法提供与zip文件相匹配的解码方法。