Python zipfile не распаковывает папки для zip-архива Windows

У меня есть zip-файл, который был создан на Windows машине с помощью этого инструмента System.IO.Compression.ZipFile (этот zip-архив содержит множество файлов и папок). У меня есть код Python, который работает на Linux машине (точнее, raspberry pi), который должен распаковать архив и создать все необходимые папки и файлы. Я использую библиотеки Python 3.5.0 и zipfile, это пример кода:

import zipfile

zip = zipfile.ZipFile("MyArchive.zip","r")
zip.extractall()
zip.close()

Теперь, когда я запускаю этот код, вместо того, чтобы получить красивое распакованное дерево каталогов, я получаю все файлы в корневом каталоге со странными именами, например Folder1\Folder2\MyFile.txt.

Я предполагаю, что, поскольку zip-архив был создан в Windows, а разделитель каталогов в Windows - \, тогда как в Linux это /, библиотека python zipfile рассматривает \ как часть имени файла, а не разделителя каталогов. Также обратите внимание, что когда я извлекаю этот архив вручную (не через код Python), все папки создаются должным образом, поэтому кажется, что это определенно проблема библиотеки zipfile. Еще одно замечание: для zip-архивов, созданных с помощью другого инструмента (не System.IO.Compression.ZipFile), он работает нормально, используя тот же код Python.

Есть какие-нибудь сведения о том, что происходит и как это исправить?


person Mykhailo Seniutovych    schedule 30.08.2018    source источник
comment
Zip-файлы хранят все в корне. Иерархия виртуально создается за счет косой черты в именах файлов.   -  person Mad Physicist    schedule 30.08.2018
comment
Покажите, как вы распаковываете архив вручную   -  person Mad Physicist    schedule 30.08.2018
comment
@MadPhysicist - это весь код на Python. Или о каком коде вы спрашиваете?   -  person Mykhailo Seniutovych    schedule 30.08.2018
comment
@MadPhysicist очень просто sudo unzip MyArchive.zip, и это прекрасно работает.   -  person Mykhailo Seniutovych    schedule 30.08.2018
comment
Попробуйте это: from os import path; path.altsep = '\\' где угодно, прежде чем позвонить extractall.   -  person Mad Physicist    schedule 30.08.2018
comment
Если сработает, опубликую ответ   -  person Mad Physicist    schedule 30.08.2018
comment
Спасибо! Помогло, можешь выложить, поставлю как ответ, если хочешь.   -  person Mykhailo Seniutovych    schedule 30.08.2018
comment
@MadPhysicist Хороший звонок. Я не заметил, что строка, следующая за строкой, которую я указал в своем ответе, использует os.path.altsep для замены.   -  person blhsing    schedule 30.08.2018


Ответы (2)


Происходит то, что, хотя Windows распознает как \ (path.sep), так и / (path.altsep) как разделители путей, Linux распознает только / (path.sep).

Как показывает ответ @blhsing, существующая реализация ZipFile всегда гарантирует, что path.sep и / считаются допустимыми символами-разделителями. Это означает, что в Linux \ рассматривается как буквальная часть имени файла. Чтобы изменить это, вы можете установить os.altsep в \, так как он проверяется, не является ли он пустым.

Если вы пойдете по пути изменения самого ZipFile, как предлагает другой ответ, просто добавьте строку, чтобы слепо изменить \ на path.sep, поскольку / всегда уже изменяется. Таким образом, /, \ и, возможно, path.altsep все будут преобразованы в path.sep. Это то, что делает инструмент командной строки.

person Mad Physicist    schedule 30.08.2018

Это действительно ошибка модуля zipfile, где он содержит следующую строку в ZipFile._extract_member(), чтобы слепо заменить '/' в именах файлов на разделитель путей, специфичный для ОС, когда он также должен искать '\\':

arcname = member.filename.replace('/', os.path.sep)

Вы можете исправить это, заменив ZipFile._extract_member() версией, которая напрямую скопирована из исходного кода, но с исправленной строкой выше:

from zipfile import ZipFile, ZipInfo
import shutil
import os
def _extract_member(self, member, targetpath, pwd):
    """Extract the ZipInfo object 'member' to a physical
       file on the path targetpath.
    """
    if not isinstance(member, ZipInfo):
        member = self.getinfo(member)

    if os.path.sep == '/':
        arcname = member.filename.replace('\\', os.path.sep)
    else:
        arcname = member.filename.replace('/', os.path.sep)

    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
ZipFile._extract_member = _extract_member
person blhsing    schedule 30.08.2018
comment
Это не имеет особого смысла, учитывая, что метод уже делает это. - person Mad Physicist; 30.08.2018
comment
Вместо этого исправлено условным оператором. - person blhsing; 30.08.2018
comment
Сохранение косой черты действительно имеет смысл. Реальным решением было бы передать altsep функции. - person Mad Physicist; 31.08.2018