Python zipfile:高效处理 ZIP 文件(翻译)

本文最后更新于:2022年3月13日 晚上

翻译信息

原文链接:Python’s zipfile: Manipulate Your ZIP Files Efficiently

作者:Leodanis Pozo Ramos

译者:muzing

翻译时间:2022.3

允许转载,转载必须保留全部翻译信息,且不能对正文有任何修改

Python 的 zipfile 是一个旨在操作 ZIP 文件的标准库模块。在归档和压缩数字数据时,该文件格式是一种广泛采用的行业标准。可以使用它将几个相关文件打包在一起。它还可以减小文件大小并节省磁盘空间。最重要的是,它促进了计算机网络上的数据交换。

作为 Python 开发者或 DevOps 工程师,了解如何使用 zipfile 模块创建、读取、写入、填充、提取和列出 ZIP 文件是一项有用的技能。

在本教程中,您将学到如何:

  • 使用 Python 的 zipfile 从 ZIP 文件中读取、写入和提取文件
  • 使用 zipfile 读取关于 ZIP 文件内容的元数据metadata
  • 使用 zipfile 来操作现有 ZIP 文件中的成员文件
  • 创建新的 ZIP 文件以存档和压缩文件

如果您经常处理 ZIP 文件,那么这些知识可以帮助您精简工作流程,从而自信地处理文件。

为充分使用本教程,您应该了解处理文件、使用 with 语句、使用 pathlib 处理文件系统路径,以及使用类和面向对象编程

要获取您将用于编写本教程中的示例的文件和存档,请查看本文 附A:材料下载 一节。

ZIP 文件入门

ZIP 文件 是当今数字世界中最广为人知和流行的工具。这些文件相当流行,广泛用于计算机网络(尤其是 Internet)上的跨平台数据交换。

您可以使用 ZIP 文件将常规文件打包到一个归档中,压缩数据以节省一些磁盘空间,分发数字产品等等。在本教程中,您将学习如何使用 Python 的 zipfile 模块操作 ZIP 文件。

由于关于 ZIP 文件的术语有时让人感到困惑,因此本教程将遵循以下有关术语的约定:

术语 含义
ZIP 文件, ZIP 归档,或归档 使用 ZIP 格式的物理文件
文件 常规的计算机文件
成员文件 作为现有 ZIP 文件的一部分的文件

将这些术语牢记于心,有助于避免在阅读以下部分时混淆。现在您已经准备好继续学习如何在 Python 代码中高效操作 ZIP 文件!

什么是 ZIP File?

您可能已经遇到并使用过 ZIP 文件。是的,文件扩展名为 .zip 的文件无处不在!ZIP 文件,也称为ZIP 归档,是使用 ZIP 文件格式的文件。

PKWARE 是创建并首先实现此文件格式的公司。该公司整理并维护了当前的格式规范,该规范为公开可用的,允许创建使用 ZIP 文件格式读写文件的产品、程序和进程。

ZIP 文件格式是一种跨平台、可互操作的文件存储和传输格式。它结合了无损数据压缩、文件管理以及数据加密

数据压缩并非一个归档成为 ZIP 文件的必选项。因此您可以在 ZIP 归档中包含压缩或未压缩的成员文件。ZIP 文件格式支持数种压缩算法,但其中 Deflate 是最常见的。该格式还支持使用 CRC32 进行信息完整性检查。

尽管还有其他类似的归档格式,比如 RARTAR 文件,但 ZIP 文件格式已经迅速成为高效数据存储和通过计算机网络进行数据交换的通用标准。

ZIP 文件无处不在。例如,Microsoft OfficeLibre Office 等 office 套件依赖 ZIP 文件格式作为其 文档容器文件。这意味着 .docx.xlsx.pptx.odt.ods.odp 文件实际上是包含构成每个文档的多个文件和文件夹的 ZIP 归档。其他使用 ZIP 格式的常见文件包括 .jar.war.epub 文件。

您可能熟悉 GitHub,它为使用 Git 的软件开发和版本控制提供 Web 托管。当您将软件下载到本地计算机时,GitHub 使用 ZIP 文件打包软件项目。例如,您可以在 ZIP 文件中下载 Python Basics: A Practical Introduction to Python 3 书的练习解决方案,或者下载您选择的其他任何项目。

ZIP 文件运行您将文件聚合、压缩和加密到单个可互操作且可移植的容器中。您可以流式传输 ZIP 文件、将它们分割成段、使其自解压等。

为什么使用 ZIP Files?

了解如何创建、读取、写入和提取 ZIP 文件对于使用计算机和数字信息的开发者和专业人士来说可能是一项有用的技能。除其他优点外,ZIP 文件还允许您:

  • 在不丢失信息的前提下减小文件大小和存储需求
  • 由于减小了文件大小和单文件传输,提高了网络传输速度
  • 将多个文件打包到一个归档中,以实现高效管理
  • 将您的代码打包到一个归档中,以进行分发
  • 通过使用加密来保护数据,这是当今的普遍需求
  • 保证信息完整性,避免对数据意外或恶意更改

