Работа с ZIP архивами с помощью Python Python

Работа с ZIP архивами с помощью Python

Развитие информационных технологий (IT) и расширяющаяся потребность в сервисах по обработки данных приводит к тому, что вновь создаваемые программные продукты, как правило, должны иметь дело с огромными массивами разнообразной цифровой информации. Так, заявки на какую-либо электронную услугу, различные творческие отчеты, справки об имуществе или всякие иные документы со мульти-структурой «под капотом» зачастую содержат десятки файлов разных форматов, среди которых могут быть сканы изображений, файлы с видео или аудио фиксациями, тексты и специализированные файлов. Но, представьте, что будет если таких мульти-структурированных документов вашей программе ежедневно придется обрабатывать сотнями, а то и тысячами штук. В такой ситуации за короткое время работы с этой программой накопятся миллионы файлов и папок со сложнейшей иерархией, где вручную отобрать нужную информацию будет практически не возможно, а вероятность утери данных со временем будет только возрастать. Кроме того, огромное количество накапливаемых и не сжимаемых файлов при условии их передачи по компьютерным сетям в данной ситуации может стать просто критичным, поскольку будет чрезвычайно замедлять этот процесс.

Наряду с использованием возможностей баз данных, наиболее оптимальным вариантом решения вышеописанной проблемы является объединение файлов одного мульти-структурированного документа (той же заявки или отчета) в соответствующий контейнер – ZIP-файл.

Примечание: Использование ZIP-файлов нисколько не исключает параллельного применения с ними баз данных, что в совокупности позволяет существенно упростить структуру последних.

В Python все основные действия с вышеупомянутыми ZIP-файлами могут быть осуществлены с помощью стандартного библиотечного модуля zipfile, которому посвящена данная статья, где можно узнать о том, как:

  • Читать, записывать и извлекать файлы из ZIP-файлов с помощью модуля zipfile в Python.
  • Читать метаданные о содержимом ZIP-файлов с помощью zipfile
  • Использовать zipfile для управления файлами-членами в уже существующих ZIP-файлах.
  • Создавать новые ZIP-файлы для архивации и сжатия файлов.

Все эти знания помогут вам оптимизировать процессы обработки файлов, при создании тех или иных программ, работающих со мульти-структурными документами.

Но, следует учесть, что для получения максимальной отдачи от этой статьи, вы должны иметь представление об основах работы с файлами, использовании with инструкции и обработки путей для файловой системы с помощью pathlib, а также быть знакомы с объектно-ориентированным программированием и знать работать с классами.

Рекомендации: Ниже в этой статье приводятся многочисленные листинги с примерами, которые помогут вам лучше усвоить соответствующий материал. Но, так как все эти примеры используют дополнительные файлы, наглядно показывающие результаты от работы соответствующих листингов, то для продолжения изучения нижеследующего материала вам желательно предварительно скачать архив с данными файлами по этой ссылке.

Начало работы с ZIP-файлами

ZIP-файлы – являются хорошо зарекомендовавшем и общепризнанным инструментарием в современном цифровом мире. Так как поддержка ZIP-формата включена во множество современных операционных систем, данные файлы достаточно широко используются для межплатформенного обмена данными в компьютерных сетях, к которым относится и Internet.

Возможности ZIP-файлов вы можете использовать для объединения обычных файлов в единый архив, сжатия данных для экономии места на диске, распространения цифровых продуктов и многого другого. Но, в этой статье мы остановимся лишь на описании возможностей использования ZIP-файлов с помощью Python модуля zipfile.

Поскольку терминология, применяемая в отношении ZIP-файлов иногда может сбивать с толку, ниже в этой статье представлены разъяснения по основным, используемым здесь терминам:

Термин Значение
ZIP-файл, ZIP-архив или архив Физический файл, где структура данных соответствует ZIP формату
Файл Обычный компьютерный файл
Файл участник Файл, входящий в состав существующего ZIP-архива

Что такое ZIP-файлы?

Вряд ли кто-то из нас, кто пусть даже имеет поверхностное представление об IT, не сталкивался с обработкой ZIP-файлов. Файлы с расширением .zip практически есть в любой операционной системе в неимоверном количестве. Эти файлы, также известные как ZIP-архивы, хранят внутри себя данные в специализированном кросс-платформенном формате, предусматривающем возможность сжатия, и управления файлами, а также шифрования и обмена данными.

Для того чтобы архив считался ZIP-файлом, требование сжатие данных внутри его не является обязательным. Таким образом, ZIP-архивы могут содержать в себе, как сжатые, так и несжатые файлы участники. ZIP-формат может поддерживать несколько алгоритмов сжатия, наиболее распространенным из которых является Deflate. Также в этом формате предусмотрена проверка целостности информации с помощью Циклической проверки избыточности - CRC32.

Несмотря на существования ряда различных форматов архивирования, таких как RAR и TAR, ZIP-формат стал общепринятым стандартом для эффективного хранения данных и обмена ими по компьютерным сетям.

ZIP-файлы существуют на дисках наших компьютеров и в неявном виде. Например, все файлы, порождаемые офисными пакетами Microsoft Office и Libre Office и имеющие расширения: .docx, .xlsx, .pptx, .odt, .ods, .odp, на самом деле являются ZIP-архивами, содержащими ряд файлов и папок, совокупность которых складывается в конкретные, так знакомые нам, офисные документы. На том же ZIP-формате основываются и такие распространенные форматы файлов, как: .jar, .war и .epub.

Также с использованием ZIP-формата работает и такой суперпопулярный сервис, как GitHub, предоставляющий веб-хостинг для разработки программного обеспечения и управления версиями с помощью Git. В частности, GitHub использует файлы ZIP-формата для упаковки программных проектов, когда мы загружаем их на свои локальные компьютеры.

Подытоживая все сказанное в этом разделе, можно утверждать, что ZIP-файлы являются мощным инструментом, позволяющим объединять, сжимать и шифровать файлы в единый совместимый переносимый контейнер. Кроме того, ZIP-файлы можно передавать в потоковом режиме, разбивать на сегменты и делать самораспаковывающимися.

Зачем использовать ZIP-файлы?

Знание всех основных возможностей, которые предоставляют ZIP-файлы по архивации, чтению, хранению и извлечению цифровой информации является несомненно полезным навыком для разработчиков и специалистов, работающих в сфере IT. Это, в частности, обусловлено тем, что среди других преимуществ ZIP-файлы позволяют:

  • Уменьшать размер файлов, а следовательно, снижать и требования к их хранению без потери данных.
  • Повышать скорость передачи данных по сети за счет сжатия и передачи одного файла в пакете.
  • Упаковывать несколько файлов (папок) в один архив с последующим обеспечением эффективного управления ними.
  • Объединять код создаваемых программ в единый архив для дальнейшего его распространения.
  • Защищать цифровые данные с помощью шифрования, что в настоящее время является стандартным требованием безопасности.
  • Гарантировать целостность цифровой информации с тем, чтобы избежать случайного или злонамеренного ее изменения.

Все эти возможности ZIP-файлов могут оказаться полезным дополнением к вашему набору инструментов Python, особенно в случае, когда вы ищете гибкий и надежный способ архивирования данных для создаваемых вами программ.

Может ли Python манипулировать ZIP-файлами?

Выше уже был назван стандартный Python модуль zipfile для манипулирования ZIP-файлами, которому и посвящена данная статья. Но, в Python есть еще целый ряд инструментов, которые также дают возможность манипулировать ZIP-файлами. Причем, некоторые из этих инструментов, доступные в стандартной библиотеке Python, представляют собой низкоуровневые модули для сжатия и распаковки данных с использованием таких алгоритмов сжатия, как: zlib, bz2, lzma и др.

Вмести с тем, следует отметить, что упомянутый выше высокоуровневый Python модуль zipfile, является наиболее универсальным инструментарием для работы с ZIP-файлами и, также предусматривает использования названных выше алгоритмов сжатия. Именно вследствие этого, данная статья ставит своей целью научить вас эффективно работать с соответствующим Python модулем zipfile.

Управление существующими ZIP-файлами с помощью Python модуля - ZIPFILE

Python модуль zipfile представляет собой набор удобных классов и функции, которые дают вам возможность создавать, читать, записывать, извлекать и отображать содержимое ваших ZIP-файлов. Наряду с этим, данный модуль обладает рядом дополнительных возможностей, некоторые из которых приведены ниже:

  • Обработка ZIP-файлов размером более 4 ГБ (файлы ZIP64).
  • Распаковка зашифрованных архивов.
  • Применения нескольких алгоритмов сжатия, таких как Deflate, Bzip2 и LZMA.
  • Проверка целостности информации с помощью CRC32.

Вместе с тем, у модуля zipfile есть и ряд ограничений. Например, процесс распаковки зашифрованных архивов занимае довольно длительное время, так как использует чистый код Python. Кроме того, данный модуль не поддерживает создание зашифрованных ZIP-файлов и работу с многодисковыми ZIP-файлами. Но, несмотря на все эти ограничения, zipfile является крайне популярным, удобным и полезным инструментом для программирования на Python.

