Упаковываем и распаковываем файлы, а также управляем файловыми архивами в любых нужных нам форматах с помощью Python.

Упаковываем и распаковываем файлы, а также управляем файловыми архивами в любых нужных нам форматах с помощью Python.

Стандартная библиотека Python предоставляет модули и инструменты для решения практически любой прикладной задачи, и как вы понимаете, работа с файловыми архивами не является исключением. При этом будь то самые распространенные форматы архивации такие, как tar или zip, более специфические – gzip и bz2, и совсем экзотические – lzma, в стандартной библиотеке Python есть все.

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

О форматах сжатия

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

  • zlibмодуль Python, созданный на основе одноименной библиотеки zlib, который предоставляет функционал для работы с файловыми архивами, упакованными с использованием алгоритма Deflate. Он применяется для сжатия и распаковки архивов файлов в формате zip, gzipи многих других. Итак, используя этот модуль, вы, по сути, используете gzip совместимый алгоритм сжатия, но с более удобным интерфейсом. Подробнее об этой библиотеке можно найти в Википедии .
  • bz2модуль, обеспечивающий поддержку алгоритма сжатия bzip2. Этот алгоритм более эффективен по степени сжатия, чем алгоритм Deflate, но работает достаточно медленно. Еще одним его недостатком является то, что он применим для сжатия только отдельных файлов и поэтому с его использованием нельзя создавать файловый архивы, содержащие файлы и директории с файлами.
  • lzma – это имя алгоритма и модуля Python . Он позволяет обеспечить более высокую степень компрессии, чем большинство «старых» алгоритмов сжатия, а также является алгоритмом, лежащим в основе функционирования утилиты xz.
  • gzip – это модуль стандартной библиотеки Python, с которым знакомо большинство из нас. Он использует уже упомянутый алгоритм сжатия zlib и служит интерфейсом, по функционалу схожим с модулями gzipи gunzip.
  • shutilsмодуль, который мы обычно не связываем с операциями компрессии и декомпресси файлов, но он предоставляет вспомогательные методы для работы с архивами для управления архивами в форматах tar, gztar, zip, bztarили xztar.
  • zipfileмодуль, который как следует из названия, позволяет работать в Python с zip архивами. Он предоставляет все необходимые методы для создания, чтения, записи или добавления файлов в ZIP-архивы, а также классы и объекты для упрощения работы с ними.
  • tarfile – этот модуль, как и zipfile выше, используется для работы с tar архивами. Он помогает работать с файловыми архивами gzip, bz2а также lzma . А также поддерживает другие функции, которые присутствуют в утилите tar, предоставляемой операционной системой Linux.

Упаковываем и распаковываем файлы

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

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

import zlib, sys

filename_in = "data"
filename_out = "compressed_data"

with open(filename_in, mode="rb") as fin, open(filename_out, mode="wb") as fout:
    data = fin.read()
    compressed_data = zlib.compress(data, zlib.Z_BEST_COMPRESSION)
    print(f"Original size: {sys.getsizeof(data)}")
    # Original size: 1000033
    print(f"Compressed size: {sys.getsizeof(compressed_data)}")
    # Compressed size: 1024

    fout.write(compressed_data)

with open(filename_out, mode="rb") as fin:
    data = fin.read()
    compressed_data = zlib.decompress(data)
    print(f"Compressed size: {sys.getsizeof(data)}")
    # Compressed size: 1024
    print(f"Decompressed size: {sys.getsizeof(compressed_data)}")
    # Decompressed size: 1000033

В примере кода выше в качестве “подопытного” мы используем файл с именем data. В общем случае конечно же можно взять любой файл, но, например, в ОС Linux можно сгенерировать его с помощью команды head -c 1MB </dev/zero > data . В результате получаем файл с размером 1 МБ и бинарным содержимым в виде нулей. Далее мы открываем и считываем его в память, а затем используем функцию compress для создания сжатых данных, которые затем записываются в выходной файл compressed_data. Чтобы продемонстрировать то, что мы можем восстановить данные обратно, мы снова открываем сжатый файл и используем для его обработки функцию decompress. Операторы print выводят в терминале информацию, что размеры как сжатых, так и распакованных файлов данных совпадают.

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

import bz2, os, sys

filename_in = "data"
filename_out = "compressed_data.bz2"

with open(filename_in, mode="rb") as fin, bz2.open(filename_out, "wb") as fout:
    fout.write(fin.read())

print(f"Uncompressed size: {os.stat(filename_in).st_size}")
# Uncompressed size: 1000000
print(f"Compressed size: {os.stat(filename_out).st_size}")
# Compressed size: 48

with bz2.open(filename_out, "rb") as fin:
    data = fin.read()
    print(f"Decompressed size: {sys.getsizeof(data)}")
    # Decompressed size: 1000033

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

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

import lzma, os
lzc = lzma.LZMACompressor()