如果您正在寻找一种灵活、可移植且可靠的方式来归档您的数字文件,这些特性使得 ZIP 文件成为 Python 工具箱的实用补充。

Python 能处理 ZIP 文件吗?

是的!Python 有几个工具可以让您操作 ZIP 文件。其中一些工具在 Python 标准库 中可用。它们包括用于使用特定压缩算法(例如 zlibbz2lzma其他)压缩和解压缩数据的低级库。

Python 还提供了一个名为“zipfile”的高级模块,专门用于创建、读取、写入、提取和列出 ZIP 文件的内容。 在本教程中,您将了解 Python 的 zipfile 以及如何有效地使用它。

使用 Python 的 zipfile 操纵已存在的 ZIP 文件

Python 的 zipfile 提供了便于使用的类和函数,允许创建、读取、写入、提取和列出 ZIP 文件包含的内容。以下是 zipfile 支持的一些附加功能:

  • 大于 4 GiB 的 ZIP 文件(ZIP64 files
  • 数据解密
  • 多种压缩算法,例如 Deflate、 Bzip2LZMA
  • 使用 CRC32 进行信息完整性检查

请注意,zipfile 确实有一些局限性。例如,当前的数据解密功能可能非常慢,因为它使用纯 Python 代码。该模块无法处理加密 ZIP 文件的创建。最后,也不支持使用多磁盘(multi-disk) ZIP 文件。尽管有这些局限性,zipfile 仍然是一个很棒且实用的工具。继续阅读以探索其功能。

打开 ZIP 文件进行读写

zipfile 模块中,您会找到 ZipFile 类。这个类的工作方式很像 Python 内置的 open() 函数,允许使用不同的模式打开 ZIP 文件。读取模式("r")为默认值。也可以使用写入("w")、追加("a")和独占("x")模式。稍后您将详细学习其中每一项。

zipfile 实现了上下文管理器协议,以便于在一个 with 语句中使用该类。此特性允许您快速打开和使用 ZIP 文件,而无需担心在完成工作后关闭文件。

在编写任何代码之前,请确保您拥有将要使用的文件和归档的副本:

要获取您将用于编写本教程中的示例的文件和存档,请查看本文 附A:材料下载 一节。

请将下载的资源移入您的家目录(home folder)下名为 python-zipfile/ 的目录中,以准备好工作环境。将文件放在正确的位置后,移动至新创建的目录并在那里启动 Python 交互式会话。

译者注:对于 Windows 10/11 用户,可以在任意路径创建该目录。在文件资源管理器中进入目录后,按住 Shift 键的同时鼠标右键单击空白处,然后选择“在此处打开 Powershell 窗口”,输入 python 命令即可。

首先从读取名为 sample.zip 的 ZIP 文件开始热身。为此,可以在读取模式下使用 ZipFile

1
2
3
4
5
6
7
8
9
>>> 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 文件的路径。这个参数也可以接受文件对象路径类对象。在此示例中,使用了基于字符串的路径。

ZipFile 的第二个参数是一个单字母的字符串,表示用于打开文件的模式。正如您在本节开头所了解的,根据需求,ZipFile 可以接受四种可能的模式。mode 位置参数默认为 "r",所以如果想以只读模式打开归档,可以省略它。

with 语句中,在 archive 上调用 .printdir()archive 变量现在包含 ZipFile 本身的实例。此函数提供了一种在屏幕上显示底层 ZIP 文件内容的快捷方法。其输出为易读的表格形式,有三列信息:

  • File Name
  • Modified
  • Size

如果您想在尝试打开之前确保目标为有效的 ZIP 文件,那么可以将 ZipFile 包装在 tryexcept 语句中,并捕获任何 BadZipFile 异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> 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() 函数:

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

在这些示例中,使用一个 is_zipfile() 作为条件的条件语句。该函数接受一个保存文件系统中 ZIP 文件的路径的 filename 参数。此参数可以接受字符串、类文件或类路径对象。如果 filename 是有效的 ZIP 文件,则该函数返回 True。否则返回 False

现在假设您想使用 ZipFilehello.txt 添加到 hello.zip 归档中。为此,可以使用写入模式("w")。该模式打开一个 ZIP 文件进行写入。如果目标 ZIP 文件存在,则 "w" 模式会截断它并写入您传入的任何新内容。

Note: 如果您使用 ZipFile 处理现有文件,那么应该小心使用 "w" 模式。您可能截断 ZIP 文件并丢失所有原有内容。

如果目标 ZIP 文件不存在,则 ZipFile 会在您关闭归档时为您创建它:

1
2
3
4
5
>>> 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() 的参数应是已存在的文件。

Note: 当您在写模式下使用类且目标归档不存在时, ZipFile 足够智能,可以创建一个新的归档。然而,如果这些目录尚不存在,则该类不会在目标 ZIP 文件的路径中创建新目录。

这就解释了为何如下代码无法工作:

1
2
3
4
5
6
7
8
>>> 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'

因为目标 hello.zip 文件路径中的 missing/ 目录不存在,所以会出现 FileNotFoundError 异常。

追加模式("a")允许您将新的成员文件追加到现有 ZIP 文件。此模式不会截断归档,故其原始内容是安全的。如果目标 ZIP 文件不存在,则 "a" 模式会为您创建一个新文件,然后追加作为参数传入 .write() 的任何文件。

为尝试 "a" 模式,更进一步添加 new_hello.txt 文件到新创建的 hello.zip 归档中:

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

最后,如果您使用 "w""a""x" 模式创建一个 ZIP 文件,然后在不添加任何成员文件的情况下关闭归档,则 ZipFile 会创建一个具有适当 ZIP 格式的空归档。

从 ZIP 文件中读取元数据

您已将 .printdir() 付诸实践。这是一种实用的方法,可用于快速列出 ZIP 文件包含的内容。与 .printdir() 一道,ZipFile 类提供了几种从现有 ZIP 文件中提取元数据的便捷方法。

以下是这些方法的摘要:

方法 描述
.getinfo(filename) 返回一个关于 filename 提供的成员文件的信息的 ZipInfo 对象。注意 filename 必须包含底层 ZIP 文件中目标文件的路径。
.infolist() 返回一个 ZipInfo 对象列表,每个文件占一项。
.namelist() 返回一个包含底层归档中所有成员文件名的列表。该列表中的名称是 .getinfo() 的有效参数。

使用这三个工具,您可以检索许多关于 ZIP 文件内容的实用信息。例如,下面的例子使用了 .getinfo()

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
34
35
36
37
38
39
40
41
42
43
>>> import zipfile

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

>>> info.file_size
83
>>> 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
--------------------

>>> info.compress_size
83

>>> info.filename
'hello.txt'

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

正如您在上表中所了解到的,.getinfo() 将成员文件作为参数,并返回一个包含关于其信息的 ZipInfo 对象。

Note: ZipInfo 并不想要被直接实例化。.getinfo().infolist() 方法在被调用时会自动返回 ZipInfo 对象。然而,ZipInfo 包含一个名为 .from_file() 的类方法,它允许您在需要时显式地实例化该类。

ZipInfo 对象有一些属性,可以检索有关目标成员文件的实用信息。例如,.file_size.compress_size 分别保存未压缩原始文件和压缩后文件的大小(以字节为单位)。该类还有一些其他实用的属性,例如 .filename.date_time ,它们返回文件名和最后修改日期。

Note: 默认情况下,ZipFile 不会压缩输入文件以将其添加到最终归档中。这就是上例中 size 和 compressed size 大小相同的原因。您将在下面的 压缩文件和目录 部分了解有关此话题的更多信息。

使用 .infolist() ,您可以从指定归档中提取所有文件信息。以下是一个使用此方法生成最小报告的示例,其中包含有关 sample.zip 归档中所有成员文件的信息:

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
>>> 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 循环迭代来自 .infolist()ZipInfo 对象,检索文件名、最后修改日期、未压缩大小 ,以及每个成员文件的压缩后大小。在此示例中,使用 datetime 以人类易读的方式格式化日期。

Note: 上面的例子改编自zipfile — ZIP Archive Access

如果只需要对 ZIP 文件执行快速检查并列出其成员文件的名称,那么可以使用 .namelist()

1
2
3
4
5
6
7
8
9
>>> import zipfile

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

因为此输出中的文件名是 .getinfo() 的有效参数,所以可以结合这两种方法来仅检索关于特定成员文件的信息。

例如,可能有一个 ZIP 文件,其中包含不同类型的成员文件(.docx.xlsx.txt 等)。但并不需要使用 .infolist() 获取完整信息,只需获取有关 .docx 文件的信息。那么可以按扩展名过滤文件并仅在 .docx 文件上调用 .getinfo() 。 来试一试吧!

读写成员文件

有时需要在不解压一个 ZIP 文件的情况下读取指定成员文件。为此,可以使用 .read()。此方法接收一个成员文件的 name 并将该文件的内容作为字节返回:

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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() 以字节流的形式返回目标文件的内容。在此示例中,以换行符 "\n" 作为分隔符,使用 .split() 将流拆分为行。因为 .split() 正在在一个字节对象上进行操作,所以需要在字符串前添加前导 b 作为参数

ZipFile.read() 还接受名为 pwd 的第二个位置参数。此参数允许您提供用以读取加密文件的密码。要尝试此功能,您可以使用随本教程材料下载的 sample_pwd.zip 文件:

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

在第一个例子中,提供密码 secret 来读取加密文件。pwd 参数接受字节类型的值。如果在未提供所需密码的情况下对加密文件使用 .read() ,则会得到 RuntimeError,如第二个示例所示。

Note: Python 的 zipfile 支持解密。但是它不支持创建加密 ZIP 文件。这就是需要使用外部文件归档工具来加密文件的原因。

一些流行的文件归档工具包括 Windows 上的 7zWinRAR,Linux 上的 ArkGNOME Archive Manager,macOC 上的 Archiver

对于大型加密 ZIP 文件,请留意解密操作可能会非常慢,因为它是在纯 Python 中实现的。在这种情况下,请考虑使用专门的程序来处理您的归档而不是使用 zipfile

如果经常使用加密文件,那么可能希望避免每次调用 .read() 或其他接受 pwd 参数的方法时提供密码。如果是这种情况,可以使用 ZipFile.setpassword() 设置一个全局密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> 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(),只需要提供一次密码。ZipFile 使用该唯一密码来解密所有成员文件。

相反,如果您的 ZIP 文件的各个成员文件具有不同的密码,那么需要使用 .read()pwd 参数为每个文件提供特定的密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> 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 的。最后一个需要考虑的细节是,当使用 pwd 参数时,会覆盖可能已经通过 .setpassword() 设置的任何归档级密码。

Note: 在使用不支持的压缩算法的 ZIP 文件上调用 .read() 会引发 NotImplementedError。如果所需的压缩模块在您的 Python 安装中不可用,也会收到错误信息。

如果您正在寻找一种更灵活的方式来读取成员文件并创建和添加新的成员文件到归档中,那么 ZipFile.open() 适合您。与内置的 open() 函数一样,该方法实现了上下文管理器协议,因此它支持 with 语句:

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

在本例中,打开 hello.txt 以读取。open() 的第一个参数是 name ,表示要打开的成员文件。第二个参数是模式,像往常一样默认为 "r"ZipFile.open() 还接受一个用于打开加密文件的 pwd 参数。此参数与 .read() 中的同名参数作用相同。

您还可以将 .open()"w" 模式组合使用。此模式允许创建一个新的成员文件,向其中写入内容,最后将该文件附加到底层归档,应以追加模式打开:

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

在第一个代码片段中,以追加模式("a")打开 sample.zip 。然后通过以 "w" 模式调用 .open() 创建 new_hello.txt。此函数返回一个支持 .write() 的类文件对象,它允许您将字节写入新创建的文件。

Note: 需要为 .open() 提供一个非现有的文件名。如果使用底层归档中已经存在的文件名,那么会最终得到一个重复的文件和一个 UserWarning 异常。

在此示例中,将 b'Hello, World!' 写入 new_hello.txt。当执行流退出内部 with 语句时,Python 将输入字节写入成员文件。当外部 with 语句退出时,Python 会将 new_hello.txt 写入底层 ZIP 文件 sample.zip

第二个代码片段证实了 new_hello.txt 现在是 sample.zip 的成员文件。在这个示例的输出中需要注意的一个细节是,.write() 将新添加的文件的 Modified 日期设置为 1980-01-01 00:00:00。这是一个奇怪的行为,使用此方法时应牢记。

将成员文件的内容作为文本读取

正如您在上一节中所了解的,可以使用 .read().write() 方法来实现不从 ZIP 归档中提取而直接读写成员文件。这两种方法都仅适用于字节。

然而,当您有一个包含文本文件的 ZIP 归档时,可能希望将其内容作为文本而不是字节来读取。至少有两种方法可以做到这一点。可以使用:

  1. bytes.decode()
  2. io.TextIOWrapper

因为 ZipFile.read() 以字节形式返回目标成员文件的内容,故 .decode() 可以直接对这些字节进行操作。.decode() 方法使用给定的字符编码格式bytes 对象解码为字符串。

以下为如何使用 .decode() 以从 sample.zip 归档中的 hello.txt 文件中读取文本:

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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 参数,使用 "utf-8" 字符串。然而,可以使用任何其他有效编码,例如 UTF-16cp1252,用不区分大小写的字符串表示它们。注意 "utf-8".decode()encoding 参数的默认值。

时刻注意,您需要事先知道要使用 .decode() 处理的任何成员文件的字符编码格式。如果使用了错误的字符编码,那么代码将无法将底层字节正确解码为文本,最终可能会得到大量无法辨认的字符。

译者注:对于简体中文读者,最常见的一个乱码错误是,中文文本文件使用 Windows 默认的 GB2312 编码保存,而此处被以默认的 UTF-8 解码。因此见到许多“烫烫烫的锟斤拷”时不必惊慌,指定使用 GB2312 再尝试读取一次,很可能便解决了问题。

从成员文件中读取文本的第二个选项是使用 io.TextIOWrapper 对象,它提供具有缓冲的文字流。这次需要使用 .open() 而不是 .read()。以下是一个使用 io.TextIOWrapperhello.txt 成员文件作为文字流读取的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> 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 语句中,从 sample.zip 归档中打开成员文件 hello.txt。然后将生成的二进制类文件对象 hello 作为参数传递给 io.TextIOWrapper。这通过使用 UTF-8 字符编码格式解码 hello 的内容来创建有缓冲的文字流。因此,可以直接从目标成员文件中获得文字流。

就像 .encode() 一样,io.TextIOWrapper 类接受一个 encoding 参数。您应该始终为此参数指定一个值,因为默认文本编码取决于运行代码的操作系统,并且可能并非尝试解码的文件的正确值。

从您的 ZIP 归档中提取成员文件

提取给定归档的内容是对 ZIP 文件执行的最常见操作之一。根据您的需要,可能希望一次提取一个文件,或一次提取所有文件。

ZipFile.extract() 可以完成第一个任务。此方法接受一个 member 成员文件的名称,并将其提取到由 path 指定的目录。目标 path 默认为当前目录:

1
2
3
4
5
6
>>> import zipfile

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

现在 new_hello.txt 将会出现在 output_dir/ 目录中。如果目标文件名已存在于输出目录中,则 .extract() 将不经请求确认直接覆盖它。如果输出目录不存在,则 .extract() 会为您创建它。注意 .extract() 返回提取文件的路径。

成员文件的名称必须是 .namelist() 返回的文件全名。它也可以是一个包含文件信息的 ZipInfo 对象。

您还可以将 .extract() 用于加密文件。在这种情况下,需要提供所需的 pwd 参数或使用 .setpassword() 设置归档级密码。

当涉及到从归档中提取所有成员文件时,可以使用 .extractall()。顾名思义,此方法将所有成员文件提取到目标路径(默认为当前目录):

1
2
3
4
5
>>> import zipfile

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

运行此代码后,sample.zip 中的所有当前内容将出现在 out_dir/ 目录中。如果将一个不存在的目录传递给 .extractall(),那么该方法会自动创建这个目录。最后,如果目标目录中已经存在任何成员文件,那么 .extractall() 将不经请求确认直接覆盖掉它们,所以要小心。

如果只需要从给定归档中提取一些成员文件,则可以使用 members 参数。此参数接受成员文件列表,即手头归档中全部文件列表的子集。最后,就像 .extract() 一样,.extractall() 方法也接受 pwd 参数来提取加密文件。

使用后关闭 ZIP 文件

有时候,不使用 with 语句可以便捷地打开一个给定的 ZIP 文件。在这些情况下,需要在使用后手动关闭归档以完成所有写入操作并释放获取的资源。

为此,可以在 ZipFile 对象上调用 .close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> 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() 会关闭 archive 。在退出程序之前,必须调用 .close()。否则可能无法执行某些写入操作。例如,如果打开一个 ZIP 文件追加("a")新的成员文件,则需要关闭归档以写入文件。

创建、填充和提取您自己的 ZIP 文件

到目前为止,您已经学会了如何操纵现有的 ZIP 文件。您已经学会了使用 ZipFile 的不同模式来读取、写入和追加成员文件。您还学习了如何读取相关元数据以及如何提取给定 ZIP 文件的内容。

在本节中,您将编写一些练习示例,帮助您学习如何使用 zipfile 和其他 Python 工具从多个输入文件和整个目录创建 ZIP 文件。您还将学习如何使用 zipfile 进行文件压缩等。

从多个常规文件创建 ZIP 文件

有时您需要从多个相关的文件创建 ZIP 归档。这样将所有的文件放在一个容器中,以便通过计算机网络分发或与朋友同事分享。为此可以创建目标文件列表并使用 ZipFile 和循环将它们写入归档:

1
2
3
4
5
6
7
8
>>> 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)
...