Открытие ZIP-файлов для чтения и записи

В модуле zipfile вы найдете одноименный класс ZipFile, который работает почти так же, как встроенная в Python функция open(). В частности, вышеназванный класс позволяет открывать ZIP-файлы в разных режимах, а режим чтения ("r") здесь используется по умолчанию. Также как и в функции open(), вы можете использовать с этим классом режимы записи ("w"), добавления ("a") и эксклюзивной записи ("x") для предотвращения потери данных в уже существующих файлах. Ниже мы рассмотрим все эти режимы более подробно.

Класс ZipFile реализует протокол менеджера контекста, благодаря чему вы можете использовать его в with операторе. Эта возможность позволяет быстро открывать ZIP-файлы и работать с ними, не беспокоясь о закрытии этих файлов после завершения их использования.

Но, что такое теория без практики, время которой, наконец-то, настало и для нас. Вместе с тем, прежде чем приступить к кодингу, сначала убедитесь, что у вас есть все необходимое для этого.

Рекомендации: Ниже здесь приводится ряд листингов с примерами, которые помогут вам лучше усвоить представляемый в статье материал. Все эти примеры используют дополнительные файлы различных форматов, которые наглядно показывают результаты от работы соответствующих листингов. Поэтому, для эффективного продолжения изучения нижеследующего материала, желательно прежде:

  • скачать архив с вышеназванными файлами по этой ссылке и переместить его в ту папку, где вы обычно практикуетесь в кодинге на Python;
  • распаковать только что скаченный архив в текущую папку, после чего у вас должна добавиться подпапка ‘python-zipfile’ с учебными файлами для этой статьи;
  • перейти в папку ‘python-zipfile’ и запустить там интерактивный сеанс Python.

Итак, начнем с чтения ZIP-файла sample.zip при помощи применения класса ZipFile в соответствующем режиме:

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     archive.printdir()
...
File Name                                        Modified             Size
hello.txt                                 2021-09-07 19:50:10           83
lorem.md                                  2021-09-07 19:50:10         2609
realpython.md                             2021-09-07 19:50:10          428

Первый аргумент инициализатора класса ZipFile должен указывать на путь к открываемому ZIP-файлу. Этим аргументом может быть либо _последовательность символов_в виде строки, либо объект подобный файлам - file-like, или путям к ним - path-like. В этом примере используется путь, сформированный на основе строки.

Второй аргумент ZipFile, представляет собой строку из одной буквы, указывающую на режим, который должен использоваться для открытия ZIP-файла. Как упоминалось выше, в зависимости от ваших потребностей для открытия файла в классе ZipFile можно указывать один из четырех возможных режимов. Значение нужного режима указывается в позиционном аргументе mode, который по умолчанию равен ("r"), что позволяет вам вообще его не указывать при открытии файла архива только для чтения.

Применяя оператор контекстного менеджера with в вышеприведенном листинге класс ZipFile создает файловый объект, записываемый в переменную archive. Этот файловый объект из переменной archive затем вызывается внутри контекстного менеджера методом printdir(), позволяющим наглядно и быстро отображать содержимое базовых ZIP-файлов на экране. Так, в частности, вывод этого метода осуществляется в удобном табличном формате с тремя информативными столбцами:

  • File Name
  • Modified
  • Size

Всегда есть вероятность использования ZIP-файлов с некорректным (поврежденным либо вообще несоответствующем действительности) форматом. Поэтому, для гарантии использования в вашей программе действительно корректного ZIP-файла, работающий с ним ZipFile класс вы можете предварительно обернуть в оператор try ... except и, таким образом, перехватить соответствующее BadZipFile исключение:

>>> import zipfile

>>> try:
...     with zipfile.ZipFile("sample.zip") as archive:
...         archive.printdir()
... except zipfile.BadZipFile as error:
...     print(error)
...
File Name                                        Modified             Size
hello.txt                                 2021-09-07 19:50:10           83
lorem.md                                  2021-09-07 19:50:10         2609
realpython.md                             2021-09-07 19:50:10          428

>>> try:
...     with zipfile.ZipFile("bad_sample.zip") as archive:
...         archive.printdir()
... except zipfile.BadZipFile as error:
...     print(error)
...
File is not a zip file

В приведенном выше листинге, первый пример успешно открывает sample.zip без возникновения какого-либо BadZipFile исключения. Это происходит потому, что sample.zip имеет корректный (допустимый) формат ZIP. С другой же стороны, во втором примере открыть bad_sample.zip не удается, поскольку данный файл имеет некорректный либо поврежденный ZIP-формат.

Для проверки корректности формата используемого ZIP-файла также можно воспользоваться функцией is_zipfile():

>>> import zipfile

>>> if zipfile.is_zipfile("sample.zip"):
...     with zipfile.ZipFile("sample.zip", "r") as archive:
...         archive.printdir()
... else:
...     print("File is not a zip file")
...
File Name                                        Modified             Size
hello.txt                                 2021-09-07 19:50:10           83
lorem.md                                  2021-09-07 19:50:10         2609
realpython.md                             2021-09-07 19:50:10          428

>>> if zipfile.is_zipfile("bad_sample.zip"):
...     with zipfile.ZipFile("bad_sample.zip", "r") as archive:
...         archive.printdir()
... else:
...     print("File is not a zip file")
...
File is not a zip file

В этих двух примерах оператор контекстного менеджера with дополнительно оборачивается в условный оператор if, условием для которого выступает функция is_zipfile(). Эта функция принимает аргумент filename, указывающий путь к ZIP-файлу в вашей файловой системе. Точно также, как в классе ZipFile, путь к файлу в этом аргументе может быть представлен в виде строки, файлового объекта или объекта пути - path-like. При условии, если аргумент filename в функции указывает на ZIP-файл с корректным форматом, возвращается значение True, во всех иных случаях возвращается False.

Что же делать если вы, например, используя ZipFile хотите добавить файл hello.txt в hello.zip архив. Для этого можно воспользоваться режимом ("w"), открывающим ZIP-файл для записи. Но, при этом, следует иметь в виду, что если целевой ZIP-файл уже существует, то ("w") режим предварительно полностью удалит его содержимое и перезапишет в этот файл новый, указываемый вами в программе, контент.

Примечание. Используя класс ZipFile для работы с уже существующими файлами, вам следует быть осторожным с ("w") режимом, так как благодаря ему уже имеющиеся содержимое в обрабатываемых ZIP-файлах может полностью потеряться.

Если целевой ZIP-файл еще не существует, то в результате применения ("w") режима в ZipFile данный целевой файл создастся автоматически после закрытия архива:

>>> import zipfile

>>> with zipfile.ZipFile("hello.zip", mode="w") as archive:
...     archive.write("hello.txt")
...

После запуска вышеприведенного кода у вас в папке python-zipfile появится новый файл hello.zip, содержимое которого при его выводе методом .printdir() будет состоять всего лишь из одного упакованного файла hello.txt. Главной составляющей приведенного выше кода является вызов для объекта ZipFile метода .write(), позволяющего записывать файлы участников в ваши ZIP-архивы. Важно иметь в виду, что аргументом метода .write() должен быть только лишь существующий файл.

Примечание: класс ZipFile достаточно умен, чтобы в режиме записи автоматически создать новый архив, в случае, когда этот архив на диске еще не существует. Однако этот класс не может сам создавать новые (еще не существующие) папки для записи в них целевых ZIP-файлов.
Данное обстоятельство, в частности, объясняет причину неработоспособности нижеследующего кода:

>>> import zipfile
>>> with zipfile.ZipFile("missing/hello.zip", mode="w") as archive:
...     archive.write("hello.txt")
...
Traceback (most recent call last):
    ...
FileNotFoundError: [Errno 2] No such file or directory: 'missing/hello.zip'
Поскольку папка missing в пути к целевому файлу hello.zip не существует, вы при выполнении данного кода получите исключение FileNotFoundError.

В отличии от режима записи, режим ("a") позволяет добавлять в уже существующие ZIP-архивы новые файлы участники не удаляя прежнее содержимое этих архивов. Следовательно, данный режим является более безопасным нежели режим записи, так как старое (уже имеющиеся) содержимое архивов в нем не может быть утеряно. При этом, точно также, как режим записи, режим ("a") при несуществующих целевых ZIP-файлах, создает их автоматически, а затем добавляет в них любые входные файлы, которые вы им передаете в качестве аргумента .write().

Нижеприведенный пример с помощью режима ("a") добавляет файл new_hello.txt в предварительно созданный нами архив hello.zip:

>>> import zipfile

>>> with zipfile.ZipFile("hello.zip", mode="a") as archive:
...     archive.write("new_hello.txt")
...

>>> with zipfile.ZipFile("hello.zip") as archive:
...     archive.printdir()
...
File Name                                        Modified             Size
hello.txt                                 2021-09-07 19:50:10           83
new_hello.txt                             2021-08-31 17:13:44           13

В этом примере после добавления файла new_hello.txt в архив hello.zip запускается метод .printdir(), который убеждает нас в том, что вновь добавленный нами текстовой файл действительно присутствует в ZIP-архиве.

