对于流行的文件压缩格式,如 tar
、zip
、gzip
、bz2
等,乃至于更奇特的 lzma
等格式,Python 都能轻易实现。本文将对有关压缩文件的问题给予阐述。
压缩格式以及相关模块
Python 提供了几乎为所有现有压缩文件的工具,下面逐一领略。
zlib
是一个 Python 库,能够实现zip
、gzip
格式文件的压缩和解压缩。bz2
模块提供了对bzip2
格式的压缩支持。它也只对单个文件起作用,因此不能归档。lzma
既是算法的名称,也是 Python 模块。它可以产生比一些旧方法更高的压缩比,并且是xz
(更具体地说是 LZMA2 )背后的算法。gzip
是大多数人都熟悉的应用,此外它也是一个 Python 模块的名称。此模块使用前面提到的zlib
压缩算法,并充当类似于实用程序gzip
和gunzip
的接口。shutils
是一个模块,我们通常不把该模块与压缩和解压缩联系在一起。但它提供了处理归档文件的实用方法,便于生成tar
、gztar
、zip
、bztar
或者xztar
这些类型的归档文件。- 顾名思义,
zipfile
允许我们用 Python 中实现 zip 归档,提供了创建、读取、写入或追加zip
文件所需的所有方法,还提供了便于操作这些文件的类和对象。 - 和上面的
zipfile
类似,tarfile
这个模块用于实现tar
归档,可以读取和写入gzip
、bz2
和lzma
文件或归档文件。也支持与常规的tar
压缩软件能实现的其他功能。
压缩与解压缩
上面列出了很多选择,它们中有一些比较基本,有一些具有许多其他功能,但共同点显然是包含压缩功能。下面就来看看有关基本操作。
先看 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
上面的代码中所需要的输入文件,可以用 head -c 1MB </dev/zero > data
指令生成,此文件由零组成且大小为 1MB 。将文件读入内存滞后,用 zlib
中的 compress
方法创建压缩数据。然后将该数据写入输出文件。
为了证明能够恢复数据——解压缩,再次打开上述生成的压缩文件并对其通过 zlibb
的 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
不出所料,使用方法大同小异。为了显示一些不同之处,在上面的示例中,我们简化了压缩步骤,将其减少到1行,并使用 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 位块中迭代随机数据,并使用 LZMACompressor.compress
方法压缩它们。然后将这些块写入输出文件。在读取和压缩整个文件之后,我们需要调用 flush
,以完成压缩过程、并从压缩器中清除任何剩余数据。
为了证实上述操作的有效性,我们以通常的方式打开并解压缩文件,并从文件中打印出几个单词。
下面要研究高级别的模块。现在使用 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
,或者必须用其中的一种格式存档,就应该认真阅读下面的内容。除了基本的压缩或解压缩操作外,这两个模块还包括其他的一些实用方法,例如校验、使用密码、在归档文件中列出文件等。所以,很有必要深入研究一番,确保掌握这些技能。
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) # also zf.extractall()
上述代码有点长,它涵盖了 zipfile
模块的所有重要功能。在这段代码中,首先在 with
上下文管理中,以 w
模式使用 ZipFile
创建 ZIP 归档文件,然后将文件添加到归档文件中。你会注意到,实际上不需要打开要添加的文件 —— 我们所需要做的就是调用 write
方法,并传入文件名为参数。添加所有文件后,我们还使用 setpassword
方法设置存档密码。
接下来,为了证明这种操作方法的有效性,打开归档文件。在读取任何文件之前,检查CRC和文件头,然后检索存档中所有文件的信息。在本例中,我们只打印 ZipInfo
对象的列表,但你也可以检查其属性,以获得CRC、大小、压缩类型等。
检查完所有文件后,打开并读取其中一个文件。我们看到它具有预期的内容,所以可以继续并将其解压缩都指定路径(/tmp/
)。
除了创建和读取归档文件或普通文件外,ZIP 还允许我们将文件追加到现有的存档中。为此,只需将访问模式更改为 a
(追加模式):
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
模块相同,Python的 zipfile
和 tarfile
也提供 CLI 。要执行基本存档和提取,请使用以下命令:
python3 -m zipfile -c arch.zip words1.txt words2.txt # Create
python3 -m zipfile -t arch.zip # Test
Done testing
python3 -m zipfile -e arch.zip /tmp # Extract
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) # can also be dir (added recursively), symlink, etc
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 contains words1.txt with permissions 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
方法,它提供了所有 Linux 文件属性。
tarfile
提供了一个我们在其他模块中没有看到的很酷的特性,那就是在将文件添加到归档文件时能够修改文件的属性。在上面的代码片段中,通过提供 filter
参数来更改文件的权限,该参数修改了 TarInfo.mode
。此值必须作为八进制数提供,此处的 0o100600
将权限设置为 0600
或 -rw-------.
。
为了在进行此更改后获得文件的完整概览,我们可以运行 list
方法,它提供类似于 ls -l
的输出。
使用tar
存档的最后一件事是打开它并将其解压缩。为此,我们使用 "r:gz"
模式打开它,以文件名作为 getmember
方法的参数,返回文件对象,并将其解压缩到指定路径中。
with tarfile.open(archive, "r:gz") as tar:
member = tar.getmember("words3.txt")
if member.isfile():
tar.extract(member, "/tmp/")
结论
如你所见,Python 的提供了包括低级和高级、特定和通用、简单和复杂的各类模块或库。可以根据实际需要进行选择,通常建议使用通用模块,如 zipfile
或 tarfile
,只有在必要时才使用 lzma
之类的模块。
当然,要想熟练使用以上各个模块的各种方法,还是要阅读官方文档。
参考文献
https://towardsdatascience.com/all-the-ways-to-compress-and-archive-files-in-python-e8076ccedb4b