此处,创建了一个 ZipFile 对象,并将所需的归档名称作为其第一个参数。"w" 模式允许您将成员文件写入最终的 ZIP 文件。

for() 循环遍历您输入的文件列表,并使用 .write() 将它们写入底层 ZIP 文件。一旦程序流退出 with 语句,ZipFile 会自动关闭归档,保存更改。现在获得了一个包含原始文件列表中所有文件的 multiple_files.zip 归档。

从目录创建 ZIP 文件

将一个目录中的内容打包到单个归档中是 ZIP 文件的另一个日常用法。Python 有几个工具可以与 zipfile 一起使用来完成这项任务。例如,可以使用 pathlib 读取给定目录的内容。有了这些信息,可以使用 ZipFile 创建一个容器归档。

python-zipfile/ 目录中,有一个名为 source_dir/ 的子目录,内容如下:

1
2
3
4
5
source_dir/

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

source_dir/ 中,只有三个普通文件。因为目录不包含子目录,所以可以使用 pathlib.Path.iterdir() 直接迭代其内容。按照这个思路,以下是从 source_dir/ 的内容构建 ZIP 文件的方法:

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

在此示例中,从源目录创建一个 pathlib.Path 对象。第一个 with 语句创建了一个可以写入的 ZipFile 对象。然后对 .iterdir() 的调用会返回一个遍历底层目录中条目的迭代器。