Клас ZipFile также поддерживает эксклюзивный режим ("x"), который позволяет вам создавать исключительно новые ZIP-файлы и записывать в них новые файлы-участники. Этот эксклюзивный режим используется для предотвращения перезаписывания данных при создании новых ZIP-файлов, имена которых совпадают с именами уже имеющихся у вас на диске файлов. При возникновении такой ситуации благодаря этому режиму вы получите исключение FileExistsError.

Наконец, если вы создаете ZIP-архив в ("w"), ("a"), или ("x") режимах, а затем закрываете этот архив, не добавляя к нему никаких файлов-участников, то класс ZipFile создает пустой файл с соответствующим форматом и расширением ZIP.

Чтение метаданных из ZIP-файлов

Выше при кодинге примеров из этой статьи вам уже доводилось несколько раз использовать .printdir(). Это полезный метод, который применяется для быстрого просмотра содержимого ZIP-архивов используемых вашими программами. Но, помимо .printdir(), в классе ZipFile присутствуют также и другие удобные методы для извлечения метаданных из существующих ZIP-архивов.

Вот краткое описание назначения этих методов:

Метод Описание
.getinfo(filename) Возвращает ZipInfo объект с информацией о файле-участнике архива, предоставленном в аргументе filename. Необходимо помнить, что путь к целевому файлу в аргументе filename должен быть идентичен тому пути, который фактически содержит у себя внутри сам базовый ZIP-файл.
.infolist() Возвращает список объектов ZipInfo - файлов участников в целевом ZIP-архиве.
.namelist() Возвращает список, содержащий имена всех файлов-участников целевого ZIP-архива. Имена в этом списке являются допустимыми аргументами для метода .getinfo().

С помощью этих трех методов можно получить множество полезных сведений, как о содержимом ваших ZIP-архивов в целом, так и о файлах участниках, которые эти архивы составляют. Это наглядно показывает нижеследующий листинг, где показан пример использования метода .getinfo():

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     info = archive.getinfo("hello.txt")
...

>>> info.file_size
83

>>> info.compress_size
83

>>> info.filename
'hello.txt'

>>> info.date_time
(2021, 9, 7, 19, 50, 10)

Из данного листинга видно, что принимая в качестве аргумента строку с именем файла участника hello.txt метод .getinfo() возвращает соответствующие сведения об этом файле из архива sample.zip в переменную объекта ZipInfo под названием info.

Примечание: ZipInfo не предназначен для непосредственного создания экземпляров своего класса. Но эту функцию могут выполнять методы .getinfo() и .infolist(), которые возвращают ZipInfo объекты при своем вызове. Вместе с тем, ZipInfo включает в себя метод с именем .from_file(), который при необходимости позволяет вам явно создавать экземпляры этого класса.

Для получения необходимых сведений о файлах участниках архивов ZipInfo объекты имеют несколько соответствующих атрибутов. Так, например, среди этих атрибутов есть .file_size и .compress_size, которые хранят размер в байтах соответственно для исходного и сжатого файлов. К другим полезным атрибутам данного класса можно отнести такие как .filename и .date_time, которые возвращают имя файла и дату его последнего изменения.

Примечание. По умолчанию, при добавлении файлов-участников в архив методами класса ZipFile, эти файлы не сжимаются. Этим обстоятельством, как раз и объясняется то, что размеры исходного и сжатого файлов в вышеприведенном примере совпадают. Более подробно данная тема освещеается в разделе "Сжатие файлов и каталогов (папок)" ниже в этой статье.

Метод .infolist() позволяет извлечь информацию о всех файлах участниках целевого архива. Вот пример использования данного метода для создания минимального отчета с информацией обо всех файлах, сохраненных в архиве sample.zip из комплекта материалов для этой статьи:

>>> import datetime
>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     for info in archive.infolist():
...         print(f"Filename: {info.filename}")
...         print(f"Modified: {datetime.datetime(*info.date_time)}")
...         print(f"Normal size: {info.file_size} bytes")
...         print(f"Compressed size: {info.compress_size} bytes")
...         print("-" * 20)
...
Filename: hello.txt
Modified: 2021-09-07 19:50:10
Normal size: 83 bytes
Compressed size: 83 bytes
--------------------
Filename: lorem.md
Modified: 2021-09-07 19:50:10
Normal size: 2609 bytes
Compressed size: 2609 bytes
--------------------
Filename: realpython.md
Modified: 2021-09-07 19:50:10
Normal size: 428 bytes
Compressed size: 428 bytes
--------------------

В этом листинге цикл for перебирает объекты ZipInfo, записываемые в переменную info из .infolist(). Затем из этой переменной для каждого файла участника архива sample.zip извлекаются такие их данные, как: имя, дата последней модификации, исходный и сжатый размеры. Метод datetime необходим в данном примере для форматирования дат последней модификации файлов в удобочитаемый вид.

При необходимости выполнить быструю проверку ZIP-архива и просто перечислить имена его файлов-участников, вы можете использовать метод .namelist():

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     for filename in archive.namelist():
...         print(filename)
...
hello.txt
lorem.md
realpython.md

Поскольку имена файлов в результате вызова метода .namelist() могут использоваться в качестве аргументов метода .getinfo(), вы можете сочетать эти два метода для получения информации лишь по избранным файлам участникам, например, исходя из расширений в их имени.

Чтение и запись в файлы участники

Иногда вам нужно будет прочитать содержимое конкретного файла участника ZIP-архива не распаковывая его. Для этого можно воспользоваться методом .read(). Данный метод принимает файл-участник в аргументе name и возвращает содержимое этого файла в виде байтов :

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     for line in archive.read("hello.txt").split(b"\n"):
...         print(line)
...
b'Hello, Pythonista!'
b''
b'Welcome to Real Python!'
b''
b"Ready to try Python's zipfile module?"
b''

Для использования .read(), вы должны открыть ZIP-файл в режиме чтения или добавления. Следует также обратить внимание, что .read() возвращает содержимое целевого файла участника в виде потока байтов. Именно поэтому в данном примере используется метод .split(), позволяющий разделить поток байтов на строки при помощи символа перевода строки - "\n" указываемого в качестве разделителя. Поскольку .split() в нашем примере работает с байтовым объектом, то дополнительно к аргументу данного метода равному "\n" должно также добавляться значение b.

Дополнительно в виде второго позиционного аргумента метод ZipFile.read() может принимать, так называемый pwd, позволяющий указывать пароль для чтения зашифрованных файлов. Попробовать данную возможность можно на примере файла sample_pwd.zip, входящего в состав комплекта файлов для практики, ссылка на скачивания которого приведена в начале этой статьи:

>>> import zipfile

>>> with zipfile.ZipFile("sample_pwd.zip", mode="r") as archive:
...     for line in archive.read("hello.txt", pwd=b"secret").split(b"\n"):
...         print(line)
...
b'Hello, Pythonista!'
b''
b'Welcome to Real Python!'
b''
b"Ready to try Python's zipfile module?"
b''

>>> with zipfile.ZipFile("sample_pwd.zip", mode="r") as archive:
...     for line in archive.read("hello.txt").split(b"\n"):
...         print(line)
...
Traceback (most recent call last):
    ...
RuntimeError: File 'hello.txt' is encrypted, password required for extraction

В первом примере вышеприведенного листинга для чтения вашего зашифрованного файла в аргументе pwd указывается пароль secret, предваряемый значением b, поскольку мы работаем с битовым объектом. Если же вы попробуете использовать .read() к тому же зашифрованному файлу, но без указания соответствующего пароля для него, то как видно из 2-го примера данного листинга, возникает исключение RuntimeError.

Примечание. Хотя Python модуль zipfile и поддерживает расшифровку, однако он не умеет создавать зашифрованные ZIP-файлы, для получения которых вам придется использовать внешние файловые архиваторы. В частности, для создания таких зашифрованных ZIP-файлов можно воспользоваться такими популярными файловыми архиваторами, как: 7z и WinRAR для Windows, Ark и GNOME Archive Manager для Linux и Archiver для macOS.

Следует иметь в виду, что для больших зашифрованных ZIP-файлов операция расшифровки с помощью модуля zipfile может быть очень медленной, поскольку она реализована чисто лишь на Python интерпретаторе. Поэтому, иногда в таких случаях вместо использования zipfile целесообразнее будет рассмотреть возможность применения соответствующих специализированных программ.

Если вы регулярно работаете с зашифрованными файлами, то для их расшифровки вам совсем необязательно каждый раз указывать соответствующий пароль в аргументе pwd при вызове .read() или другого метода. В таких ситуациях будет более оправданным использования метода ZipFile.setpassword() для установки глобального пароля:

>>> import zipfile

>>> with zipfile.ZipFile("sample_pwd.zip", mode="r") as archive:
...     archive.setpassword(b"secret")
...     for file in archive.namelist():
...         print(file)
...         print("-" * 20)
...         for line in archive.read(file).split(b"\n"):
...             print(line)
...
hello.txt
--------------------
b'Hello, Pythonista!'
b''
b'Welcome to Real Python!'
b''
b"Ready to try Python's zipfile module?"
b''
lorem.md
--------------------
b'# Lorem Ipsum'
b''
b'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    ...