# cat /usr/share/dict/words | sort -R | head -c 1MB > data
filename_in = "data"
filename_out = "compressed_data.xz"

with open(filename_in, mode="r") as fin, open(filename_out, "wb") as fout:
    for chunk in fin.read(1024):
        compressed_chunk = lzc.compress(chunk.encode("ascii"))
        fout.write(compressed_chunk)
    fout.write(lzc.flush())

print(f"Uncompressed size: {os.stat(filename_in).st_size}")
# Uncompressed size: 972398
print(f"Compressed size: {os.stat(filename_out).st_size}")
# Compressed size: 736

with lzma.open(filename_out, "r") as fin:
    words = fin.read().decode("utf-8").split()
    print(words[:5])
    # ['dabbing', 'hauled', "seediness's", 'Iroquoian', 'vibe']

Как и в примерах выше, мы начинаем с создания исходного файла, состоящего из набора слов, извлеченных из словаря в /usr/share/dict/words. Это сделано для того, чтобы мы смогли впоследствии проверить, что распакованные данные идентичны оригиналу.

Затем открываем файл с исходными данными и новый для вывода результата сжатия его содержимого, как в предыдущих примерах. Однако на этот раз мы перебираем содержимое 1024-битными блоками chunk и сжимаем их с помощью метода LZMACompressor.compress. Далее сжатые данные записываются в выходной файл. После того, как весь исходный файл будет прочитан по частям и сжат, нам нужно вызвать функцию flush, чтобы завершить процесс сжатия и удалить все оставшиеся данные из буфера объекта «компрессора» lzc.

Чтобы убедиться, что все работает так, как нам надо, открываем и распаковываем файл обычным способом, а затем выводим в окно терминала пару слов из файла.

Переходим к модулям более высокого уровня – давайте теперь используем модуль gzip для тех же задач:

import os, sys, shutil, gzip

filename_in = "data"
filename_out = "compressed_data.tar.gz"

with open(filename_in, "rb") as fin, gzip.open(filename_out, "wb") as fout:
    # Reads the file by chunks to avoid exhausting memory
    shutil.copyfileobj(fin, fout)

print(f"Uncompressed size: {os.stat(filename_in).st_size}")
# Uncompressed size: 1000000
print(f"Compressed size: {os.stat(filename_out).st_size}")
# Compressed size: 1023

with gzip.open(filename_out, "rb") as fin:
    data = fin.read()
    print(f"Decompressed size: {sys.getsizeof(data)}")
    # Decompressed size: 1000033

В этом примере мы объединили возможности модулей gzip и shutils. И может показаться, что ранее мы делали те же базовые операции сжатие/распаковка, как в примерах выше с модулями zlib и bz2. Но благодаря функции shutil.copyfileobj мы можем производить поблочное инкрементное сжатие файла без необходимости осуществлять непосредственно перебор блоков данных в цикле, как мы это делали в примере использования модуля lzma.

Одним из преимуществ модуля gzip является то, что он предоставляет интерфейс для управления работой модуля из командной строки. И это работает не только в Linux, модули gzip и gunzip интегрированы непосредственно в установочные пакеты Python, и могут вызываться из командной сроки следующим образом:

python3 -m gzip -h
usage: gzip.py [-h] [--fast | --best | -d] [file [file ...]]
...

ls -l data*
-rw-rw-r-- 1 martin martin 1000000 aug 22 18:48 data

# Use fast compression on file "data"
python3 -m gzip --fast data

# File named "data.gz" was generated:
ls -l data*
-rw-rw-r-- 1 martin martin 1000000 aug 22 18:48 data
-rw-rw-r-- 1 martin martin    1008 aug 22 20:50 data.gz

Берем Большой молоток

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

import zipfile

# shuf -n5 /usr/share/dict/words > words.txt
files = ["words1.txt", "words2.txt", "words3.txt", "words4.txt", "words5.txt"]
archive = "archive.zip"
password = b"verysecret"

with zipfile.ZipFile(archive, "w") as zf:
    for file in files:
        zf.write(file)

    zf.setpassword(password)

with zipfile.ZipFile(archive, "r") as zf:
    crc_test = zf.testzip()
    if crc_test is not None:
        print(f"Bad CRC or file headers: {crc_test}")

    info = zf.infolist()  # also zf.namelist()
    print(info)  # See all attributes at https://docs.python.org/3/library/zipfile.html#zipinfo-objects
    # [ <ZipInfo filename='words1.txt' filemode='-rw-r--r--' file_size=37>,
    #   <ZipInfo filename='words2.txt' filemode='-rw-r--r--' file_size=47>,
    #   ... ]

    file = info[0]
    with zf.open(file) as f:
        print(f.read().decode())
        # Olav
        # teakettles
        # ...

    zf.extract(file, "/tmp", pwd=password)  # также как и zf.extractall()

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