因为在 source_dir/ 中没有任何子目录,.iterdie() 函数只生成(yield)文件。for 循环遍历文件并将它们写入归档。

在此示例中,将 file_path.name 传递给 .write() 的第二个参数。此参数名为 arcname,保存着生成的归档中成员文件的名称。到目前为止,以上所有示例都依赖于 arcname 的默认值,即与作为第一个参数传递给 .write() 的文件名相同。

如果不将 file_path.name 传递给 arcname,那么源目录将作为 ZIP 文件的根目录。根据您的需要,这也可能是有效的结果。

现在查看工作目录中的 root_dir/ 文件夹。在此例中,您会发现以下结构:

1
2
3
4
5
6
7
8
root_dir/

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

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

此处有普通文件和一个包含单个文件的子目录。如果想创建一个具有相同内部结构的 ZIP 文件,那么需要一个工具来递归地遍历 root_dir/ 下的目录树

以下是如何使用 zipfilepathlib 模块中的Path.rglob() 来 zip 一个像上面那个一样的完整的目录树的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> 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 归档。

这一次,使用 Path.relative_to() 来获取每个文件的相对路径,然后将其结果传递给 .write() 的第二个参数。这样生成的 ZIP 文件最终具有与源目录相同的内部结构。再一次,如果您希望源目录作为 ZIP 文件的根目录,则可以去掉这个参数。