Благодаря использованию .setpassword(), для файлов зашифрованного ZIP-архива вам достаточно указать соответствующий пароль всего один раз. Затем ZipFile будет уже автоматически использовать этот пароль для расшифровки всех файлов-участников вашего целевого архива.

Если же ваш ZIP-архив для своих отдельных файлов-участников будет иметь разные пароли, то тут .setpassword() вам не поможет и, придется указывать конкретные пароли для каждого файла архива, используя все тот же pwd аргумент в методе .read():

>>> import zipfile

>>> with zipfile.ZipFile("sample_file_pwd.zip", mode="r") as archive:
...     for line in archive.read("hello.txt", pwd=b"secret1").split(b"\n"):
...         print(line)
...
b'Hello, Pythonista!'
b''
b'Welcome to Real Python!'
b''
b"Ready to try Python's zipfile module?"
b''

>>> with zipfile.ZipFile("sample_file_pwd.zip", mode="r") as archive:
...     for line in archive.read("lorem.md", pwd=b"secret2").split(b"\n"):
...         print(line)
...
b'# Lorem Ipsum'
b''
b'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    ...

В данном примере пароль secret1 используется для чтения файла hello.txt, а пароль secret2 для чтения файла lorem.md. Иногда бывают случаи, когда наряду с .setpassword() в методах типа .read() параллельно используется и pwd аргумент. В такой ситуации pwd всегда будет переопределять любой пароль, заданный на уровне архива с помощью .setpassword().

Примечание. Нужно помнить, что при вызове .read()для ZIP-архива, в котором используется неподдерживаемый Python метод сжатия, создается файл NotImplementedError. Также сообщение об ошибке можно получить и в случае, когда необходимый модуль сжатия просто недоступен в вашей установке Python.

Наряду с уже рассмотренными методами работы с файлами участниками, в модули zipfile существуют гораздо более гибкие способы чтения, создания и добавления вышеназванных файлов путем применения метода ZipFile.open(). Как и встроенная в Python функция open(), этот метод реализует протокол диспетчера контекста и, следовательно, поддерживает оператор with:

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     with archive.open("hello.txt", mode="r") as hello:
...         for line in hello:
...             print(line)
...
b'Hello, Pythonista!\n'
b'\n'
b'Welcome to Real Python!\n'
b'\n'
b"Ready to try Python's zipfile module?\n"

Данный пример с помощью метода .open() открывает файл hello.txt в режиме чтения. Данный метод в нашем примере содержит всего два аргумента, первый из которых name указывает на файл участника, который нужно открыть, а второй задает режим открытия и, по умолчанию, как обычно, установлен в ("r"). Кроме этих двух аргументов ZipFile.open() также может принимать аргумент pwd, который открывает зашифрованные файлы по паролю и работает так же, как эквивалентный pwd аргумент в .read().

Дополнительно, метод .open() может работать, как с режимом ("w"), обеспечивающим создания новых файлов участников и записывания в них содержимого, так и с режимом ("a"), позволяющим добавлять новые файлы участники в уже существующие архивы:

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="a") as archive:
...     with archive.open("new_hello.txt", "w") as new_hello:
...         new_hello.write(b"Hello, World!")
...
13

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     archive.printdir()
...     print("------")
...     archive.read("new_hello.txt")
...
File Name                                        Modified             Size
hello.txt                                 2021-09-07 19:50:10           83
lorem.md                                  2021-09-07 19:50:10         2609
realpython.md                             2021-09-07 19:50:10          428
new_hello.txt                             1980-01-01 00:00:00           13
------
b'Hello, World!'

Первый фрагмент кода данного примера открывает архив sample.zip в режиме добавления ("a"), а затем создает в нем новый файл new_hello.txt с помощью вызова метода .open() в режиме "w". Данный метод, в свою очередь, возвращает файлоподобный объект, поддерживающий метод .write(), который позволяет вам записывать байты во вновь созданный файл.

Примечание. Для добавления файла в архив методом .open() вам обязательно следует указывать уникальное его имя, которого еще нет в этом архиве. Иначе, при добавлении файла с именем, которое уже используется существующими участниками архива, у вас возникнет, как дублированный файл в архиве, так и исключение UserWarning.

Вышеприведенный пример записывает текст b'Hello, World!' в файл new_hello.txt. при выполнении внутреннего оператора with. Данная запись текста в виде байтов фиксируется (сохраняется) в файле участнике, как только поток выполнения программы выходит из этого внутреннего оператора with. Когда же в программе из вышеприведенного листинга завершается и внешний оператор with, Python записывает new_hello.txt в базовый ZIP-файл sample.zip.

Второй фрагмент кода данного примера просто подтверждает, что файл new_hello.txt теперь является частью архива sample.zip. При этом, как вы могли заметить из выходных данных этого кода, дата вновь добавленного методом .write() файла в архиве обнуляется и устанавливается в 1980-01-01 00:00. Это казус Python интерпретатора, который вам следует исправлять, восстанавливая реальные даты добавляемых в архивы файлов программным путем.

Чтение содержимого файлов участников в виде текста

Из предыдущего раздела вы узнали об использовании методов .read() и .write() для работы с файлами участниками без предварительного их извлечения из ZIP-архива. Оба эти метода работают исключительно с байтами.

Однако зачастую в ZIP-архивах хранятся текстовые файлы, содержимое которых нужно прочитать именно, как текст, а не как байты. Сделать это вы можете как минимум с помощью двух следующих методов:

  1. decode()
  2. TextIOWrapper

Так, в случае с ZipFile.read(), который возвращает содержимое целевого файла участника в виде байтов, рассматриваемый метод .decode() может напрямую декодировать эти байты (bytes объект) в строку, используя заданный формат кодировки символов.

В нижеприведенном примере демонстрируется использования .decode() для чтения текста из файла hello.txt, хранящегося в архиве sample.zip:

>>> import zipfile

>>>  with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     text = archive.read("hello.txt").decode(encoding="utf-8")
...

>>> print(text)
Hello, Pythonista!

Welcome to Real Python!

Ready to try Python's zipfile module?

Код данного примера читает содержимое hello.txt сначала как байты, но затем тут же вызывает метод .decode() для декодирования этих байтов в строку, используя UTF-8 в качестве стандарта кодировки. В данном примере для аргумента encoding метода .decode() используется соответствующая строка со значением кодировки "utf-8". Однако в этом аргументе вы можете использовать любую другую допустимую кодировку, такую как UTF-16 или cp1252, которая может быть представлена в виде строки без учета регистра. Следует отметить, что значением по умолчанию для аргумента encoding является именно "utf-8", поэтому данный аргумент в .decode() зачастую можно просто опускать.

При обработки нужного вам файла с использованием .decode() очень важно заранее знать его формат кодировки. Иначе, при неверной кодировки, ваш код не сможет правильно декодировать базовые байты в текст, и в результате вы получите содержание файла, состоящее из массы неразборчивых символов.

Второй вариант чтения текста из файла участника может быть организован на основе использования io.TextIOWrapper объекта, представляющего собой буферизованный текстовый поток. Но, для использования данного объекта вам нужно будет использовать .open() вместо .read(). Вот пример использования io.TextIOWrapper для чтения содержимого файла участника hello.txt в виде потока текста:

>>> import io
>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     with archive.open("hello.txt", mode="r") as hello:
...         for line in io.TextIOWrapper(hello, encoding="utf-8"):
...             print(line.strip())
...
Hello, Pythonista!

Welcome to Real Python!

Ready to try Python's zipfile module?

Внутренней оператор with в этом примере открывает файл участник hello.txt, хранящейся в архиве sample.zip. Затем полученный двоичный файловый объект из переменной hello передается в инициализатор io.TextIOWrapper, создающий буферизованный текстовый поток путем декодирования содержимого hello по стандарту UTF-8. В результате вы получаете поток текста прямо из целевого файла участника.

Как и в случае с .encode(), io.TextIOWrapper класс принимает аргумент encoding. В этом классе для аргумента encoding рекомендуется не полагаться на значение по умолчанию и всегда его указывать. Это объясняется тем, что значение по умолчанию тут зависит от операционной системы, в которой выполняется ваш код и, следовательно, оно может быть неправильным для файла, который вы пытаетесь декодировать.

Извлечение файлов участников из ZIP-архивов

Извлечение содержимого из имеющихся у вас архивов, является одной из наиболее распространенных операций, выполняемых с ZIP-файлами. В зависимости ваших потребностей за один раз из архива можно извлекать либо только один файл, либо сразу все файлы.

Первую из вышеназванных позволяет выполнять метод ZipFile.extract(), в первом аргументе которого указывается имя извлекаемого файла участника, а во втором путь, куда этот файл будет извлечен. Если же второй аргумент (место назначения path) в данном методе опустить, то по умолчанию файл участник будет извлечен в текущую папку:

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     archive.extract("new_hello.txt", path="output_dir/")
...
'output_dir/new_hello.txt'

