老齐教室

用 Python 压缩文件方法汇总

对于流行的文件压缩格式,如 tarzipgzipbz2 等,乃至于更奇特的 lzma 等格式,Python 都能轻易实现。本文将对有关压缩文件的问题给予阐述。

压缩格式以及相关模块

Python 提供了几乎为所有现有压缩文件的工具,下面逐一领略。

  • zlib 是一个 Python 库,能够实现 zipgzip 格式文件的压缩和解压缩。

  • bz2 模块提供了对 bzip2 格式的压缩支持。它也只对单个文件起作用,因此不能归档。

  • lzma 既是算法的名称,也是 Python 模块。它可以产生比一些旧方法更高的压缩比,并且是 xz (更具体地说是 LZMA2 )背后的算法。

  • gzip 是大多数人都熟悉的应用,此外它也是一个 Python 模块的名称。此模块使用前面提到的 zlib 压缩算法,并充当类似于实用程序 gzipgunzip的接口。

  • shutils 是一个模块,我们通常不把该模块与压缩和解压缩联系在一起。但它提供了处理归档文件的实用方法,便于生成 targztarzipbztar 或者 xztar 这些类型的归档文件。

  • 顾名思义,zipfile 允许我们用 Python 中实现 zip 归档,提供了创建、读取、写入或追加 zip 文件所需的所有方法,还提供了便于操作这些文件的类和对象。

  • 和上面的 zipfile 类似, tarfile 这个模块用于实现 tar 归档,可以读取和写入 gzipbz2lzma 文件或归档文件。 也支持与常规的 tar 压缩软件能实现的其他功能。

压缩与解压缩

上面列出了很多选择,它们中有一些比较基本,有一些具有许多其他功能,但共同点显然是包含压缩功能。下面就来看看有关基本操作。

先看 zlib ,这是一个相当低级的库,因此可能不太常用,让我们来看看针对整个文件的压缩或解压缩方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 方法创建压缩数据。然后将该数据写入输出文件。

为了证明能够恢复数据——解压缩,再次打开上述生成的压缩文件并对其通过 zlibbdecompress 方法。通过 print ,可以看到压缩和解压缩数据的大小都是匹配的。

下一个是 bz2 ,它的使用方式与上面的 zlib 非常相似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 ,为了避免反复显示相同的代码,这次执行增量压缩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 执行相同的任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

在这个例子中,结合了 gzipshutils 。看起来我们所做的批量压缩与之前使用 zlibbz2 的效果相同,但由于 shutil.copyfileobj 方法,我们实现了分块增量压缩,而不必像使用lzma那样循环数据。

gzip 模块的一个优点是:它还提供了命令行接口,我说的不是 Linux gzipgunzip ,而是 Python 中所集成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

更高效的工具

如果你熟悉 ziptar ,或者必须用其中的一种格式存档,就应该认真阅读下面的内容。除了基本的压缩或解压缩操作外,这两个模块还包括其他的一些实用方法,例如校验、使用密码、在归档文件中列出文件等。所以,很有必要深入研究一番,确保掌握这些技能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 (追加模式):

1
2
3
4
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的 zipfiletarfile 也提供 CLI 。要执行基本存档和提取,请使用以下命令:

1
2
3
4
5
6
7
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 ,但也实现了一些额外的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 方法的参数,返回文件对象,并将其解压缩到指定路径中。

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

结论

如你所见,Python 的提供了包括低级和高级、特定和通用、简单和复杂的各类模块或库。可以根据实际需要进行选择,通常建议使用通用模块,如 zipfiletarfile ,只有在必要时才使用 lzma 之类的模块。

当然,要想熟练使用以上各个模块的各种方法,还是要阅读官方文档。

参考文献

https://towardsdatascience.com/all-the-ways-to-compress-and-archive-files-in-python-e8076ccedb4b

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能