压缩文件和目录

如果您的文件占用了太多磁盘空间,那么可以考虑压缩它们。Python 的 zipfile 支持一些流行的压缩方法。但该模块默认情况下不会压缩文件。如果想让文件更小,那么需要显式地为 ZipFile 提供一种压缩方法。

通常,术语 stored 指代写入未压缩的 ZIP 文件的成员文件。这就是为什么 ZipFile 的默认压缩方法称为 ZIP_STORED,它实际上是指简单存储在归档中未压缩的成员文件。

compression 方法是 ZipFile 初始化方法的第三个参数。如果想在文件写入 ZIP 归档时压缩文件,则可以将此参数设置为以下常量之一:

常量 压缩方法 所需模块
zipfile.ZIP_DEFLATED Deflate zlib
zipfile.ZIP_BZIP2 Bzip2 bz2
zipfile.ZIP_LZMA LZMA lzma

这些是您目前可以与 ZipFile 一起使用的压缩方法。除此之外的方法会引发 NotImplementedError。从 Python 3.10 开始,zipfile 没有其他额外可用的压缩方法。

作为一项附加的 requirement,如果选择其中一种方法,则为其提供支持的压缩模块必须在您的 Python 安装中可用。否则,会得到一个 RuntimeError 异常,然后代码会崩溃。

