文件
持久化
到目前为止,我们已经学习了如何编写程序,并使用条件执行、函数和迭代将我们的意图传达给中央处理器。我们已经学会了如何在主内存中创建和使用数据结构。CPU 和内存是我们软件工作和运行的地方。所有的“思考”都在这里发生。
但如果你还记得我们关于硬件架构的讨论,一旦电源关闭,存储在 CPU 或主内存中的任何东西都会被擦除。所以到目前为止,我们的程序只是学习 Python 的短暂有趣的练习。
辅助内存
在本章中,我们开始使用辅助内存(或文件)。辅助内存中的数据在电源关闭时不会被擦除。或者,就 U 盘而言,我们从程序中写入的数据可以从系统中移除并传输到另一个系统。
我们将主要关注读写文本文件,例如我们在文本编辑器中创建的文件。稍后我们将看到如何处理数据库文件,它们是二进制文件,专门设计用于通过数据库软件进行读写。
打开文件
当我们想要读取或写入一个文件(比如在你的硬盘上)时,我们首先必须打开 (open) 这个文件。打开文件会与你的操作系统通信,操作系统知道每个文件的数据存储在哪里。当你打开一个文件时,你是在请求操作系统按名称查找文件并确保文件存在。在这个例子中,我们打开文件 mbox.txt,它应该存储在你启动 Python 时所在的同一个文件夹中。你可以从 www.py4e.com/code3/mbox.txt 下载此文件。
>>> fhand = open('mbox.txt')
>>> print(fhand)
<_io.TextIOWrapper name='mbox.txt' mode='r' encoding='cp1252'>
如果 open
成功,操作系统会返回给我们一个文件句柄 (file handle)。文件句柄不是文件中包含的实际数据,而是我们可以用来读取数据的“句柄”。如果请求的文件存在并且你有读取该文件的适当权限,你就会得到一个句柄。
文件句柄
如果文件不存在,open
将会失败并返回一个回溯信息 (traceback),你将无法获得访问文件内容的句柄:
>>> fhand = open('stuff.txt')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'stuff.txt'
稍后我们将使用 try
和 except
来更优雅地处理我们尝试打开一个不存在的文件的情况。
文本文件和行
文本文件可以被看作是一系列行,就像 Python 字符串可以被看作是一系列字符一样。例如,这是一个记录了一个开源项目开发团队中不同个体的邮件活动的文本文件示例:
From [email protected] Sat Jan 5 09:14:16 2008
Return-Path: <[email protected]>
Date: Sat, 5 Jan 2008 09:12:18 -0500
To: [email protected]
From: [email protected]
Subject: [sakai] svn commit: r39772 - content/branches/
Details: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772
...
完整的邮件交互文件可从以下地址获取:
该文件的缩短版本可从以下地址获取:
www.py4e.com/code3/mbox-short.txt
这些文件采用包含多个邮件消息的文件的标准格式。以“From”开头的行分隔消息,以“From:”开头的行是消息的一部分。有关 mbox 格式的更多信息,请参阅 https://en.wikipedia.org/wiki/Mbox。
为了将文件分成行,有一个表示“行尾”的特殊字符,称为换行符 (newline character)。
在 Python 中,我们在字符串常量中使用反斜杠-n (\n
) 来表示换行符。尽管这看起来像两个字符,但它实际上是一个单一字符。当我们在解释器中输入“stuff”来查看变量时,它会向我们显示字符串中的 \n
,但是当我们使用 print
来显示字符串时,我们看到字符串被换行符分成了两行。
>>> stuff = 'Hello\nWorld!'
>>> stuff
'Hello\nWorld!'
>>> print(stuff)
Hello
World!
>>> stuff = 'X\nY'
>>> print(stuff)
X
Y
>>> len(stuff)
3
你还可以看到字符串 X\nY
的长度是三个字符,因为换行符是一个单一字符。
所以当我们查看文件中的行时,我们需要想象每行末尾都有一个特殊的不可见字符,称为换行符,它标记了行的结束。
因此,换行符将文件中的字符分隔成行。
读取文件
虽然文件句柄不包含文件的数据,但构造一个 for
循环来读取并计算文件中每一行的数量是相当容易的:
fhand = open('mbox-short.txt')
count = 0
for line in fhand:
count = count + 1
print('Line Count:', count)
# 代码: https://www.py4e.com/code3/open.py
我们可以使用文件句柄作为 for
循环中的序列。我们的 for
循环只是计算文件中的行数并打印出来。将 for
循环粗略地翻译成中文是:“对于文件句柄代表的文件中的每一行,将 count
变量加一。”
open
函数不读取整个文件的原因是文件可能非常大,包含数千兆字节的数据。无论文件大小如何,open
语句花费的时间是相同的。而 for
循环实际上导致数据从文件中被读取。
当以这种方式使用 for
循环读取文件时,Python 负责使用换行符将文件中的数据分割成单独的行。Python 读取每一行直到换行符,并在 for
循环的每次迭代中将换行符作为 line
变量中的最后一个字符包含进来。
因为 for
循环一次读取一行数据,所以它可以高效地读取和计算非常大文件中的行数,而不会耗尽主内存来存储数据。上面的程序可以使用非常少的内存计算任何大小文件中的行数,因为每一行都被读取、计数,然后丢弃。
如果你知道文件相对于你的主内存大小来说比较小,你可以使用文件句柄上的 read
方法将整个文件读入一个字符串中。
>>> fhand = open('mbox-short.txt')
>>> inp = fhand.read()
>>> print(len(inp))
94626
>>> print(inp[:20])
From stephen.marquar
在这个例子中,文件 mbox-short.txt 的全部内容(所有 94,626 个字符)被直接读入变量 inp
中。我们使用字符串切片打印出存储在 inp
中的字符串数据的前 20 个字符。
当以这种方式读取文件时,所有的字符,包括所有的行和换行符,都在变量 inp
中形成一个大的字符串。将 read
的输出存储为变量是一个好主意,因为每次调用 read
都会耗尽资源:
>>> fhand = open('mbox-short.txt')
>>> print(len(fhand.read()))
94626
>>> print(len(fhand.read()))
0
请记住,只有当文件数据能够舒适地容纳在你计算机的主内存中时,才应使用这种形式的 open
函数。如果文件太大而无法放入主内存,你应该编写程序使用 for
或 while
循环分块读取文件。
搜索文件
当你在文件中搜索数据时,一个非常常见的模式是通读文件,忽略大部分行,只处理满足特定条件的行。我们可以将读取文件的模式与字符串方法结合起来,构建简单的搜索机制。
例如,如果我们想读取一个文件,并且只打印出以前缀“From:”开头的行,我们可以使用字符串方法 startswith
来仅选择具有所需前缀的那些行:
fhand = open('mbox-short.txt')
for line in fhand:
if line.startswith('From:'):
print(line)
# 代码: https://www.py4e.com/code3/search1.py
当这个程序运行时,我们得到以下输出:
From: [email protected]
From: [email protected]
From: [email protected]
From: [email protected]
...
输出看起来很棒,因为我们看到的唯一行是那些以“From:”开头的行,但是为什么我们会看到额外的空行呢?这是由于那个不可见的换行符。每一行的末尾都有一个换行符,所以 print
语句打印出变量 line
中的字符串(它包含一个换行符),然后 print
又添加了另一个换行符,导致我们看到的双倍行距效果。
我们可以使用行切片来打印除最后一个字符之外的所有内容,但更简单的方法是使用 rstrip
方法,它可以从字符串的右侧去除空白,如下所示:
fhand = open('mbox-short.txt')
for line in fhand:
line = line.rstrip()
if line.startswith('From:'):
print(line)
# 代码: https://www.py4e.com/code3/search2.py
当这个程序运行时,我们得到以下输出:
From: [email protected]
From: [email protected]
From: [email protected]
From: [email protected]
From: [email protected]
From: [email protected]
From: [email protected]
...
随着你的文件处理程序变得越来越复杂,你可能希望使用 continue
来构造你的搜索循环。搜索循环的基本思想是,你正在寻找“有趣的”行,并有效地跳过“不有趣的”行。然后当我们找到一个有趣的行时,我们对该行进行某些操作。
我们可以将循环构造成遵循跳过不有趣行的模式,如下所示:
fhand = open('mbox-short.txt')
for line in fhand:
line = line.rstrip()
# 跳过 '不有趣的行'
if not line.startswith('From:'):
continue
# 处理我们 '有趣的' 行
print(line)
# 代码: https://www.py4e.com/code3/search3.py
程序的输出是相同的。用中文来说,不有趣的行是那些不以“From:”开头的行,我们使用 continue
跳过它们。对于“有趣的”行(即那些以“From:”开头的行),我们执行处理。
我们可以使用 find
字符串方法来模拟文本编辑器的搜索,查找搜索字符串出现在行中任何位置的行。由于 find
在另一个字符串中查找字符串的出现,并返回字符串的位置,如果未找到字符串则返回 -1,我们可以编写以下循环来显示包含字符串“@uct.ac.za”的行(即,它们来自南非开普敦大学):
fhand = open('mbox-short.txt')
for line in fhand:
line = line.rstrip()
if line.find('@uct.ac.za') == -1: continue
print(line)
# 代码: https://www.py4e.com/code3/search4.py
这将产生以下输出:
From [email protected] Sat Jan 5 09:14:16 2008
X-Authentication-Warning: set sender to [email protected] using -f
From: [email protected]
Author: [email protected]
From [email protected] Fri Jan 4 07:02:32 2008
X-Authentication-Warning: set sender to [email protected] using -f
From: [email protected]
Author: [email protected]
...
这里我们还使用了 if
语句的缩写形式,我们将 continue
放在与 if
同一行。这种 if
的缩写形式的功能与 continue
在下一行并缩进时相同。
让用户选择文件名
我们真的不希望每次想处理不同的文件时都必须编辑我们的 Python 代码。更可取的做法是每次程序运行时要求用户输入文件名字符串,这样他们就可以在不更改 Python 代码的情况下对不同的文件使用我们的程序。
通过使用 input
从用户那里读取文件名,这非常简单,如下所示:
fname = input('Enter the file name: ')
fhand = open(fname)
count = 0
for line in fhand:
if line.startswith('Subject:'):
count = count + 1
print('There were', count, 'subject lines in', fname)
# 代码: https://www.py4e.com/code3/search6.py
我们从用户那里读取文件名并将其放入名为 fname
的变量中,然后打开该文件。现在我们可以重复运行程序处理不同的文件了。
python search6.py
Enter the file name: mbox.txt
There were 1797 subject lines in mbox.txt
python search6.py
Enter the file name: mbox-short.txt
There were 27 subject lines in mbox-short.txt
在偷看下一节之前,请看一下上面的程序并问自己,“这里可能会出什么问题?”或者“我们友好的用户可能会做什么,导致我们可爱的小程序不雅地退出并显示回溯信息,让我们在用户眼中看起来不那么酷?”
使用 try, except,
和 open
我告诉过你不要偷看。这是你最后的机会。
如果我们的用户输入了不是文件名的内容怎么办?
python search6.py
Enter the file name: missing.txt
Traceback (most recent call last):
File "search6.py", line 2, in <module>
fhand = open(fname)
FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
python search6.py
Enter the file name: na na boo boo
Traceback (most recent call last):
File "search6.py", line 2, in <module>
fhand = open(fname)
FileNotFoundError: [Errno 2] No such file or directory: 'na na boo boo'
不要笑。用户最终会做尽一切可能的事情来破坏你的程序,无论是误操作还是恶意意图。事实上,任何软件开发团队的一个重要组成部分是一个称为质量保证(Quality Assurance,简称 QA)的人员或小组,他们的工作就是做尽可能疯狂的事情,试图破坏程序员创建的软件。
QA 团队负责在我们向最终用户交付程序之前发现程序中的缺陷,这些最终用户可能是购买软件或支付我们薪水来编写软件的人。所以 QA 团队是程序员最好的朋友。
所以现在我们看到了程序中的缺陷,我们可以使用 try
/except
结构优雅地修复它。我们需要假设 open
调用可能会失败,并在 open
失败时添加恢复代码,如下所示:
fname = input('Enter the file name: ')
try:
fhand = open(fname)
except:
print('File cannot be opened:', fname)
exit()
count = 0
for line in fhand:
if line.startswith('Subject:'):
count = count + 1
print('There were', count, 'subject lines in', fname)
# 代码: https://www.py4e.com/code3/search7.py
exit
函数终止程序。它是我们调用的一个永不返回的函数。现在,当我们的用户(或 QA 团队)输入无意义的内容或错误的文件名时,我们会“捕获”它们并优雅地恢复:
python search7.py
Enter the file name: mbox.txt
There were 1797 subject lines in mbox.txt
python search7.py
Enter the file name: na na boo boo
File cannot be opened: na na boo boo
保护 open
调用是 Python 程序中正确使用 try
和 except
的一个很好的例子。当我们以“Python 的方式”做事时,我们使用术语“Pythonic”。我们可以说上面的例子是打开文件的 Pythonic 方式。
一旦你在 Python 方面变得更加熟练,你可以与其他 Python 程序员进行交流,以决定对于一个问题的两个等效解决方案中哪一个“更 Pythonic”。追求“更 Pythonic”的目标体现了编程既是工程学也是艺术的概念。我们并不总是只对让某事能够工作感兴趣,我们也希望我们的解决方案是优雅的,并被我们的同行认为是优雅的。
写入文件
要写入文件,你必须以模式“w”作为第二个参数来打开它:
>>> fout = open('output.txt', 'w')
>>> print(fout)
<_io.TextIOWrapper name='output.txt' mode='w' encoding='cp1252'>
如果文件已经存在,以写入模式打开它会清除旧数据并重新开始,所以要小心!如果文件不存在,则会创建一个新文件。
文件句柄对象的 write
方法将数据放入文件,返回写入的字符数。默认的写入模式是用于写入(和读取)字符串的文本模式。
>>> line1 = "This here's the wattle,\n"
>>> fout.write(line1)
24
同样,文件对象会跟踪它所在的位置,所以如果你再次调用 write
,它会将新数据添加到末尾。
我们必须确保在写入文件时管理好行的结尾,通过在我们想要结束一行时显式插入换行符。print
语句会自动附加一个换行符,但 write
方法不会自动添加换行符。
>>> line2 = 'the emblem of our land.\n'
>>> fout.write(line2)
24
当你完成写入后,你必须关闭文件,以确保最后一点数据被物理写入磁盘,这样在断电时就不会丢失。
>>> fout.close()
我们也可以关闭我们为读取而打开的文件,但如果我们只打开了少量文件,我们可以稍微马虎一点,因为 Python 确保所有打开的文件在程序结束时都会关闭。当我们在写入文件时,我们希望显式关闭文件,以免留下任何侥幸的机会。
调试
当你读写文件时,你可能会遇到空白问题。这些错误可能很难调试,因为空格、制表符和换行符通常是不可见的:
>>> s = '1 2\t 3\n 4'
>>> print(s)
1 2 3
4
内置函数 repr
可以提供帮助。它接受任何对象作为参数,并返回该对象的字符串表示形式。对于字符串,它用反斜杠序列表示空白字符:
>>> print(repr(s))
'1 2\t 3\n 4'
这对于调试可能很有帮助。
你可能遇到的另一个问题是不同的系统使用不同的字符来表示行尾。有些系统使用换行符,表示为 \n
。其他系统使用回车符,表示为 \r
。有些两者都用。如果你在不同系统之间移动文件,这些不一致可能会导致问题。
对于大多数系统,有应用程序可以从一种格式转换为另一种格式。你可以在 https://www.wikipedia.org/wiki/Newline 找到它们(并阅读更多关于此问题的信息)。当然,你也可以自己编写一个。
术语表
捕获 (catch)
使用 try
和 except
语句阻止异常终止程序。
换行符 (newline)
在文件和字符串中用于表示行尾的特殊字符。
Pythonic
在 Python 中优雅地工作的技术。“使用 try 和 except 是从文件丢失中恢复的 Pythonic 方式”。
质量保证 (Quality Assurance)
专注于确保软件产品整体质量的个人或团队。QA 通常参与测试产品并在产品发布前识别问题。
文本文件 (text file)
存储在永久存储(如硬盘)中的字符序列。
练习
练习 1: 编写一个程序,通读一个文件并以全大写形式打印文件的内容(逐行)。执行该程序将如下所示:
python shout.py
Enter a file name: mbox-short.txt
FROM [email protected] SAT JAN 5 09:14:16 2008
RETURN-PATH: <[email protected]>
RECEIVED: FROM MURDER (MAIL.UMICH.EDU [141.211.14.90])
BY FRANKENSTEIN.MAIL.UMICH.EDU (CYRUS V2.3.8) WITH LMTPA;
SAT, 05 JAN 2008 09:14:16 -0500
你可以从 www.py4e.com/code3/mbox-short.txt 下载该文件。
练习 2: 编写一个程序,提示输入文件名,然后通读文件并查找以下形式的行:
X-DSPAM-Confidence: 0.8475
当你遇到以“X-DSPAM-Confidence:”开头的行时,解析该行以提取行上的浮点数。计算这些行的数量,然后计算这些行的垃圾邮件置信度值的总和。当你到达文件末尾时,打印出平均垃圾邮件置信度。
Enter the file name: mbox.txt
Average spam confidence: 0.894128046745
Enter the file name: mbox-short.txt
Average spam confidence: 0.750718518519
在 mbox.txt 和 mbox-short.txt 文件上测试你的文件。
练习 3:
有时程序员感到无聊或想找点乐子时,他们会在程序中添加一个无害的彩蛋 (Easter Egg)。修改提示用户输入文件名的程序,以便当用户输入确切的文件名“na na boo boo”时,打印一条有趣的消息。对于所有其他存在和不存在的文件,程序应正常运行。以下是该程序的一个示例执行:
python egg.py
Enter the file name: mbox.txt
There were 1797 subject lines in mbox.txt
python egg.py
Enter the file name: missing.tyxt
File cannot be opened: missing.tyxt
python egg.py
Enter the file name: na na boo boo
NA NA BOO BOO TO YOU - You have been punk'd!
我们不鼓励你在程序中放置彩蛋;这只是一个练习。
如果你在本书中发现错误,欢迎使用 Github 给我发送修正。