Благодаря выполнению кода из данного примера, извлеченный из sample.zip файл new_hello.txt окажется в папке output_dir/. Если имя извлекаемого файла в указываемой .extract() паке уже существует, то оно перезаписывается без запроса на подтверждение. Если указываемая в методе папка еще не существует, то .extract() ее создаст автоматически. Полезным будет знать и то, что возвращаемым значением .extract() является путь к извлеченному файлу.

Имя извлекаемого файла участника для первого аргумента .extract() должно быть либо строкой, которая в частности может возвращаться методом .namelist(), либо ZipInfoобъектом, содержащим информацию о файле.

Метод .extract() также можно использовать с зашифрованными файлами. Для этого вам необходимо указать требуемый пароль в аргументе pwd или установить необходимый пароль на уровне архива за счет .setpassword().

Для извлечения из архива всех составляющих его файлов можно воспользоваться методом .extractall(). Данный метод, как следует из его названия, извлекает все файлы участники в папку, путь к которой прописывается первым его аргументом. Если этот аргумент опустить, то по умолчанию все файлы архива будут извлечены в текущую папку:

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     archive.extractall("output_dir/")
...

После запуска вышеприведенного кода все распакованное содержимое архива sample.zip будет сохранено в паке output_dir/. В случае, если вы для .extractall() укажите несуществующую папку, то данная папка будет создана автоматически. Если же какие-либо файлы участники уже существуют в указываемой ддя .extractall() целевой паке, то они будут перезаписаны без запроса на подтверждение. Поэтому, во избежание данной ситуации, вам следует быть осторожными.

При необходимости извлечения из архива лишь некоторых его файлов участников в .extract() можно воспользоваться аргументом members, который может содержать необходимое подмножество файлов, выбранных из всего списка файлов, имеющихся в архиве. И, наконец, следует не забывать, что как .extract(), так и .extractall() методы для извлечения зашифрованных файлов, могут дополнительно принимать аргумент pwd.

Закрытие ZIP-файлов после их использования

Иногда возникают ситуации, когда при работе с ZIP-файлами в программах целесообразнее отказаться от использования with инструкций. Но тогда, чтобы завершить соответствующие операции записи и освободить отведенные для файлов ресурсы, вам в обязательном порядке придется вручную закрывать все используемые до этого архивы.

Такое закрытие архивов можно осуществить путем вызова метода .close() для соответствующего ZipFile объекта:

>>> import zipfile

>>> archive = zipfile.ZipFile("sample.zip", mode="r")

>>> # Полученный архив можно использовать в разных частях вашего кода
>>> archive.printdir()
File Name                                        Modified             Size
hello.txt                                 2021-09-07 19:50:10           83
lorem.md                                  2021-09-07 19:50:10         2609
realpython.md                             2021-09-07 19:50:10          428
new_hello.txt                             1980-01-01 00:00:00           13

>>> # Данный архив следует закрыть, когда завершится работа с ним
>>> archive.close()
>>> archive
<zipfile.ZipFile [closed]>

В данном примере вызов .close()закрывает архив sample.zip, хранящейся в переменной archive. Помните, что при отсутствии в программе with инструкций, использования метода .close() является обязательным. Иначе результаты от выполняемых в программе операций записи и добавления, после ее завершения не сохранятся. Например, если вы открываете ZIP-архив в режиме добавления ("a"), то соответствующее добавление новых файлов участников к этому архиву осуществиться только после вызова для объекта данного архива метода .close().

Создание, наполнение и распаковка собственных ZIP-файлов

Итак, вы уже научились работать с существующими ZIP-архивами. В частности, вы уже можете читать, записывать и добавлять к ним файлы участники, используя различные режимы ZipFile. Также вы узнали, как читать соответствующие метаданные и как извлекать содержимое из файлов в ZIP-архивах не распаковывая их.

В нижеследующих разделах мы рассмотрим несколько практических примеров, которые помогут вам научиться создавать ZIP-архивы, как из нескольких входных файлов, так и из папок с имеющимися в них файлами и подпапками. Для этого мы наряду с использованием модуля zipfile, рассмотрим применения ряда дополнительных инструментов Python. Также в следующих разделах вы узнаете, как использовать zipfile для сжатия файлов и многого другого.

Создание ZIP-архива из нескольких файлов

Иногда вам бывает нужно просто упаковать ряд файлов в один контейнер - ZIP-архив, так чтобы потом их удобнее можно было бы распространять по компьютерным сетям или обмениваться с коллегами. Для этого обычно предварительно создается список целевых файлов, которые затем, используя класс ZipFile заносятся в архив с помощью того или иного цикла:

>>> import zipfile

>>> filenames = ["hello.txt", "lorem.md", "realpython.md"]

>>> with zipfile.ZipFile("multiple_files.zip", mode="w") as archive:
...     for filename in filenames:
...         archive.write(filename)
...

Данный пример в переменной archive создает ZipFile объект, куда в качестве первого аргумента заносится имя вновь создаваемого архива, а в качестве второго – режим открытия этого архива со значением "w", позволяющим записывать в него соответствующие файлы участники.

Затем цикл for перебирая список файлов, предварительно сохраненный в переменной filenames, последовательно записывает их в базовый ZIP-архив (ZipFile объект), используя метод .write(). Как только поток выполнения программы выходит из with инструкции, ZipFile автоматически закрывает вновь созданный и наполненный архив, сохраняя в нем все те файлы, имена которых были перечислены в переменной filenames. Теперь у вас на диске должен появится архив multiple_files.zip, содержащий все нужные вам файлы из списка.

Создание ZIP-архива из каталога (папки)

Объединение содержимого каталогов (папок) в один архив, является еще одним повседневным вариантом использования ZIP-файлов. В Python, наряду с модулем zipfile есть несколько дополнительных инструментов, которые необходимо применять с целью решения вышеупомянутой задачи. Например, вы сначала с помощью pathlib можете получить содержимое нужной вам папки, а затем данную информацию использовать для создания соответствующего архива этой папки на основе использования класса ZipFile.

В папке python-zipfile/, которую вам рекомендавалось скачать по ссылке в начале этой статьи, у вас есть подпапка source_dir/ со следующим содержимым:

source_dir/

├── hello.txt
├── lorem.md
└── realpython.md

Здесь вы можете видеть, что папка source_dir/ содержит лишь три обычных файла без каких-либо дополнительных подпапок. При этом, если вам не нужно получать содержимой каких-либо подпапок, а достаточно узнать информацию только лишь о файлах в целевой папке, проще всего воспользоваться методом pathlib.Path.iterdir(). Таким образом, создать ZIP-архив из содержимого папки source_dir/ можно следующим образом:

>>> import pathlib
>>> import zipfile

>>> directory = pathlib.Path("source_dir/")

>>> with zipfile.ZipFile("directory.zip", mode="w") as archive:
...    for file_path in directory.iterdir():
...        archive.write(file_path, arcname=file_path.name)
...

>>> with zipfile.ZipFile("directory.zip", mode="r") as archive:
...     archive.printdir()
...
File Name                                        Modified             Size
realpython.md                             2021-09-07 19:50:10          428
hello.txt                                 2021-09-07 19:50:10           83
lorem.md                                  2021-09-07 19:50:10         2609

В этом примере содержимое папки source_dir/ заносится в переменную directory_в виде _pathlib.Path объекта. Затем, в первом операторе with создается предназначенный для записи ZipFile объект, в который, благодаря итерации по записям за счет вызова в цикле метода .iterdir(), происходит добавление нужных файлов в архив.

Отметим, что метод .iterdir() здесь применяется только в связи с тем, что в source_dir/ у нас нет подпапок. Следовательно, в цикле for возвращаются, а затем и записываются в архив лишь только файлы.

Как видно из кода данного листинга, при применении метода .write(), в нем кроме первого аргумента file_path с именем добавляемого файла используется также и второй аргумент arcname, приравниваемый к значению file_path.name. Данный (второй) аргумент .write() необходим для указания имени файла участника в результирующем архиве. Следует отметить, что в предыдущих примерах статьи, этот аргумент просто не использовался поскольку значение для arcname по умолчанию всегда автоматически приравнивается к первому аргументу метода .write().

Вместе с тем для архивирования папок, аргумент arcname со значением file_path.name крайне важен, так как если данный аргумент в методе .write() опустить, то файлы в архиве сохранятся без указания имен папок, где они должны были бы лежать. В такой ситуации, все файлы после распаковки окажутся не в надлежащих папках, а в корневой директории вашего диска.

Теперь пришло время в вашем комплекте учебных материалов обратиться к папке root_dir/, которая имеет следующую структуру:

root_dir/

├── sub_dir/
│   └── new_hello.txt

├── hello.txt
├── lorem.md
└── realpython.md

В этой папке у вас есть, как три обычных файла, так и подпапка с одним файлом в ней. Поэтому, для создания ZIP-архива с такой же разветвлённой внутренней структурой подпапок из модуля pathlib нам понадобится соответствующий инструмент, рекурсивно перебирающий дерево каталогов в root_dir/. Таким инструментом является метод Path.rglob(), который совместно с zipfile может заархивировать многоуровневую структуру папки root_dir/следующим образом:

>>> import pathlib
>>> import zipfile

>>> directory = pathlib.Path("root_dir/")

>>> with zipfile.ZipFile("directory_tree.zip", mode="w") as archive:
...     for file_path in directory.rglob("*"):
...         archive.write(
...             file_path,
...             arcname=file_path.relative_to(directory)
...         )
...

>>> with zipfile.ZipFile("directory_tree.zip", mode="r") as archive:
...     archive.printdir()
...
File Name                                        Modified             Size
sub_dir/                                  2021-09-09 20:52:14            0
realpython.md                             2021-09-07 19:50:10          428
hello.txt                                 2021-09-07 19:50:10           83
lorem.md                                  2021-09-07 19:50:10         2609
sub_dir/new_hello.txt                     2021-08-31 17:13:44           13

В этом примере метод Path.rglob() используется для рекурсивного обхода дерева каталогов (подпапок) в root_dir/. Полученные таким образом, каждый файл и подпапка после этого записываются в целевой ZIP-архив.

Здесь для добавления файлов и подпапок в архив также используется метод .write() с двумя аргументами. Но, на этот раз для получения второго аргумента в .write() используется не свойство pathlib.Path объекта file_path.name, а его метод Path.relative_to(), обеспечивающий получения относительного пути к каждому файлу (подпапки). В итоге, результирующий ZIP-файл будет иметь ту же внутреннюю структуру, что и ваша исходная папка. Опять же, вы можете избавиться от второго аргумента метода .write() в случае если хотите, чтобы ваш архив содержал все упакованные в нем файлы в корне, без учета какой-либо структуры дерева каталогов.

Сжатие файлов и каталогов (папок)

При наличии файлов большого объема, естественно, для экономии места на диске и ускорения передачи по компьютерным сетям, вам приходится эти файлы сжимать. Для этого в модули Python zipfile поддерживаются несколько популярных методов сжатия. Однако этот модуль по умолчанию не сжимает ваши файлы и, если вы все же захотите уменьшить размер своих файлов, вам придется явно указать необходимый метод сжатия в инициализаторе ZipFile.

Для обозначения файлов участников, записанных в ZIP-архив без сжатия, как правило, используется термин " stored". Вот почему метод сжатия по умолчанию в классе ZipFile называется ZIP_STORED и свидетельствует о том, что файлы участники в архиве хранятся просто несжатыми.

Конкретный метод сжатия указывается в третьем аргументе инициализатора ZipFile. Поэтому, в случае если вам потребуется сжать файлы во время их записи в ZIP-архив, то нужно будет установить для этого аргумента одну из следующих констант:

Константа Метод сжатия Требуемый модуль Python
zipfile.ZIP_DEFLATED Deflate Zlib
zipfile.ZIP_BZIP2 Bzip2 bz2
zipfile.ZIP_LZMA LZMA Lzma

Все эти методы сжатия, являются стандартными и допустимыми для их использования с файлами ZipFile. Однако использования других методов вызовет исключение NotImplementedError, поскольку даже Python 3.10 для zipfile не предусматривает иных дополнительных методов.

Также следует помнить, что при использовании вами того или иного метода, поддерживающий его модуль сжатия должен быть обязательно доступен в вашей установке Python. Иначе вы получите исключение RuntimeError и, ваш код будет не работоспособен.

Дополнительным параметром сжатия файлов в архив должно выступать значение compresslevel, указываемое в виде соответствующего аргумента в инициализаторе ZipFile и определяющее уровень сжатия файлов участников архива.

Так, для метода Deflate значение compresslevel может принимать целые числа от 0 до 9, а для метода Bzip2целые числа от 1 до 9. В обоих случаях с увеличением значения compresslevel файлы заносятся в ZIP-архив с более высокой степенью сжатия, но с более низкой скоростью его формирования

Примечание. Следует помнить, что двоичные файлы наподобие: PNG, JPG, MP3 и т. п., сохраняются в уже сжатом формате. Поэтому, добавление этих файлов в ZIP-архив с целью еще большего сжатия, обычно нецелесообразно.

Наступило время попробовать заархивировать и сжать содержимое папки source_dir/, имеющейся в наших учебных материалах, применяя для этого наиболее часто использующейся метод сжатия Deflate. С этой целью запустим следующий код:

>>> import pathlib
>>> from zipfile import ZipFile, ZIP_DEFLATED

>>> directory = pathlib.Path("source_dir/")

>>> with ZipFile("comp_dir.zip", "w", ZIP_DEFLATED, compresslevel=9) as archive:
...     for file_path in directory.rglob("*"):
...         archive.write(file_path, arcname=file_path.relative_to(directory))
...

При инициализации архива с помощью ZipFile, четвертый аргумент этого метода с ключевым словом compresslevel у нас приравнивается к 9-ти, что задает максимальную степень сжатия для файлов этого архива. Явное указание данного ключевого слова в инициализаторе ZipFileявляется обязательным, так как далеко не всегда степень сжатия является именно четвертым аргументом этого метода.

Примечание. Четвертым параметром инициализатора ZipFile может также являться аргумент с именем allowZip64, представляющий собой логическое значение, указывающее ZipFile на необходимость создания ZIP-архивов с .zip64 расширением для файлов размером более 4 ГБ.

После запуска вышеприведенного кода у нас в текущей папке будет создан новый архив comp_dir.zip, который будет аналогичен по содержанию с архивом sample.zip. Но, если вы сравните размеры файлов двух этих архивов, то заметите, что первый файл значительно меньше второго.

Последовательное накопление файлов в ZIP-архивах

Последовательное накопление файлов в ZIP-архивах может быть еще одной распространенной задачей вашего повседневного программирования. Вполне возможно, что вам придётся создать исходный ZIP-архив, а затем постепенно добавлять к нему все новые файлы участники по мере их доступности в системе. В такой ситуации вам потребуется открывать и закрывать целевой ZIP-архив несколько раз.

С целью решения данной проблемы можно воспользоваться инициализацией объекта ZipFile в режиме добавления ("a"), что мы уже неоднократно делали в приведенных выше примерах. Благодаря этому режиму вы сможете безопасно добавлять в ZIP-архив новые файлы участники, не уничтожая его текущее содержимое:

>>> import zipfile

>>> def append_member(zip_file, member):
...     with zipfile.ZipFile(zip_file, mode="a") as archive:
...         archive.write(member)
...

>>> def get_file_from_stream():
...     """Имитация потока файлов."""
...     for file in ["hello.txt", "lorem.md", "realpython.md"]:
...         yield file
...

>>> for filename in get_file_from_stream():
...     append_member("incremental.zip", filename)
...

>>> with zipfile.ZipFile("incremental.zip", mode="r") as archive:
...     archive.printdir()
...
File Name                                        Modified             Size
hello.txt                                 2021-09-07 19:50:10           83
lorem.md                                  2021-09-07 19:50:10         2609
realpython.md                             2021-09-07 19:50:10          428

В этом примере объявляется функция append_member(), которая через свой второй аргумент member добавляет файл в целевой ZIP-архив, определяемый ее первым аргументом zip_file. Так как внутри данной функции используется with инструкция, то сама эта функция открывает и закрывает вышеназванный целевой архив каждый раз, когда вы ее вызываете. Таким образом, использования данной функции позволяет в любых участках программы и в любое время добавлять необходимые файлы к желаемым архивам столько раз, сколько вам нужно.

Также объявленная в данном примере функция get_file_from_stream() представляет собой функцию-генератор, имитирующую поток файлов для обработки их в соответствующем for цикле. В частности, внутри данного цикла функцией append_number() производится последовательное добавление файлов, порождаемых get_file_from_stream(), в архив incremental.zip. После выполнения данного кода в рабочей папке у вас должен появиться incremental.zip архив, содержащий три файла, переданные в цикле с помощью вышеназванной функции-генератора.

Извлечение файлов и каталогов (папок)

Одним из наиболее распространенных действий в отношении ZIP-архивов, конечно же является извлечение их содержимого в заданный каталог в нашей файловой системе. Мы с вами уже изучили основы использования методов .extract() и .extractall(), позволяющих извлекать соответственно один или все файлы из архива. Но, что если вам из архива нужно извлечь определенное подмножество файлов, заданное тем или иным конкретным критерием.

Вернемся к нашему sample.zip архиву из скаченных учебных материалов. На данный момент у вас в этом архиве должны находиться два файла с расширением .txt и два файла с расширением .md. Как же нам тут справится с задачей извлечения, например, файлов сугубо с расширением .md ? Давайте посмотрим, как это делается на примере нижеследующего кода:

>>> import zipfile

>>> with zipfile.ZipFile("sample.zip", mode="r") as archive:
...     for file in archive.namelist():
...         if file.endswith(".md"):
...             archive.extract(file, "output_dir/")
...
'output_dir/lorem.md'
'output_dir/realpython.md'

Весь код данного примера здесь прописан в with инструкции, которая открывает архив sample.zip в режиме чтения. Далее, цикл for перебирает каждый файл этого архиве, используя метод namelist(), а размещенный внутри этого цикла условный оператор if с помощью метода endswith() проверяет, заканчивается ли имя файла расширением .md. Если расширение проверяемого файла совпадает с условием, то он извлекается в целевую папку output_dir/ с помощью метода .extract().