在压缩文件时,另一个与 ZipFile 相关的参数是 compresslevel。此参数控制使用的压缩级别。

使用 Deflate 方法,compresslevel 可以取从 09 的整数。使用 Bzip2 方法,可以传递从 19 的整数。在这两种情况下,当压缩级别增加时,压缩率会更高,压缩速度会更慢。

Note: PNG、JPG、MP3 之类的二进制文件,已经使用了某种压缩方式。因此,将它们添加到 ZIP 文件可能并不会使数据变得更小,因为它已经被压缩到一定程度。

现在假设您要使用 Deflate 方法来归档和压缩给定目录的内容(这是 ZIP 文件中最常用的方法)。为此可以运行以下代码:

1
2
3
4
5
6
7
8
9
>>> 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))
...

在此示例中,将 9 传递给 compresslevel 以获得最大压缩。提供此参数时要使用关键字参数。这是因为 compresslevel 并非 ZipFile 初始化时的第四个位置参数

Note: ZipFile 的初始化时,接受名为 allowZip64 的第四个参数。这是一个布尔参数,告诉 ZipFile 为大于 4 GB 的文件使用 .zip64 扩展。

运行此代码后,在当前目录中会出现一个 comp_dir.zip 文件。如果将该文件的大小与原始 sample.zip 文件的进行比较,会发现文件大小显著减小。

依次创建 ZIP 文件

依次创建 ZIP 文件可能是日常编程中的另一个常见需求。例如,可能需要创建一个包含或不包含内容的初始 ZIP 文件,然后在新成员文件可用时立即追加它们。在这种情况下,需要多次打开和关闭目标 ZIP 文件。

为解决这个问题,可以在追加模式("a")下使用 ZipFile,像先前那样。此模式允许您安全地将新成员文件追加到 ZIP 归档而不截断其当前内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> 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():
... """Simulate a stream of files."""
... 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)的函数。要执行此操作,该函数会在每次被调用时打开和关闭目标归档。使用函数来执行此任务可以根据需要多次复用代码。

get_file_from_stream() 函数是一个生成器函数,用于模拟要处理的文件流。同时,for 循环使用 append_number() 将成员文件依次添加到 incremental.zip 中。如果在运行此代码后检查工作目录,那么您会发现一个 incremental.zip 归档,其中包含传递到循环中的三个文件。

提取文件和目录

对 ZIP 文件执行的最常见操作之一是将其内容提取到文件系统中的指定目录中。您已经学习了使用 .extract().extractall() 从归档中提取一个或所有文件的基础知识。