Далее, чтобы продемонстрировать, что все сработало так как надо, открываем архив. Перед чтением файлов из него с помощью метода testzip проверяем соответствие их контрольной суммы CRC и значения, указанного в заголовке. Затем используя метод infolist получаем информацию о файлах, находящихся в архиве. В этом примере мы просто выводим в терминале список объектов типа ZipInfo, но вы также можете получить значение их атрибутов: получить CRC, размер, параметры сжатия и т.д.

После проверки файлов в архиве открываем и прочитаем один из них, а затем выведем его содержимое в терминале. Мы видим, что его содержимое соответствует ожидаемому, поэтому можем продолжить и извлечь его в новый файл по заданному пути /tmp/.

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

with zipfile.ZipFile(archive, "a") as zf:
    zf.write("words6.txt")
    print(zf.namelist())
    # ['words1.txt', 'words2.txt', 'words3.txt', 'words4.txt', 'words5.txt', 'words6.txt']

Как и в случае с модулем gzip, модули zipfile и tarfile также предоставляют интерфейс командной строки. Чтобы выполнить в терминале простейшие операции архивирования и распаковки, можно воспользоваться следующими приемами:

python3 -m zipfile -c arch.zip words1.txt words2.txt  # архивируем
python3 -m zipfile -t arch.zip  # проверяем
Done testing

python3 -m zipfile -e arch.zip /tmp  # распаковываем
ls /tmp/words*
/tmp/words1.txt  /tmp/words2.txt

И последнее, но не менее важное. Рассмотрим подробнее модуль tarfile. Синтаксис его использования схож с zipfile, но он предоставляет некоторые дополнительные функции:

import tarfile

files = ["words1.txt", "words2.txt", "words3.txt", "words4.txt"]
archive = "archive.tar.gz"

with tarfile.open(archive, "w:gz") as tar:
    for file in files:
        tar.add(file)  # выможете также добавить директории, ее содержимое рекурсивно также будет добавлено в архив, включая ссылки symlink

    print(f"archive contains: {tar.getmembers()}")
    # [<TarInfo 'words1.txt' at 0x7f71ed74f8e0>,
    #  <TarInfo 'words2.txt' at 0x7f71ed74f9a8>
    #  ... ]

    info = tar.gettarinfo("words1.txt")  # Other Linux attributes - https://docs.python.org/3/library/tarfile.html#tarinfo-objects
    print(f"{tar.name} contains {info.name} with permissions {oct(info.mode)[-3:]}, size: {info.size} and owner: {info.uid}:{info.gid}")
    # .../archive.tar содержит words1.txt со следующими полномочиями доступа 644, size: 37 and owner: 500:500

    def change_permissions(tarinfo):
        tarinfo.mode = 0o100600  # -rw-------.
        return tarinfo
    
    tar.add("words5.txt", filter=change_permissions)

    tar.list()
    # -rw-r--r-- martin/martin   37 2021-08-23 09:01:56 words1.txt
    # -rw-r--r-- martin/martin   47 2021-08-23 09:02:06 words2.txt
    # ...
    # -rw------- martin/martin   42 2021-08-23 09:02:22 words5.txt

Мы начинаем с базовой операции создания архива, но, как можем заметить, здесь мы используем режим доступа "w:gz", который указывает, что мы хотим использовать определенный тип сжатия GZ. После этого мы добавляем все необходимые файлы в архив. С помощью модуля tarfile мы также можем добавлять в архив символические ссылки или целые директории с содержимым, которое будет добавлено в архив рекурсивно.

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

Модуль tarfile содержит еще одну интересную функцию, которую мы не увидим у других, ранее рассмотренных модулей, а именно возможность изменять атрибуты файлов при их добавлении в архив. В приведенном выше фрагменте кода мы изменяем режим доступа к файлу, передав в качестве параметра функцию filter, которая изменяет значение соответствующего атрибута файла TarInfo.mode. Это значение должно быть представлено в виде восьмеричного числа, в нашем примере это 0o100600. Вызов функции для добавляемого в архив файла устанавливает его права доступа 0600 или -rw-------.

Чтобы получить полный список файлов после добавления их в архив, вызываем метод list, который выводит в терминале результат, аналогичный команде ls -l.

Последнее, что мы еще можем сделать с tar архивом – это открыть его и извлечь содержимое. Для этого открываем его в режиме "r:gz", явно указывая тип сжатия. Получаем объект member, который содержит информацию о всех файлах, упакованных в архиве. Используя строковое значение, соответствующее имени файла "words3.txt" , проверяем, действительно ли он находится в архиве, и извлекаем его в указанное место:

with tarfile.open(archive, "r:gz") as tar:
    member = tar.getmember("words3.txt")
    if member.isfile():
        tar.extract(member, "/tmp/")

Заключение

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

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

Оставить комментарий