Изучение дополнительных классов модуля ZIPFILE

Из предыдущих разделов этой статьи вы узнали о ZipFile и ZipInfo - двух классах, которые доступны в модули zipfile. Но, этот модуль предоставляет также и такие классы, как: zipfile.Path и zipfile.PyZipFile, которые в некоторых ситуациях могут оказаться крайне полезными. В следующих разделах мы познакомимся с функциями и основными принципами использования этих двух классов.

Сведения о файлах и файловых путях в ZIP-архивах

Когда вы открываете ZIP-файл в каком-либо архиваторе, вам всегда выводится информация о его внутренней структуре. Там, вы сможете увидеть массу файлов и папок, а перейдя в любую из этих папок, не исключено, что вы также наткнетесь еще на ряд подпапок с различным уровнем вложенности и множеством файлов в них. Таким образом, архив выглядит как обычная папка (каталог) в вашей файловой системе, где каждый файл, кроме имени имеет еще и свой путь доступа, состоящий из соответствующих папок и подпапок.

Класс zipfile.Path позволяет создавать объекты, которые кроме хранения информации о путях к файлам участникам и папкам внутри заданного ZIP-файла могут еще и управлять этими путями. Данный класс принимает два следующие аргумента:

  • root – указывает на целевой ZIP-файл, из которого требуется получит информацию. Данный аргумент может быть либо ZipFile объектом, либо строкой, указывающей путь к физическому ZIP-файлу.
  • at – строка пути, которая содержит месторасположения определенного файла участника или папки внутри архива. По умолчанию этот аргумент является пустой строкой, указывающей на корень архива.

Давайте посмотрим, как работает этот класс на примере уже знакомого нам sample.zip архива:

>>> import zipfile

>>> hello_txt = zipfile.Path("sample.zip", "hello.txt")

>>> hello_txt
Path('sample.zip', 'hello.txt')

>>> hello_txt.name
'hello.txt'

>>> hello_txt.is_file()
True

>>> hello_txt.exists()
True

>>> print(hello_txt.read_text())
Hello, Pythonista!

Welcome to Real Python!

Ready to try Python's zipfile module?

Этот код показывает возможности класса zipfile.Path в области реализации нескольких функций, аналогичных с функциями pathlib.Path объектов. В частности, из этого листинга видно, как применяя к объекту типа zipfile.Path, хранящемуся в переменной hello_txt, различные свойства и методы можно: получить имя файла с помощью свойства .name; проверить методом .is_file(), является ли данный элемент архива действительно файлом; посмотреть, существует ли вообще необходимый вам файл внутри целевого ZIP-архива, и многое, много другое.

Pathтакже предоставляет метод .open() для открытия файлов участников с использованием различных режимов. Например, код ниже открывает файл hello.txt в режиме чтения:

>>> import zipfile

>>> hello_txt = zipfile.Path("sample.zip", "hello.txt")

>>> with hello_txt.open(mode="r") as hello:
...     for line in hello:
...         print(line)
...
Hello, Pythonista!

Welcome to Real Python!

Ready to try Python's zipfile module?

Данный пример показывает, как к предварительно созданному zipfile.Path объекту, указывающему на файл hello.txt можно применить метод .open()и сразу же получить доступ к его содержимому.

Как в случае с pathlib.Path объектом, для zipfile.Path вы можете просмотреть содержимое ZIP-архива благодаря вызову метода .iterdir():

>>> import zipfile

>>> root = zipfile.Path("sample.zip")
>>> root
Path('sample.zip', '')

>>> root.is_dir()
True

>>> list(root.iterdir())
[
    Path('sample.zip', 'hello.txt'),
    Path('sample.zip', 'lorem.md'),
    Path('sample.zip', 'realpython.md')
]

Этот пример, как и ряд предыдущих листингов данного раздела вполне убеждают в том, что класс zipfile.Path предоставляет множество полезных функций, которыми можно воспользоваться для практически мгновенного управления файлами участниками в соответствующих ZIP-архивах.

Создание импортируемых Python пакетов с помощью PyZipFile

Еще одним полезным классом в модули zipfile является PyZipFile. Этот класс очень похож на ZipFile, но особенно удобен при сборе в ZIP-архивы Python модулей и пакетов с предварительной оптимизацией их кода. Именно поэтому, основным отличием данного класса от ZipFile является то, что инициализатор PyZipFile может принимать необязательный аргумент с именем optimize, позволяющий оптимизировать Python-код за счет его предварительной компиляции в байт-код при архивировании.

Предоставляя ту же функциональность, что и ZipFile, класс PyZipFile имеет еще и дополнительный метод .writepy(). Данный метод в качестве аргумента принимает файл Python (.py), который добавляется в целевой ZIP-архив исходя из значения optimize в инициализаторе класса. Если это значение равно -1 (по умолчанию), то .py файл сначала автоматически компилируется в .pyc файл, а затем уже добавляется в соответствующий целевой архив.

Обеспечиваемая PyZipFile возможность импорта Python кода из файлов ZIP, также известная, как Zip imports появилась еще в интерпретаторе Python версии 2.3. Сейчас данный функционал является крайне востребованным, поскольку позволяет создавать мпортируемые ZIP-файлы для дальнейшего распространения авторских модулей и пакетов в виде единого архива.

Примечание. Формат ZIP-архивов также может использоваться для создания и распространения исполняемых Python приложений, которые широко известны как приложения Python Zip. Но, изложение принципов построения таких приложений, к сожалению, весьма обширно и, требует написания отдельной статьи.

Как вы уже, наверное, поняли, с помощью PyZipFile файлы с расширением .py предварительно компилируются и размещаются в архивах с расширением .pyc. В свою очередь, импорт из таких архивов уже скомпилированного кода с последующим его выполнением в программах, делает ваши программы намного более быстрыми и эффективными.

полезно, когда вам нужно создать импортируемые ZIP-файлы. Упаковка файла, а не самого , делает процесс импорта более эффективным, поскольку он пропускает этап компиляции.

Итак, приступим к практике и, поработаем с hello.py модулем, лежащим в вашей папке python-zipfile/. Этот модуль имеет следующее содержимое:

"""Вывод приветственного сообщения"""
# hello.py

def greet(name="World"):
    print(f"Hello, {name}! Welcome to Real Python!")

В содержимом данного файла объявляется функция greet(), которая принимает аргумент name и выводит на экран приветственное сообщение. Теперь предположим, что мы хотим упаковать этот модуль в ZIP-файл для последующего распространения. Это можно сделать запустив следующий код:

>>> import zipfile

>>> with zipfile.PyZipFile("hello.zip", mode="w") as zip_module:
...     zip_module.writepy("hello.py")
...

>>> with zipfile.PyZipFile("hello.zip", mode="r") as zip_module:
...     zip_module.printdir()
...
File Name                                        Modified             Size
hello.pyc                                 2021-09-13 13:25:56          311

В этом примере после вызова метода .writepy() производится сначала автоматическая компиляция файла hello.py в файл hello.pyc, который затем добавляется в архив hello.zip. Это становится очевидным при выводе содержимого вышеназванного архива с помощью метода . printdir().

После формирования вышеупомянутого hello.zip архива, вы можете использовать так называемый Zip imports для импорта сохраненного и скомпилированного в этом архиве модуля hello в код вашей программы:

>>> import sys

>>> # Insert the archive into sys.path
>>> sys.path.insert(0, "/home/user/python-zipfile/hello.zip")
>>> sys.path[0]
'/home/user/python-zipfile/hello.zip'

>>> # Импорт кода для последующего использования
>>> import hello

>>> hello.greet("Pythonista")
Hello, Pythonista! Welcome to Real Python!

Для импорта кода из соответствующего ZIP-архива первым делом нужно обеспечить доступность данного архива в глобальной переменной операционной системы - sys.path. Эта переменная, в частности, должна содержать список строк, указывающих Python пути, где размещены его модули. При этом для добавления к sys.path новых элементов (путей), можно воспользоваться методом .insert().

Как видно из нашего примера, для обеспечения возможности импортирования модуля hello, мы предварительно переменной sys.path передаем путь хранения нашего hello.zip.

Примечание. В вашем конкретном случае, путь доступа к hello.zip может совсем не совпадать с тем, который указан в вышеприведенном листинге. Поэтому для того, чтобы данный листинг работал, вы должны скорректировать в нем строку пути для sys.path, так чтобы она указывала на действительное месторасположения вашего архива.

Как только путь к вашему импортируемому ZIP-архиву появится в списке переменной sys.path, вы сможете импортировать код из этого архива так же, как вы это неоднократно делали со стандартными модулей Python.

Далее, рассмотрим подпапку hello в нашей папке с учебным материалом. Эта подпапка содержит небольшой пакет Python со следующей структурой:

hello/
|
├── __init__.py
└── hello.py