再举一个例子,回到 sample.zip 文件。此时归档包含四个不同类型的文件。有两个 .txt 文件和两个 .md 文件。假设只想提取 .md 文件。为此,可以运行以下代码:

1
2
3
4
5
6
7
8
9
>>> 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 以供读取。循环使用 namelist() 遍历归档中的每个文件,而条件语句检查文件名是否以 .md 扩展名结尾。如果是,则使用 .extract() 将手头的文件解压到目标目录 output_dir/

探索 zipfile 的其他类

到目前为止,您已经了解了 zipfile 中可用的两个类: ZipFileZipInfo。该模块还提供了另外两个在某些情况下能派上用场的类。它们是 zipfile.Pathzipfile.PyZipFile。在以下两节中,您将了解这些类的基础知识及其主要功能。

在 ZIP 文件中查找 PATH

当使用您最爱的归档应用程序打开 ZIP 文件时,会看到归档的内部结构。可能在归档的根目录中有文件。也可能有包含更多文件的子目录。归档看起来很像文件系统上的普通目录,每个文件都位于特定路径(PATH)。

zipfile.Path 类可以构建路径对象以快速创建和管理给定 ZIP 文件中的成员文件和目录的路径。该类接受两个参数:

  • root 接受一个 ZIP 文件,可以是 ZipFile 对象,也可以是字符串格式的指向物理 ZIP 文件的路径。
  • at 保存归档中特定成员文件或目录的位置。它默认为空字符串,表示归档的根路径。

以老朋友 sample.zip 作为目标,运行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> 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 对象相同的几个功能。可以使用 .name 获取文件的名称。可以使用 .is_file() 来检查路径是否指向一个普通文件。可以检查给定文件是否存在于特定 ZIP 文件中,等等。

Path 还提供了 .open() 方法来使用不同的模式打开成员文件。例如,下面的代码打开 hello.txt 以供读取:

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

使用 Path,可以快速创建指向给定 ZIP 文件中特定成员文件的路径对象,并使用 .open() 立即访问其内容。

就像使用 pathlib.Path 对象一样,可以通过在 zipfile.Path 对象上调用 .iterdir() 来列出 ZIP 文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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 归档中的成员文件。

使用 PyZipFile 构建可导入的 ZIP 文件

zipfile 中另一个实用的类是 PyZipFile。这个类与 ZipFile 非常相似,当需要将 Python 模块和包捆绑到 ZIP 文件中时,它特别方便。与 ZipFile 的主要区别在于,PyZipFile 初始化时使用一个名为 optimize 的可选参数,它允许您通过在归档前将 Python 代码编译为字节码来优化 Python 代码。

PyZipFile 提供与 ZipFile 相同的接口,但增加了 .writepy()。此方法可以接受一个 Python 文件(.py)作为参数,并将其添加到底层 ZIP 文件中。如果 optimize-1(默认值),则 .py 文件会自动编译为 .pyc 文件,然后添加到目标归档中。为什么会这样?

从 2.3 开始,Python 解释器支持从 ZIP 文件导入 Python 代码,这种功能称为 Zip imports。它可以创建可导入的 ZIP 文件以将模块和包作为单个归档分发。

Note: 也可以使用 ZIP 文件格式来创建和分发 Python 可执行应用程序(通常称为 Python Zip 应用程序)。要了解如何创建它们,请查看 Python’s zipapp: Build Executable Zip Applications

当需要生成可导入的 ZIP 文件时,PyZipFile 很有用。打包 .pyc 文件而不是 .py 文件使得导入过程效率大幅提高,因为它跳过了编译步骤。

python-zipfile/ 目录中,有一个 hello.py 模块,其内容如下:

1
2
3
4
5
"""Print a greeting message."""
# hello.py

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

这段代码定义了一个名为 greet() 的函数,它接受 name 作为参数并将问候消息打印到屏幕上。现在假设您想将此模块打包成一个 ZIP 以进行分发。为此,可以运行以下代码:

1
2
3
4
5
6
7
8
9
10
11
>>> 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.py 捆绑到一个 ZIP 文件后,可以使用 Python 的 import 系统从其包含的归档中导入此模块:

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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 and use the code
>>> import hello

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

从 ZIP 文件导入代码的第一步是使该文件在 sys.path 中可用。此变量包含一个字符串列表,该列表为模块指定 Python 的搜索路径。要向 sys.path 添加新项,可以使用 .insert()

为了让这个示例正常工作,需要更改占位符路径并将路径指向您的文件系统上的 hello.zip。一旦可导入 ZIP 文件在此列表中,就可以像使用常规模块一样导入代码。

最后,考虑一下工作目录中的 hello/ 子目录。它包含一个具有以下结构的小型 Python 包:

1
2
3
4
hello/
|
├── __init__.py
└── hello.py

