Python自动化实战指南(让繁琐操作自动搞定)第二辑:第九章详解文件的读取与写入
原文:automatetheboringstuff.com/2e/chapter9…
变量是在程序运行时存储数据的好方法,但是如果您希望数据在程序完成后仍然存在,您需要将其保存到一个文件中。你可以把一个文件的内容想象成一个单独的字符串值,大小可能是千兆字节。在本章中,您将学习如何使用 Python 来创建、读取和保存硬盘上的文件。
文件和文件路径
一个文件有两个关键属性:文件名(通常写成一个单词)和路径。路径指定文件在计算机上的位置。例如,我的 Windows 笔记本电脑上有一个文件名为project.docx
的文件,路径为C:\Users\Al\Documents
。最后一个句点之后的文件名部分称为文件的扩展名,它告诉您文件的类型。文件名project.docx
为 Word 文档,Users
、Al
、Documents
均是文件夹(也称目录)。文件夹可以包含文件和其他文件夹。例如,project.docx
在Documents
文件夹中,该文件夹在Al
文件夹中,该文件夹在Users
文件夹中。图 9-1 显示了该文件夹的组织结构。
图 9-1:文件夹层次结构中的一个文件
路径的C:\
部分是根文件夹,它包含所有其他文件夹。在 Windows 上,根文件夹被命名为C:\
,也称为C:
驱动器。在 MacOS 和 Linux 上,根文件夹是/
。在本书中,我将使用 Windows 风格的根文件夹,C:\
。如果您在 MacOS 或 Linux 上输入交互式 Shell 示例,请输入/
。
其他卷,如 DVD 驱动器或 USB 闪存驱动器,在不同的操作系统上会有不同的显示。在 Windows 上,它们显示为新的带字母的根驱动器,如D:\
或E:\
。在 MacOS 上,它们作为新文件夹出现在/Volumes
文件夹下。在 Linux 上,它们作为新文件夹出现在/mnt
(“挂载”)文件夹下。还要注意,虽然在 Windows 和 MacOS 上文件夹名和文件名不区分大小写,但在 Linux 上是区分大小写的。
注
由于你的系统上的文件和文件夹可能与我的不同,你不可能完全按照本章中的每个例子去做。尽管如此,还是试着使用你电脑上的文件夹。
Windows 上的反斜杠,MacOS 和 Linux 上的正斜杠
在 Windows 上,路径使用反斜杠(\
)作为文件夹名称之间的分隔符。然而,MacOS 和 Linux 操作系统使用正斜杠( /
)作为路径分隔符。如果您想让您的程序在所有操作系统上工作,您将不得不编写 Python 脚本来处理这两种情况。
幸运的是,使用pathlib
模块中的Path()
函数很容易做到这一点。如果你把路径中的单个文件和文件夹名的字符串值传递给它,Path()
将使用正确的路径分隔符返回一个带有文件路径的字符串。在交互式 Shell 中输入以下内容:
>>> from pathlib import Path
>>> Path('spam', 'bacon', 'eggs')
WindowsPath('spam/bacon/eggs')
>>> str(Path('spam', 'bacon', 'eggs'))
'spam\\bacon\\eggs'
注意,导入pathlib
的约定是运行from pathlib import Path
,因为否则我们必须在代码中出现Path
的地方输入pathlib.Path
。这种额外的输入不仅是多余的,而且也是多余的。
我正在 Windows 上运行这一章的交互式 Shell 示例,所以Path('spam', 'bacon', 'eggs')
为连接的路径返回了一个WindowsPath
对象,表示为WindowsPath('spam/bacon/eggs')
。尽管 Windows 使用反斜杠,但交互式 Shell 中的WindowsPath
表示使用正斜杠显示它们,因为开源软件开发人员历来偏爱 Linux 操作系统。
如果您想获得这个路径的一个简单的文本字符串,您可以将它传递给str()
函数,在我们的例子中它返回'spam\\bacon\\eggs'
。(请注意,反斜杠是双反斜杠,因为每个反斜杠需要由另一个反斜杠字符转义。)如果我在比如说 Linux 上调用这个函数,Path()
会返回一个PosixPath
对象,当传递给str()
时,会返回'spam/bacon/eggs'
。( POSIX
是 Linux 等类 Unix 操作系统的一套标准。)
这些Path
对象(实际上,WindowsPath
或PosixPath
对象,取决于你的操作系统)将被传递给本章介绍的几个与文件相关的函数。例如,以下代码将文件名列表中的名称连接到文件夹名称的末尾:
>>> from pathlib import Path
>>> myFiles = ['accounts.txt', 'details.csv', 'invite.docx']
>>> for filename in myFiles:
print(Path(r'C:\Users\Al', filename))
C:\Users\Al\accounts.txt
C:\Users\Al\details.csv
C:\Users\Al\invite.docx
在 Windows 上,反斜杠分隔目录,所以不能在文件名中使用它。但是,在 MacOS 和 Linux 上,可以在文件名中使用反斜杠。因此,虽然在 Windows 上Path(r'spam\eggs')
引用两个独立的文件夹(或文件夹spam
中的一个文件eggs
,但是在 MacOS 和 Linux 上,相同的命令会引用一个名为spam\eggs
的文件夹(或文件)。出于这个原因,在 Python 代码中总是使用正斜杠通常是个好主意(我将在本章的剩余部分这样做)。pathlib
模块将确保它总是在所有操作系统上工作。
请注意,pathlib
是在 Python 3.4 中引入的,用来替换旧的os.path
函数。Python 标准库模块从 Python 3.6 开始支持它,但是如果您正在使用遗留的 Python 2 版本,我推荐使用pathlib2
,它为您提供了 Python 2.7 上的pathlib
特性。附录 A 有使用 PIP 安装pathlib2
的说明。每当我用pathlib
替换旧的os.path
函数时,我都会做一个简短的笔记。你可以在docs.python.org/3/library/os.path.html查找老的函数。
使用/
运算符连接路径
我们通常使用+
操作符将两个整数或浮点数相加,比如在表达式2 + 2
中,其计算结果为整数值4
。但是我们也可以使用+
操作符来连接两个字符串值,比如表达式'Hello' + 'World'
,其计算结果为字符串值'HelloWorld'
。同样,我们通常用于除法的/
运算符也可以组合Path
对象和字符串。这有助于在已经用Path()
函数创建了一个Path
对象后对其进行修改。
例如,在交互式 Shell 中输入以下内容:
>>> from pathlib import Path
>>> Path('spam') / 'bacon' / 'eggs'
WindowsPath('spam/bacon/eggs')
>>> Path('spam') / Path('bacon/eggs')
WindowsPath('spam/bacon/eggs')
>>> Path('spam') / Path('bacon', 'eggs')
WindowsPath('spam/bacon/eggs')
对Path
对象使用/
操作符使得连接路径就像字符串连接一样简单。它也比使用字符串连接或join()
方法更安全,就像我们在这个例子中所做的:
>>> homeFolder = r'C:\Users\Al'
>>> subFolder = 'spam'
>>> homeFolder + '\\' + subFolder
'C:\\Users\\Al\\spam'
>>> '\\'.join([homeFolder, subFolder])
'C:\\Users\\Al\\spam'
使用这段代码的脚本是不安全的,因为它的反斜杠只适用于 Windows。您可以添加一个if
语句来检查sys.platform
(包含一个描述计算机操作系统的字符串)以决定使用哪种斜杠,但是在任何需要的地方应用这个定制代码可能会不一致并且容易出错。
无论您的代码运行在什么操作系统上,pathlib
模块通过重用/
数学除法运算符来正确连接路径,从而解决了这些问题。以下示例使用此策略来连接与上一示例相同的路径:
>>> homeFolder = Path('C:/Users/Al')
>>> subFolder = Path('spam')
>>> homeFolder / subFolder
WindowsPath('C:/Users/Al/spam')
>>> str(homeFolder / subFolder)
'C:\\Users\\Al\\spam'
当使用/
操作符连接路径时,你需要记住的唯一事情是前两个值中的一个必须是一个Path
对象。如果你尝试在交互式 Shell 中输入以下内容,Python 会给出一个错误:
>>> 'spam' / 'bacon' / 'eggs'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'str' and 'str'
Python 从左到右计算/
操作符,并计算出一个Path
对象,因此最左边的第一个或第二个值必须是一个Path
对象,整个表达式才能计算出一个Path
对象。下面是/
操作符和一个Path
对象如何计算出最终的Path
对象。
如果您看到前面显示的TypeError: unsupported operand type(s) for /: 'str' and 'str'
错误消息,您需要在表达式的左侧放置一个Path
对象。
/
运算符取代了旧的os.path.join()
函数,你可以从docs.python.org/3/library/os.path.html#os.path.join中了解更多。
当前工作目录
你电脑上运行的每个程序都有当前工作目录,或cwd
。任何不以根文件夹开头的文件名或路径都被认为位于当前工作目录下。
注
虽然文件夹是更现代的目录名称,但是请注意当前工作目录(或者仅仅是工作目录)是标准术语,而不是“当前工作文件夹”
您可以使用Path.cwd()
函数以字符串值的形式获取当前工作目录,并使用os.chdir()
对其进行更改。在交互式 Shell 中输入以下内容:
>>> from pathlib import Path
>>> import os
>>> Path.cwd()
WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37')'
>>> os.chdir('C:\\Windows\\System32')
>>> Path.cwd()
WindowsPath('C:/Windows/System32')
这里当前工作目录设置为C:\Users\Al\AppData\Local\Programs\Python\Python37
,所以文件名project.docx
指的是C:\Users\Al\AppData\Local\Programs\Python\Python37\project.docx
。当我们将当前工作目录改为C:\Windows\System32
时,文件名project.docx
解释为C:\Windows\System32\project.docx
。
如果您尝试更改到一个不存在的目录,Python 将显示一个错误。
>>> os.chdir('C:/ThisFolderDoesNotExist')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [WinError 2] The system cannot find the file specified:
'C:/ThisFolderDoesNotExist'
没有改变工作目录的pathlib
函数,因为在程序运行时改变当前工作目录往往会导致细微的 bug。
os.getcwd()
函数是以字符串形式获取当前工作目录的老方法。
主目录
所有用户在电脑上都有一个存放自己文件的文件夹,叫做主目录或主文件夹。您可以通过调用Path.home()
来获得home
文件夹的一个Path
对象:
>>> Path.home()
WindowsPath('C:/Users/Al')
主目录位于一个固定的位置,具体取决于您的操作系统:
- 在 Windows 上,主目录位于
C:\Users
下。 - 在 Mac 上,主目录位于
/Users
下。 - 在 Linux 上,主目录通常位于
/home
下。
您的脚本几乎肯定有权限读写您的主目录下的文件,因此这是存放 Python 程序将使用的文件的理想位置。
绝对与相对路径
有两种方法可以指定文件路径:
- 绝对路径,它总是从根文件夹开始
- 相对路径,相对于程序当前的工作目录
还有圆点 ( .
)和圆点 ( ..
)文件夹。这些不是真正的文件夹,而是可以在路径中使用的特殊名称。文件夹名称的单个句点(“点”)是“此目录”的简写。两个句点(“点-点”)表示“父文件夹”
图 9-2 是一些文件夹和文件的例子。当当前工作目录设置为C:\bacon
时,其他文件夹和文件的相对路径如图所示。
图 9-2:C:\bacon
工作目录中文件夹和文件的相对路径
.\
相对路径的开头是可选的。例如,.\spam.txt
和spam.txt
引用同一个文件。
使用os.makedirs()
函数创建新文件夹
您的程序可以使用os.makedirs()
函数创建新的文件夹(目录)。在交互式 Shell 中输入以下内容:
>>> import os
>>> os.makedirs('C:\\delicious\\walnut\\waffles')
这不仅会创建C:\delicious
文件夹,还会创建C:\delicious
中的walnut
文件夹和C:\delicious\walnut
中的waffles
文件夹。也就是说,os.makedirs()
将创建任何必要的中间文件夹,以确保完整路径存在。图 9-3 显示了文件夹的层次结构。
图 9-3:os.makedirs('C:\\delicious\\walnut\\waffles')
的结果
从一个Path
对象创建一个目录,调用mkdir()
方法。例如,这段代码将在我电脑的主文件夹下创建spam
文件夹:
>>> from pathlib import Path
>>> Path(r'C:\Users\Al\spam').mkdir()
注意mkdir()
一次只能做一个目录;不会像os.makedirs()
一样一次做好几个子目录。
处理绝对和相对路径
pathlib
模块提供了检查给定路径是否是绝对路径并返回相对路径的绝对路径的方法。
在Path
对象上调用is_absolute()
方法将返回True
(如果它代表绝对路径)或False
(如果它代表相对路径)。例如,在交互式 Shell 中输入以下内容,使用您自己的文件和文件夹,而不是这里列出的文件和文件夹:
>>> Path.cwd()
WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37')
>>> Path.cwd().is_absolute()
True
>>> Path('spam/bacon/eggs').is_absolute()
False
要从相对路径获得绝对路径,可以将Path.cwd() /
放在相对Path
对象的前面。毕竟,当我们说“相对路径”时,我们几乎总是指相对于当前工作目录的路径。在交互式 Shell 中输入以下内容:
>>> Path('my/relative/path')
WindowsPath('my/relative/path')
>>> Path.cwd() / Path('my/relative/path')
WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37/my/relative/
path')
如果您的相对路径是相对于当前工作目录之外的另一个路径,那么只需用那个路径替换Path.cwd()
即可。以下示例使用主目录而不是当前工作目录获取绝对路径:
>>> Path('my/relative/path')
WindowsPath('my/relative/path')
>>> Path.home() / Path('my/relative/path')
WindowsPath('C:/Users/Al/my/relative/path')
os.path
模块也有一些与绝对和相对路径相关的有用函数:
- 调用
os.path.abspath(path)
将返回参数字符串的绝对路径。这是一种将相对路径转换成绝对路径的简单方法。 - 如果参数是绝对路径,调用
os.path.isabs(path)
将返回True
,如果是相对路径,将返回False
。 - 调用
os.path.relpath(path, start)
会返回一串从开始路径到路径的相对路径。如果没有提供start
,则使用当前工作目录作为开始路径。
在交互式 Shell 中尝试这些函数:
>>> os.path.abspath('.')
'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37'
>>> os.path.abspath('.\\Scripts')
'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37\\Scripts'
>>> os.path.isabs('.')
False
>>> os.path.isabs(os.path.abspath('.'))
True
由于C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37
是调用os.path.abspath()
时的工作目录,“单点”文件夹代表绝对路径'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37'
。
在交互式 Shell 中输入以下对os.path.relpath()
的调用:
>>> os.path.relpath('C:\\Windows', 'C:\\')
'Windows'
>>> os.path.relpath('C:\\Windows', 'C:\\spam\\eggs')
'..\\..\\Windows'
当相对路径与路径在同一父文件夹中,但在不同路径的子文件夹中,如'C:\\Windows'
和'C:\\spam\\eggs'
,您可以使用“点-点”符号返回到父文件夹。
获取文件路径的各个部分
给定一个Path
对象,您可以使用几个Path
对象属性将文件路径的不同部分提取为字符串。这对于基于现有文件路径构建新的文件路径非常有用。属性如图图 9-4 所示。
图 9-4:Windows(上)和 MacOS/Linux(下)文件路径的组成部分
文件路径的组成部分包括:
-
anchor
,它是文件系统的根文件夹 - 在 Windows 上,
drive
,这是一个字母,通常表示物理硬盘或其他存储设备 -
parent
,即包含该文件的文件夹 - 文件的
name
,由stem
(或basename
)和suffix
(或extend
组成
注意 Windows Path
对象有一个drive
属性,但是 MacOS 和 Linux Path
对象没有。drive
属性不包括第一个反斜杠。
要从文件路径中提取每个属性,请在交互式 Shell 中输入以下内容:
>>> p = Path('C:/Users/Al/spam.txt')
>>> p.anchor
'C:\\'
>>> p.parent # This is a Path object, not a string.
WindowsPath('C:/Users/Al')
>>> p.name
'spam.txt'
>>> p.stem
'spam'
>>> p.suffix
'.txt'
>>> p.drive
'C:'
这些属性求值为简单的字符串值,除了parent
,它求值为另一个Path
对象。
parents
属性(不同于parent
属性)求值具有整数索引的Path
对象的祖先文件夹:
>>> Path.cwd()
WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37')
>>> Path.cwd().parents[0]
WindowsPath('C:/Users/Al/AppData/Local/Programs/Python')
>>> Path.cwd().parents[1]
WindowsPath('C:/Users/Al/AppData/Local/Programs')
>>> Path.cwd().parents[2]
WindowsPath('C:/Users/Al/AppData/Local')
>>> Path.cwd().parents[3]
WindowsPath('C:/Users/Al/AppData')
>>> Path.cwd().parents[4]
WindowsPath('C:/Users/Al')
>>> Path.cwd().parents[5]
WindowsPath('C:/Users')
>>> Path.cwd().parents[6]
WindowsPath('C:/')
较老的os.path
模块也有类似的函数,用于将路径的不同部分写入字符串值。调用os.path.dirname(path)
将返回一串在path
参数中最后一个斜杠之前的所有内容。调用os.path.basename(path)
将返回一个字符串,该字符串包含path
参数中最后一个斜杠之后的所有内容。路径的目录(或dir
)名和基本名在图 9-5 中列出。
图 9-5:基本名称跟在路径的最后一个斜杠后面,与文件名相同。目录名是最后一个斜杠之前的所有内容。
例如,在交互式 Shell 中输入以下内容:
>>> calcFilePath = 'C:\\Windows\\System32\\calc.exe'
>>> os.path.basename(calcFilePath)
'calc.exe'
>>> os.path.dirname(calcFilePath)
'C:\\Windows\\System32'
如果你需要一个路径的目录名和基本名,你可以调用os.path.split()
来获得这两个字符串的元组值,就像这样:
>>> calcFilePath = 'C:\\Windows\\System32\\calc.exe'
>>> os.path.split(calcFilePath)
('C:\\Windows\\System32', 'calc.exe')
注意,您可以通过调用os.path.dirname()
和os.path.basename()
并将它们的返回值放在一个元组中来创建相同的元组:
>>> (os.path.dirname(calcFilePath), os.path.basename(calcFilePath))
('C:\\Windows\\System32', 'calc.exe')
但是如果你需要这两个值的话,os.path.split()
是一个很好的捷径。
另外,注意os.path.split()
没有而不是获取文件路径并返回每个文件夹的字符串列表。为此,使用split()
字符串方法并在os.sep
中拆分字符串。(注意sep
是在os
,不是os.path
。)变量os.sep
被设置为运行该程序的计算机的正确的文件夹分隔斜杠,在 Windows 上为'\\'
,在 MacOS 和 Linux 上为'/'
,对其进行分割将返回各个文件夹的列表。
例如,在交互式 Shell 中输入以下内容:
>>> calcFilePath.split(os.sep)
['C:', 'Windows', 'System32', 'calc.exe']
这会将路径的所有部分作为字符串返回。
在 MacOS 和 Linux 系统上,返回的文件夹列表将以空白字符串开头,如下所示:
>>> '/usr/bin'.split(os. sep)
['', 'usr', 'bin']
字符串方法将返回路径每个部分的列表。
查找文件大小和文件夹内容
一旦有了处理文件路径的方法,就可以开始收集关于特定文件和文件夹的信息。os.path
模块提供了查找文件大小(以字节为单位)以及给定文件夹中的文件和文件夹的函数。
- 调用
os.path.getsize(path)
将返回路径参数中文件的字节大小。 - 调用
os.listdir(path)
将在路径参数中返回每个文件的文件名字符串列表。(注意该函数在os
模块中,而不是os.path
。)
下面是我在交互式 Shell 中尝试这些函数时得到的结果:
>>> os.path.getsize('C:\\Windows\\System32\\calc.exe')
27648
>>> os.listdir('C:\\Windows\\System32')
['0409', '12520437.cpx', '12520850.cpx', '5U877.ax', 'aaclient.dll',
--snip--
'xwtpdui.dll', 'xwtpw32.dll', 'zh-CN', 'zh-HK', 'zh-TW', 'zipfldr.dll']
如你所见,我电脑上的calc.exe
程序大小为 27648 字节,我有很多文件在C:\Windows\system32
中。如果我想找到这个目录中所有文件的总大小,我可以一起使用os.path.getsize()
和os.listdir()
。
>>> totalSize = 0
>>> for filename in os.listdir('C:\\Windows\\System32'):
totalSize = totalSize + os.path.getsize(os.path.join('C:\\Windows\\System32', filename))
>>> print(totalSize)
2559970473
当我遍历C:\Windows\System32
文件夹中的每个文件名时,totalSize
变量会根据每个文件的大小递增。注意当我调用os.path.getsize()
时,我是如何使用os.path.join()
将文件夹名和当前文件名连接起来的。os.path.getsize()
返回的整数加到totalSize
的值上。在遍历完所有文件后,我打印totalSize
来查看C:\Windows\System32
文件夹的总大小。
使用 GLOB 模式修改文件列表
如果您想处理特定的文件,glob()
方法比listdir()
更容易使用。Path
对象有一个根据 GLOB 模式列出文件夹内容的glob()
方法。GLOB 模式类似于命令行命令中常用的正则表达式的简化形式。glob()
方法返回一个生成器对象(这超出了本书的范围),您需要将它传递给list()
,以便在交互式 Shell 中轻松查看:
>>> p = Path('C:/Users/Al/Desktop')
>>> p.glob('*')
<generator object Path.glob at 0x000002A6E389DED0>
>>> list(p.glob('*')) # Make a list from the generator.
[WindowsPath('C:/Users/Al/Desktop/1.png'), WindowsPath('C:/Users/Al/
Desktop/22-ap.pdf'), WindowsPath('C:/Users/Al/Desktop/cat.jpg'),
--snip--
WindowsPath('C:/Users/Al/Desktop/zzz.txt')]
星号(*
)代表“任意字符的倍数”,因此p.glob('*')
返回存储在p
中的路径中的所有文件的生成器。
与正则表达式一样,您可以创建复杂的表达式:
>>> list(p.glob('*.txt') # Lists all text files.
[WindowsPath('C:/Users/Al/Desktop/foo.txt'),
--snip--
WindowsPath('C:/Users/Al/Desktop/zzz.txt')]
GLOB 模式'*.txt'
将返回以任意字符组合开头的文件,只要它以字符串'.txt'
结尾,这是文本文件扩展名。
与星号相反,问号(?
)代表任何单个字符:
>>> list(p.glob('project?.docx')
[WindowsPath('C:/Users/Al/Desktop/project1.docx'), WindowsPath('C:/Users/Al/
Desktop/project2.docx'),
--snip--
WindowsPath('C:/Users/Al/Desktop/project9.docx')]
glob 表达式'project?.docx'
将返回'project1.docx'
或'project5.docx'
,但不会返回'project10.docx'
,因为?
只匹配一个字符——所以不会匹配到双字符字符串'10'
。
最后,您还可以组合星号和问号来创建更复杂的 Glob 表达式,如下所示:
>>> list(p.glob('*.?x?')
[WindowsPath('C:/Users/Al/Desktop/calc.exe'), WindowsPath('C:/Users/Al/
Desktop/foo.txt'),
--snip--
WindowsPath('C:/Users/Al/Desktop/zzz.txt')]
glob 表达式'*.?x?'
将返回具有任意名称和任意三个字符扩展名的文件,其中中间字符是一个'x'
。
通过挑选出具有特定属性的文件,glob()
方法可以让您轻松地指定目录中想要执行某些操作的文件。您可以使用一个for
循环来迭代glob()
返回的生成器:
>>> p = Path('C:/Users/Al/Desktop')
>>> for textFilePathObj in p.glob('*.txt'):
... print(textFilePathObj) # Prints the Path object as a string.
... # Do something with the text file.
...
C:\Users\Al\Desktop\foo.txt
C:\Users\Al\Desktop\spam.txt
C:\Users\Al\Desktop\zzz.txt
如果你想对一个目录中的每个文件执行一些操作,你可以使用os.listdir(p)
或者p.glob('*')
。
检查路径有效性
如果您为许多 Python 函数提供一个不存在的路径,它们将会崩溃并出现错误。幸运的是,Path
对象拥有检查给定路径是否存在以及它是文件还是文件夹的方法。假设一个变量p
持有一个Path
对象,您可以预期如下:
- 如果路径存在,调用
p.exists()
将返回True
,如果路径不存在,将返回False
。 - 如果路径存在并且是文件,调用
p.is_file()
返回True
,否则返回False
。 - 如果路径存在并且是目录,调用
p.is_dir()
返回True
,否则返回False
。
在我的计算机上,以下是我在交互式 Shell 中尝试这些方法时得到的结果:
>>> winDir = Path('C:/Windows')
>>> notExistsDir = Path('C:/This/Folder/Does/Not/Exist')
>>> calcFile = Path('C:/Windows
/System32/calc.exe')
>>> winDir.exists()
True
>>> winDir.is_dir()
True
>>> notExistsDir.exists()
False
>>> calcFile.is_file()
True
>>> calcFile.is_dir()
False
您可以通过使用exists()
方法检查来确定当前是否有 DVD 或闪存驱动器连接到计算机。例如,如果我想在我的 Windows 计算机上检查一个卷名为D:\
的闪存驱动器,我可以用下面的代码来完成:
>>> dDrive = Path('D:/')
>>> dDrive.exists()
False
哎呀!好像我忘了插上我的优盘。
旧的os.path
模块可以用os.path.exists(path)
、os.path.isfile(path)
和os.path.isdir(path)
函数完成相同的任务,这些函数的作用就像它们的Path
函数的对应物一样。从 Python 3.6 开始,这些函数可以接受Path
对象以及文件路径的字符串。
文件读写过程
一旦你熟悉了文件夹和相对路径,你就可以指定文件的读写位置。接下来几节中涉及的函数将适用于纯文本文件。纯文本文件只包含基本的文本字符,不包含字体、大小或颜色信息。带着txt
扩展名文本文件,或带py
扩展名的 Python 脚本文件,是纯文本文件的例子。这些可以用 Windows 的记事本或 MacOS 的 TextEdit 应用打开。您的程序可以很容易地读取纯文本文件的内容,并将它们视为普通的字符串值。
二进制文件是所有其他文件类型,如文字处理文档、PDF、图像、电子表格和可执行程序。如果你在记事本或文本编辑中打开一个二进制的文件,它会看起来像杂乱无章,就像图 9-6 中的一样。
图 9-6:记事本中打开的 Windows calc.exe
程序
因为每一种不同类型的二进制文件都必须以自己的方式处理,所以本书不会直接讨论如何读写原始的二进制文件。幸运的是,许多模块使得处理二进制文件变得更加容易——在本章的后面,您将探索其中的一个模块—shelve
模块。模块的read_text()
方法返回一个文本文件的完整内容的字符串。它的write_text()
方法用传递给它的字符串创建一个新的文本文件(或者覆盖一个现有的文件)。在交互式 Shell 中输入以下内容:
>>> from pathlib import Path
>>> p = Path('spam.txt')
>>> p.write_text('Hello, world!')
13
>>> p.read_text()
'Hello, world!'
这些方法调用创建了一个内容为'Hello, world!'
的spam.txt
文件。write_text()
返回的13
表示有 13 个字符被写入文件。(您通常可以忽略这些信息。)调用read_text()
读取并以字符串形式返回新文件的内容:'Hello, world!'
。
请记住,这些Path
对象方法只提供与文件的基本交互。更常见的写入文件的方式是使用open()
函数和文件对象。在 Python 中读写文件有三个步骤:
- 调用
open()
函数返回一个File
对象。 - 在
File
对象上调用read()
或write()
方法。 - 通过调用
File
对象上的close()
方法来关闭文件。
我们将在接下来的章节中回顾这些步骤。
用open()
函数打开文件
要用open()
函数打开一个文件,你要给它传递一个字符串路径,指明你要打开的文件;它可以是绝对路径,也可以是相对路径。open()
函数返回一个File
对象。
尝试使用记事本或文本编辑创建一个名为hello.txt
的文本文件。打hello, world!
作为该文本文件的内容,并将其保存在您的用户个人文件夹中。然后在交互式 Shell 中输入以下内容:
>>> helloFile = open(Path.home() / 'hello.txt')
open()
函数也可以接受字符串。如果您使用的是 Windows,请在交互式 Shell 中输入以下内容:
>>> helloFile = open('C:\\Users\\your_home_folder\\hello.txt')
如果您使用的是 MacOS,请在交互式 Shell 中输入以下内容:
>>> helloFile = open('/Users/your_home_folder/hello.txt')
确保用你的电脑用户名替换你的个人文件夹。例如,我的用户名是Al
,所以我会在 Windows 上输入'C:\\Users\\Al\\hello.txt'
。注意,从 Python 3.6 开始,open()
函数只接受Path
对象。在以前的版本中,你总是需要传递一个字符串给open()
。
这两个命令都将以“阅读纯文本”模式打开文件,或简称为阅读模式。当以读取模式打开文件时,Python 只允许从文件中读取数据;你不能以任何方式写或修改它。读取模式是在 Python 中打开文件的默认模式。但是如果不想依赖 Python 的默认值,可以通过将字符串值'r'
作为第二个参数传递给open()
来显式指定模式。所以open('/Users/Al/hello.txt', 'r')
和open('/Users/Al/hello.txt')
做同样的事情。
对open()
的调用返回一个File
对象。一个File
对象代表你计算机上的一个文件;它只是 Python 中另一种类型的值,很像您已经熟悉的列表和字典。在前面的例子中,您将File
对象存储在变量helloFile
中。现在,无论何时您想要读取或写入文件,您都可以通过调用helloFile
中的File
对象上的方法来实现。
读取文件内容
现在已经有了一个File
对象,可以开始从中读取数据了。如果您想以字符串值的形式读取文件的全部内容,请使用File
对象的read()
方法。让我们继续使用您存储在helloFile
中的hello.txt
对象。在交互式 Shell 中输入以下内容:
>>> helloContent = helloFile.read()
>>> helloContent
'Hello, world!'
如果你认为一个文件的内容是一个大的字符串值,那么read()
方法返回存储在文件中的字符串。
或者,您可以使用readlines()
方法从文件中获取字符串值的列表,每行文本一个字符串。例如,在与hello.txt
相同的目录下创建一个名为sonnet29.txt
的文件,并在其中写入以下文本:
When, in disgrace with fortune and men's eyes,
I all alone beweep my outcast state,
And trouble deaf heaven with my bootless cries,
And look upon myself and curse my fate,
确保用换行符将这四行分开。然后在交互式 Shell 中输入以下内容:
>>> sonnetFile = open(Path.home() / 'sonnet29.txt')
>>> sonnetFile.readlines()
[When, in disgrace with fortune and men's eyes,\n', ' I all alone beweep my
outcast state,\n', And trouble deaf heaven with my bootless cries,\n', And
look upon myself and curse my fate,']
注意,除了文件的最后一行,每个字符串值都以换行符\n
结束。字符串列表通常比单个大型字符串值更容易处理。
写入文件
Python 允许你以类似于print()
函数将字符串“写入”屏幕的方式将内容写入文件。但是,您不能写入以读取模式打开的文件。而是需要在“写纯文本”模式或者“附加纯文本”模式下打开,或者简称为写模式和附加模式。
写模式将覆盖现有文件并从头开始,就像用新值覆盖变量值一样。将'w'
作为第二个参数传递给open()
,以写模式打开文件。另一方面,附加模式会将文本附加到现有文件的末尾。你可以认为这是在一个变量中附加一个列表,而不是完全覆盖这个变量。将'a'
作为第二个参数传递给open()
,以附加模式打开文件。
如果传递给open()
的文件名不存在,写入和附加模式都将创建一个新的空白文件。在读取或写入文件之后,在再次打开文件之前调用close()
方法。
让我们把这些概念放在一起。在交互式 Shell 中输入以下内容:
>>> baconFile = open('bacon.txt', 'w')
>>> baconFile.write('Hello, world!\n')
13
>>> baconFile.close()
>>> baconFile = open('bacon.txt', 'a')
>>> baconFile.write('Bacon is not a vegetable.')
25
>>> baconFile.close()
>>> baconFile = open('bacon.txt')
>>> content = baconFile.read()
>>> baconFile.close()
>>> print(content)
Hello, world!
Bacon is not a vegetable.
首先,我们以写模式打开bacon.txt
。由于还没有一个bacon.txt
,Python 创建了一个。在打开的文件上调用write()
并向write()
传递字符串参数'Hello, world! /n'
将字符串写入文件并返回写入的字符数,包括换行符。然后我们关闭文件。
为了将文本添加到文件的现有内容中,而不是替换我们刚刚编写的字符串,我们以附加模式打开文件。我们将'Bacon is not a vegetable.'
写入文件并关闭它。最后,为了将文件内容打印到屏幕上,我们以默认的读取模式打开文件,调用read()
,将生成的File
对象存储到content
,关闭文件,并打印content
。
注意,write()
方法不会像print()
函数那样自动在字符串末尾添加一个换行符。你必须自己添加这个字符。
从 Python 3.6 开始,您还可以向open()
函数传递一个Path
对象,而不是文件名的字符串。
用shelve
模块保存变量
您可以使用shelve
模块将 Python 程序中的变量保存到二进制文件中。这样,您的程序可以将数据从硬盘恢复到变量中。shelve
模块将允许你添加保存和打开特性到你的程序中。例如,如果您运行一个程序并输入一些配置设置,您可以将这些设置保存到一个架子文件中,然后让程序在下次运行时加载它们。
在交互式 Shell 中输入以下内容:
>>> import shelve
>>> shelfFile = shelve.open('mydata')
>>> cats = ['Zophie', 'Pooka', 'Simon']
>>> shelfFile['cats'] = cats
>>> shelfFile.close()
要使用shelve
模块读写数据,首先要导入shelve
。调用shelve.open()
并给它传递一个文件名,然后将返回的架子值存储在一个变量中。您可以像修改字典一样修改架子值。完成后,调用架子值上的close()
。在这里,我们的架子值存储在shelfFile
。我们创建一个列表cats
并编写shelfFile['cats'] = cats
将列表存储在shelfFile
中,作为与键'cats'
相关联的值(就像在字典中一样)。然后我们在shelfFile
上调用close()
。注意,从 Python 3.7 开始,必须将open()
shelf 方法文件名作为字符串传递。你不能传递它Path
对象。
在 Windows 上运行前面的代码后,你会在当前工作目录中看到三个新文件: mydata.bak
、mydata.dat
和mydata.dir
。在 MacOS 上,只会创建一个mydata.db
文件。
这些二进制文件包含您存储在架子上的数据。这些二进制文件的格式并不重要;你只需要知道shelve
模块做什么,而不需要知道它是怎么做的。该模块使您不必担心如何将程序数据存储到文件中。
您的程序可以使用shelve
模块在以后重新打开并从这些架子文件中检索数据。架值不一定要以读或写模式打开,一旦打开就可以两种模式都打开。在交互式 Shell 中输入以下内容:
>>> shelfFile = shelve.open('mydata')
>>> type(shelfFile)
<class 'shelve.DbfilenameShelf'>
>>> shelfFile['cats']
['Zophie', 'Pooka', 'Simon']
>>> shelfFile.close()
在这里,我们打开架子文件来检查我们的数据是否被正确存储。输入shelfFile['cats']
返回我们之前存储的相同列表,因此我们知道列表被正确存储,我们调用close()
。
就像字典一样,架子值有keys()
和values()
方法,它们将返回架子中键和值的类似列表的值。由于这些方法返回类似列表的值,而不是真正的列表,您应该将它们传递给list()
函数,以列表的形式获取它们。在交互式 Shell 中输入以下内容:
>>> shelfFile = shelve.open('mydata')
>>> list(shelfFile.keys())
['cats']
>>> list(shelfFile.values())
[['Zophie', 'Pooka', 'Simon']]
>>> shelfFile.close()
纯文本对于创建您将在文本编辑器(如记事本或文本编辑)中阅读的文件很有用,但如果您想保存 Python 程序中的数据,请使用shelve
模块。
用pprint.pformat()
函数保存变量
回想一下第 118 页上的漂亮打印中的内容,pprint.pprint()
函数会将列表或字典的内容“漂亮打印”到屏幕上,而pprint.pformat()
函数会将相同的文本作为字符串返回,而不是打印出来。这个字符串不仅被格式化为易于阅读,而且还是语法正确的 Python 代码。假设您有一个存储在变量中的字典,您想保存这个变量及其内容以供将来使用。使用pprint.pformat()
会给你一个字符串,你可以把它写到py
文件。这个文件将是您自己的模块,您可以在需要使用存储在其中的变量时导入它。
例如,在交互式 Shell 中输入以下内容:
>>> import pprint
>>> cats = [{'name': 'Zophie', 'desc': 'chubby'}, {'name': 'Pooka', 'desc': 'fluffy'}]
>>> pprint.pformat(cats)
"[{'desc': 'chubby', 'name': 'Zophie'}, {'desc': 'fluffy', 'name': 'Pooka'}]"
>>> fileObj = open('myCats.py', 'w')
>>> fileObj.write('cats = ' + pprint.pformat(cats) + '\n')
83
>>> fileObj.close()
这里,我们导入pprint
让我们使用pprint.pformat()
。我们有一个字典列表,存储在变量cats
中。为了保持cats
中的列表在我们关闭 Shell 后仍然可用,我们使用pprint.pformat()
将其作为字符串返回。一旦我们将cats
中的数据作为一个字符串,就很容易将该字符串写入一个文件,我们称之为myCats.py
。
一个import
语句导入的模块本身就是 Python 脚本。当来自pprint.pformat()
的字符串被保存到py
文件时,该文件是一个可以像其他模块一样导入的模块。
因为 Python 脚本本身只是带有py
文件扩展名的文本文件。你的 Python 程序甚至可以生成其他 Python 程序。然后,您可以将这些文件导入脚本。
>>> import myCats
>>> myCats.cats
[{'name': 'Zophie', 'desc': 'chubby'}, {'name': 'Pooka', 'desc': 'fluffy'}]
>>> myCats.cats[0]
{'name': 'Zophie', 'desc': 'chubby'}
>>> myCats.cats[0]['name']
'Zophie'
创造.py
文件的好处(相对于用shelve
模块保存变量)是因为它是一个文本文件,文件的内容可以被任何人用一个简单的文本编辑器读取和修改。然而,对于大多数应用,使用shelve
模块保存数据是将变量保存到文件的首选方式。只有整数、浮点、字符串、列表和字典等基本数据类型可以作为简单文本写入文件。File
例如,对象不能被编码为文本。
项目:生成随机测验文件
假设你是一名地理老师,班上有 35 名学生,你想对美国各州首府进行一次突击测验。唉,你们班有几个坏蛋在里面,你不能相信学生不作弊。你想随机化问题的顺序,这样每个测验都是独一无二的,任何人都不可能抄袭别人的答案。当然,手工操作将会是一件冗长而无聊的事情。幸运的是,你知道一些 Python。
该程序的功能如下:
- 创建 35 个不同的测验
- 以随机顺序为每个测验创建 50 个多项选择题
- 按照随机顺序,为每个问题提供正确答案和三个