Где, файл __init .py обеспечивает превращение папки hello/ в соответствующий Python пакет, а файл hello.py имеет то же содержание, что и в предыдущих примерах. Данный Python пакет может быть собран в соответствующий ZIP-архив следующим образом:

>>> import zipfile

>>> with zipfile.PyZipFile("hello.zip", mode="w") as zip_pkg:
...     zip_pkg.writepy("hello")
...

>>> with zipfile.PyZipFile("hello.zip", mode="r") as zip_pkg:
...     zip_pkg.printdir()
...
File Name                                        Modified             Size
hello/__init__.pyc                        2021-09-13 13:39:30          108
hello/hello.pyc                           2021-09-13 13:39:30          317

Из данного примера видно, что вызов .writepy() принимает hello пакет в качестве аргумента, а затем ищет в этом пакете .py файлы, компилирует их в .pyc и, наконец добавляет эти файлы в целевой ZIP-архив hello.zip. В итоге, код из вновь созданного и заархивированного пакета вы впоследствии сможете импортировать в любую свою программу, исходя из полученных знаний по работе с sys.path:

>>> import sys

>>> sys.path.insert(0, "/home/user/python-zipfile/hello.zip")

>>> from hello import hello

>>> hello.greet("Pythonista")
Hello, Pythonista! Welcome to Real Python!

Поскольку теперь ваш код находится в пакете, то предварительно вам нужно импортировать hello модуль из hello пакета. Затем вы можете получить доступ к своей greet() функции в обычном режиме.

Запуск ZIPFILE из командной строки

Кроме использования в коде наших программ, Python модуль zipfile также предусматривает минимальный интерфейс командной строки, позволяющий из консоли нам быстро получать доступ к основным своим функциям. Например, вы можете использовать параметр -l или --list для отображения содержимого, интересующего вас ZIP-архива:

$ python -m zipfile --list sample.zip
File Name                                         Modified             Size
hello.txt                                  2021-09-07 19:50:10           83
lorem.md                                   2021-09-07 19:50:10         2609
realpython.md                              2021-09-07 19:50:10          428
new_hello.txt                              1980-01-01 00:00:00           13

Эта команда показывает тот же результат, что и эквивалентный вызов метода .printdir() для sample.zip архива.

Теперь предположим, что вы хотите создать новый ZIP-файл, содержащий несколько входных файлов. В этом случае вы можете воспользоваться -c или --create опцией:

$ python -m zipfile --create new_sample.zip hello.txt lorem.md realpython.md

$ python -m zipfile -l new_sample.zip
File Name                                         Modified             Size
hello.txt                                  2021-09-07 19:50:10           83
lorem.md                                   2021-09-07 19:50:10         2609
realpython.md                              2021-09-07 19:50:10          428

Тут первая команда из нашего примера создает new_sample.zip архив, содержащий три файла: hello.txt, lorem.md и realpython.md, которые также указываются в данной команде

Но, что же вам делать в случае необходимости архивирования папки со всеми имеющимися в ней файлами? Например, у вас может быть своя собственная папка source_dir/ с теми же тремя файлами, что и в примере выше. В этом случае, вы можете создать ZIP-архив из этой папки с помощью следующей команды:

$ python -m zipfile -c source_dir.zip source_dir/

$ python -m zipfile -l source_dir.zip
File Name                                         Modified             Size
source_dir/                                2021-08-31 08:55:58            0
source_dir/hello.txt                       2021-08-31 08:55:58           83
source_dir/lorem.md                        2021-08-31 09:01:08         2609
source_dir/realpython.md                   2021-08-31 09:31:22          428

С помощью первой команды данного примера модуль zipfile помещает папку source_dir/ со все файлами в ней во вновь создаваемы архив source_dir.zip. Во второй команде этого примера, благодаря использованию в zipfile опции -l мы можем посмотреть содержимое внов созданного архива.

Примечание. Когда из командной строки вы создаете архив с помощью zipfile, то в процессе добавления файлов в этот архив, данный модуль по умолчанию неявно использует алгоритм сжатия Deflate.

Вы также можете извлечь все содержимое интересующего вас ZIP-архива, используя командную строку с -e или --extract опцией:

$ python -m zipfile --extract sample.zip sample/

После запуска этой команды у вас в вашем рабочем каталоге появится новая папка sample/, где будут размещены дубли всех файлов папок, содержащихся в sample.zip архиве.

Последней опцией, которую можно использовать в командной строке на основе zipfile, является опция -t или --test, которая позволяет проверить действительно ли интересующий вас ZIP-файл является архивом, записанным в корректном ZIP формате.

Использование дополнительных модулей для работы ZIP-архивами

Наряду с zipfile, в стандартной библиотеке Python есть еще несколько инструментов (модулей), которые на более низком уровне можно использовать для архивирования, сжатия и распаковки цифровой информации. Некоторые из этих инструментов, Python zipfile использует прямо внутри себя, в основном с целью сжатия данных. Ниже приведен краткий обзор некоторых наиболее популярных из этих дополнительных модулей:

Модуль Описание
zlib Позволяет сжимать и распаковывать с помощью библиотеки zlib
bz2 Предоставляет интерфейс для сжатия и распаковки данных с использованием алгоритма сжатия Bzip2.
lzma Предоставляет классы и функции для сжатия и распаковки данных с использованием алгоритма сжатия LZMA.

Также нужно отметить, что ряд модулей, заточенных на работу с архивами, в отличие от zipfil, дополнительно позволяют еще сжимать и распаковывать данные прямо из памяти или из иных потоков данных, несвойственных обычным файлам и архивам.

Из модулей в стандартной библиотеке Python следует особо упомянуть модуль tarfile, поддерживающий формат архивации TAR, и модуль под названием gzip, предоставляющий интерфейс для сжатия и распаковки данных, подобно тому, как это делает программа GNU Gzip.

Ниже приводится пример использования модуля gzip для создания сжатого текстового файла:

>>> import gzip

>>> with gzip.open("hello.txt.gz", mode="wt") as gz_file:
...     gz_file.write("Hello, World!")
...
13

В результате запуска этого кода, у вас в текущей папке появится архив hello.txt.gz, содержащий сжатую версию файла hello.txt с записанным в нем текстом Hello, World!.

Кроме zipfile, создать ZIP-архив быстрым высокоуровневым способом можно еще и за счет использования модуля shutil, который позволяет выполнять ряд операций высокого уровня над файлами и их коллекциями. Так, например, при архивировании данных из коллекции модуля shutil можно использовать метод make_archive(), который способен создавать архивы ZIP или TAR форматов:

>>> import shutil

>>> shutil.make_archive("shutil_sample", format="zip", root_dir="source_dir/")
'/home/user/shutil_sample.zip'

Данный код в вашей рабочей папке создает архив shutil_sample.zip, сжатый в ZIP-формате и содержащий внутри себя дубли всех файлов, лежащих в папке source_dir/. Вышеназванный метод make_archive(), прежде всего, удобен как быстрый, высокоуровневый и гибкий способ создания в Python архивов разного формата.

Выводы

В ходе изучения материала этой статьи мы познакомились с форматом ZIP -архивов, который позволяя упаковывать и при необходимости сжимать цифровые данные, априори стал отраслевым стандартом информационных технологий. Также мы узнали, что одним из наиболее удобных и многофункциональных инструментов для работы с этим форматом в Python является модуль его стандартной библиотеки zipfile, позволяющий читать, записывать, сжимать, распаковывать и извлекать файлы из ZIP-архивов

Популярность применения к цифровой информации ZIP-форматов обусловлена рядом преимуществ, позволяющих: осуществлять совместное архивирование связанных файлов, экономить место на диске, упрощать передачу данных по компьютерным сетям, оптимизировать код Python для дальнейшего распространения и многое, многое другое.

Из этой статьи вы также узнали, как:

  • Использовать Python zipfile для чтения, записи и извлеченияваших ZIP-архивов.
  • Прочитать метаданныесо сведениями о содержимом ZIP-архивов с помощью
  • Использовать zipfile для управления файлами участникамив уже имеющихся ZIP-архивах.
  • Создавать собственные ZIP-архивыдля упаковки и сжатия ваших цифровых данных.

Данная статья также познакомила вас с опциями командной строки, использующей модуль zipfile для просмотра, создания и извлечения ZIP-архивов. Таким образом, вполне очевидно, что, обладая всеми знаниями из этой статьи вы стопроцентно сможете максимально эффективно архивировать, сжимать и обрабатывать свои цифровые данные с использованием ZIP-формата.

Практический Python для начинающих
Практический Python для начинающих

Станьте junior Python программистом за 7 месяцев

 7 месяцев

Возможно будет интересно

🏆 Hello, world! Python
Новичок
🏆 Hello, world!

Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.

2022-10-04
Как практиковаться в Python? Python
Новичок
Как практиковаться в Python?

Для улучшения качества знаний и повышения уровня программиста, необходим постоянный практикум. Где можно это организовать самостоятельно, и как практиковаться в Python?

2022-10-19
Условные конструкции и сопоставление структурных шаблонов Шпаргалки
Новичок
Условные конструкции и сопоставление структурных шаблонов

Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов

2022-11-09