__init__.py 模块将 hello/ 目录转换为 Python 包。hello.py 模块与在上一个示例中使用的模块相同。现在假设您想将此包捆绑到一个 ZIP 文件中。这种情况下,可以执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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 中。同样,可以按照之前学习的步骤从该归档中导入代码:

1
2
3
4
5
6
7
8
>>> 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 文件的内容:

1
2
3
4
5
6
$ 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

此命令展示与对 sample.zip 归档调用 printdir() 相同的输出。

现在假设您要创建一个包含多个输入文件的新 ZIP 文件。在这种情况下,可以使用 -c 或者 --creat 选项:

1
2
3
4
5
6
7
$ 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.txtlorem.mdrealpython.md 文件。

如果需要创建一个 ZIP 文件来归档整个目录怎么办?例如,您可能拥有自己的 source_dir/,其中包含与上例相同的三个文件。 可以使用以下命令从该目录创建 ZIP 文件:

1
2
3
4
5
6
7
8
$ 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

使用此命令,zipfilesource_dir/ 作为生成的 source_dir.zip 文件根目录。像往常一样,可以通过使用 -l 选项运行 zipfile 来列出归档内容。

Note: 当使用 zipfile 从命令行创建归档时,库在归档文件时隐式使用 Deflate 压缩算法。

还可以使用命令行中的 -e--extract 选项提取给定 ZIP 文件的所有内容:

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

运行此命令后,工作目录中将有一个新的 sample/ 文件夹。 新文件夹将包含 sample.zip 归档中的当前文件。

可以在命令行中与 zipfile 一起使用的最后一个参数时 -t--test。该选项测试给定文件是否为有效的 ZIP 文件。来吧,试一试!

使用其他库处理 ZIP 文件

Python 标准库中还有一些其他工具可用于在更底层进行归档、压缩和解压缩文件。Python 的 zipfile 在内部使用了其中的几个,主要用于压缩目的。 以下是部分工具的摘要:

模块 描述
zlib 允许使用 zlib 库进行压缩和解压缩
bz2 提供使用 Bzip2 压缩算法来压缩和解压数据的接口
lzma 提供使用 LZMA 压缩算法压缩和解压缩数据的类和函数

不同于 zipfile,其中一些模块支持从内存和数据流中压缩和解压数据,而不是普通文件和归档。

在 Python 标准库中,可以找到支持 TAR 归档格式的 tarfile。还有一个名为 gzip 的模块,它提供了一个压缩和解压数据的接口,类似于 GNU Gzip 程序的做法。

例如,可以使用 gzip 创建一个包含一些文本的压缩文件:

1
2
3
4
5
6
>>> 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.txt 中有文本 Hello, World!

在不使用 zipfile 的情况下创建 ZIP 文件的一种快速且高级的方法是使用 shutil。该模块可以对文件和文件集合执行多项高级操作。当涉及到归档操作时,有 make_archive(),它可以创建归档,例如 ZIP 或 TAR 文件:

1
2
3
4
>>> import shutil

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

此代码在工作目录中创建一个名为 sample.zip 的压缩文件。此 ZIP 文件将包含输入目录 source_dir/ 中的所有文件。当您需要一种快速且高级的方式在 Python 中创建 ZIP 文件时,make_archive() 函数非常方便。

结论

当需要从 ZIP 归档中读取、写入、压缩、解压和提取文件时,Python 的 zipfile 是一个方便的工具。ZIP 文件格式已经成为行业标准,可以打包和压缩数字数据。

使用 ZIP 文件的好处包括将所有相关文件归档到一起、节省磁盘空间、便于通过计算机网络传输数据、捆绑 Python 代码以进行分发等。

在教程中,您学到了如何:

  • 使用 Python 的 zipfile 来读取、写入、提取现有 ZIP 文件。
  • 使用 zipfile 读取关于 ZIP 文件内容的元数据
  • 使用 zipfile 操作现有 ZIP 文件中的成员文件
  • 创建您自己的 ZIP 文件以归档和压缩您的数字数据

您还学习了如何在命令行中使用 zipfile 来列出、创建和提取 ZIP 文件。有了这些知识,您就可以使用 ZIP 文件格式有效地归档、压缩和处理您的数字数据。

附A:材料下载

官方下载

文中提到的所有代码示例、ZIP 文件等,都可以到 Real Python 原文中下载。

下载链接:https://realpython.com/bonus/python-zipfile-materials/

出于尊重源站利益考虑,强烈建议读者使用此方式注册成为 Real Python 会员后下载

本站下载

考虑到国内网络情况,有部分读者可能访问源站、下载文件网速缓慢,故提供一个备用下载。

下载链接:https://oss.muzing.top/something/RealPython-materials-zipfile.zip

注意:不负责维护此下载。转载行为仅为便于部分确实存在访问源站困难的读者下载代码材料之用


Python zipfile:高效处理 ZIP 文件(翻译)
https://muzing.top/posts/4797762a/
作者
Muzing
发布于
2022年3月13日
更新于
2022年3月13日
许可协议