Think Python/文件
到目前为止,我们看到的大多数程序都是临时的,因为它们只运行一小段时间并产生一些输出,但是当它们结束时,它们的数据就会消失。如果你再次运行程序,它将从一个干净的状态开始。
其他程序是持久性的:它们运行很长时间(或一直运行);它们将至少一部分数据保存在永久存储器(例如硬盘驱动器)中;如果它们关闭并重新启动,它们将从上次中断的地方继续。
持久性程序的示例包括操作系统,它几乎在计算机开机时一直运行,以及 Web 服务器,它一直运行,等待网络上的请求。
程序维护其数据最简单的方法之一是读写文本文件。我们已经看到了读取文本文件的程序;在本章中,我们将看到编写它们的程序。
另一种方法是将程序的状态存储在数据库中。在本章中,我将介绍一个简单的数据库和一个模块,腌制,它使存储程序数据变得容易。
文本文件是存储在永久介质(如硬盘驱动器、闪存或 CD-ROM)上的字符序列。我们在第 9.1 节中看到了如何打开和读取文件。
要写入文件,你必须用模式 'w'
作为第二个参数打开它
>>> fout = open('output.txt', 'w')
>>> print fout
<open file 'output.txt', mode 'w' at 0xb7eb2410>
如果文件已经存在,用写入模式打开它会清除旧数据并从头开始,所以要小心!如果文件不存在,则会创建一个新文件。
该写方法将数据放入文件。
>>> line1 = "This here's the wattle,\n"
>>> fout.write(line1)
同样,文件对象会跟踪它所在的位置,因此如果你调用写再次,它会将新数据添加到末尾。
>>> line2 = "the emblem of our land.\n"
>>> fout.write(line2)
当你完成写入时,你必须关闭文件。
>>> fout.close()
的实参必须是字符串,因此如果我们想要将其他值放入文件,我们必须将它们转换为字符串。最简单的方法是使用写str另一种方法是使用格式化运算符,:
>>> x = 52
>>> f.write(str(x))
。当应用于整数时,%是模运算符。但当第一个操作数是字符串时,%是格式化运算符。%第一个操作数是格式化字符串,第二个操作数是表达式元组。结果是一个字符串,其中包含表达式的值,根据格式化字符串进行格式化。
例如,格式化序列 '%d'
表示元组中的第一个表达式应格式化为整数(
d代表“十进制”)结果是字符串 '42'
,不要与整数 42
混淆。
>>> camels = 42
>>> '%d' % camels
'42'
格式化序列可以出现在格式化字符串中的任何位置,因此你可以将一个值嵌入句子中42.
格式化序列 '%g'
将元组中的下一个元素格式化为浮点数(不要问为什么),'%s'
将下一个元素格式化为字符串
>>> camels = 42
>>> 'I have spotted %d camels.' % camels
'I have spotted 42 camels.'
元组中的元素数量必须与字符串中的格式化序列数量匹配。此外,元素的类型必须与格式化序列匹配
>>> 'In %d years I have spotted %g %s.' % (3, 0.1, 'camels')
'In 3 years I have spotted 0.1 camels.'
在第一个示例中,元素数量不足;在第二个示例中,元素类型错误。
>>> '%d %d %d' % (1, 2)
TypeError: not enough arguments for format string
>>> '%d' % 'dollars'
TypeError: illegal argument type for built-in operation
格式化运算符功能强大,但难以使用。你可以在以下位置阅读有关它的更多信息:
docs.python.org/lib/typesseq-strings.html文件名和路径.
os
该模块提供用于处理文件和目录的函数(“os”代表“操作系统”)。os.getcwd返回当前目录的名称cwd
>>> import os
>>> cwd = os.getcwd()
>>> print cwd
/home/dinsdale
代表“当前工作目录”。本示例中的结果为/home/dinsdale,这是名为dinsdale的用户的主目录。.
像代表“当前工作目录”。本示例中的结果为这样的字符串标识文件被称为路径。相对路径从当前目录开始;绝对路径从文件系统中的最顶层目录开始。
到目前为止,我们看到的路径都是简单文件名,因此它们相对于当前目录。要查找文件的绝对路径,可以使用os.path.abspath:
>>> os.path.abspath('memo.txt')
'/home/dinsdale/memo.txt'
os.path.exists检查文件或目录是否存在
>>> os.path.exists('memo.txt')
True
如果存在,os.path.isdir检查它是否是目录
>>> os.path.isdir('memo.txt')
False
>>> os.path.isdir('music')
True
类似地,os.path.isfile检查它是否是文件。
os.listdir返回给定目录中的文件(和其他目录)列表
>>> os.listdir(cwd)
['music', 'photos', 'memo.txt']
为了演示这些函数,以下示例“遍历”一个目录,打印所有文件的名称,并在所有目录上递归调用自身。
def walk(dir):
for name in os.listdir(dir):
path = os.path.join(dir, name)
if os.path.isfile(path):
print path
else:
walk(path)
os.path.join获取一个目录和一个文件名,并将它们组合成一个完整路径。
修改'walk',使其不再打印文件的名称,而是返回一个名称列表。
该 'os' 模块提供一个名为 'walk' 的函数,它类似于此函数,但功能更强大。阅读文档并使用它打印给定目录及其子目录中文件的名称。
当你尝试读写文件时,很多事情都可能出错。如果你尝试打开一个不存在的文件,你会得到一个IOError:
>>> fin = open('bad_file')
IOError: [Errno 2] No such file or directory: 'bad_file'
如果你没有权限访问文件
>>> fout = open('/etc/passwd', 'w')
IOError: [Errno 13] Permission denied: '/etc/passwd'
如果你尝试打开一个目录进行读取,你会得到
>>> fin = open('/home')
IOError: [Errno 21] Is a directory
为了避免这些错误,你可以使用像os.path.exists和os.path.isfile这样的函数,但这将花费很多时间和代码来检查所有可能性(如果“Errno 21”有任何指示,至少有 21 件事可能出错)。
最好是继续尝试,并在发生问题时处理它们,这正是try语句所做的。语法类似于if语句
try:
fin = open('bad_file')
for line in fin:
print line
fin.close()
except:
print 'Something went wrong.'
Python 从执行try子句开始。如果一切顺利,它将跳过except子句并继续执行。如果发生异常,它将跳出try子句并执行except子句。
使用try语句处理异常被称为捕获异常。在本示例中,except子句打印了一个不太有帮助的错误消息。通常,捕获异常可以让你有机会修复问题,或重试,或至少以优雅的方式结束程序。
数据库是一个组织用于存储数据的文件。大多数数据库的组织方式类似于字典,因为它们从键映射到值。最大的区别在于数据库位于磁盘(或其他永久存储器)上,因此在程序结束时它会保留下来。
该模块anydbm提供用于创建和更新数据库文件的接口。例如,我将创建一个包含图像文件字幕的数据库。
打开数据库类似于打开其他文件
>>> import anydbm
>>> db = anydbm.open('captions.db', 'c')
模式 'c'
表示如果数据库不存在,则应该创建它。结果是一个数据库对象,它可以像字典一样使用(对于大多数操作)。如果你创建一个新项目,anydbm更新数据库文件。
>>> db['cleese.png'] = 'Photo of John Cleese.'
当你访问其中一个项目时,anydbm读取文件
>>> print db['cleese.png']
Photo of John Cleese.
如果你对现有键进行另一个赋值,anydbm替换旧值
>>> db['cleese.png'] = 'Photo of John Cleese doing a silly walk.'
>>> print db['cleese.png']
Photo of John Cleese doing a silly walk.
许多字典方法,如keys和items,也适用于数据库对象。迭代使用for语句也是如此。
for key in db:
print key
与其他文件一样,当你完成时,你应该关闭数据库
>>> db.close()
的局限性在于anydbm键和值必须是字符串。如果尝试使用任何其他类型,则会收到错误。
该腌制模块可以提供帮助。它将几乎任何类型的对象转换为适合存储在数据库中的字符串,然后将字符串转换回对象。
pickle.dumps以对象作为参数并返回字符串表示(dumps是“转储字符串”的缩写)
>>> import pickle
>>> t = [1, 2, 3]
>>> pickle.dumps(t)
'(lp0\nI1\naI2\naI3\na.'
该格式对人类读者来说并不明显;它旨在易于腌制解释。pickle.loads(“加载字符串”)重建对象
>>> t1 = [1, 2, 3]
>>> s = pickle.dumps(t1)
>>> t2 = pickle.loads(s)
>>> print t2
[1, 2, 3]
虽然新对象的值与旧对象相同,但它(通常)不是同一个对象
>>> t == t2
True
>>> t is t2
False
换句话说,腌制然后取消腌制的效果与复制对象相同。
可以使用腌制在数据库中存储非字符串。事实上,这种组合非常常见,以至于它已被封装在一个名为shelve.
如果您完成了练习 '12.4',请修改您的解决方案,使其创建一个将列表中的每个单词映射到使用相同字母集的单词列表的数据库。
大多数操作系统都提供命令行界面,也称为shell。Shell 通常提供命令来导航文件系统和启动应用程序。例如,在 Unix 中,可以使用cd更改目录,使用ls显示目录的内容,并通过键入(例如)启动 Web 浏览器Firefox.
可以使用管道从 Python 启动从 shell 启动的任何程序。管道是一个表示正在运行的进程的对象。
例如,Unix 命令ls -l通常显示当前目录的内容(以长格式)。可以使用ls与os.popen:
>>> cmd = 'ls -l'
>>> fp = os.popen(cmd)
参数是一个包含 shell 命令的字符串。返回值是一个文件指针,其行为与打开的文件完全相同。可以使用ls进程一次一行地读取输出readline或一次获取所有内容read:
>>> res = fp.read()
完成后,像文件一样关闭管道
>>> stat = fp.close()
>>> print stat
None
返回值是ls进程的最终状态;None表示它正常结束(没有错误)。
管道的一个常见用途是增量读取压缩文件;也就是说,无需一次性解压缩整个文件。以下函数以压缩文件的名称作为参数,并返回使用gzip解压缩内容的管道
def open_gzip(filename):
cmd = 'gunzip -c ' + filename
fp = os.popen(cmd)
return fp
如果从fp一次读取一行,则无需将解压缩的文件存储在内存或磁盘中。
任何包含 Python 代码的文件都可以作为模块导入。例如,假设您有一个名为wc.py的文件,其中包含以下代码
def linecount(filename):
count = 0
for line in open(filename):
count += 1
return count
print linecount('wc.py')
如果运行此程序,它将读取自身并打印文件中行的数量,即 7。也可以这样导入它
>>> import wc
7
现在您有一个模块对象wc:
>>> print wc
<module 'wc' from 'wc.py'>
它提供了一个名为 linecount
的函数
>>> wc.linecount('wc.py')
7
这就是在 Python 中编写模块的方式。
此示例中唯一的问题是,当导入模块时,它会在底部执行测试代码。通常,当导入模块时,它会定义新函数,但不会执行它们。
将作为模块导入的程序通常使用以下习语
if __name__ == '__main__':
print linecount('wc.py')
__name__
是一个内置变量,在程序启动时设置。如果程序正在作为脚本运行,__name__
的值为 __main__
;在这种情况下,测试代码将被执行。否则,如果正在导入模块,则跳过测试代码。
__name__
的值是什么?警告:如果导入已导入的模块,Python 不会执行任何操作。它不会重新读取文件,即使它已更改。
如果要重新加载模块,可以使用内置函数 'reload',但这可能很棘手,因此最安全的操作是重新启动解释器,然后再次导入模块。
在读取和写入文件时,可能会遇到与空格相关的错误。这些错误可能很难调试,因为空格、制表符和换行符通常是不可见的
>>> 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
。有些系统同时使用这两种方式。如果在不同系统之间移动文件,这些不一致可能会导致问题。
对于大多数系统,都有应用程序可以将一种格式转换为另一种格式。您可以在wikipedia.org/wiki/Newline上找到它们(并详细了解此问题)。或者,当然,您也可以自己编写一个。
- 持久
- 与无限期运行并将至少部分数据保存在永久存储中的程序相关。
- 格式运算符
- 一个运算符,%,它接受一个格式字符串和一个元组,并生成一个包含元组元素的字符串,这些元素根据格式字符串中指定的格式进行格式化。
- 格式字符串
- 与格式运算符一起使用的字符串,其中包含格式序列。
- 格式序列
- 格式字符串中的一系列字符,例如%d,它指定了如何格式化值。
- 文本文件
- 存储在永久存储(如硬盘驱动器)中的字符序列。
- 目录
- 文件的有命名集合,也称为文件夹。
- 路径
- 标识文件的字符串。
- 相对路径
- 从当前目录开始的路径。
- 绝对路径
- 从文件系统中最顶层目录开始的路径。
- 捕获
- 使用try和except语句阻止异常终止程序。
- 数据库
- 其内容组织得像字典一样的文件,其中键对应于值。
import urllib
conn = urllib.urlopen('http://thinkpython.com/secret.html')
for line in conn.fp:
print line.strip()
运行此代码并按照您看到的说明进行操作。
- 编写一个程序,递归地搜索目录及其所有子目录,并返回具有给定后缀(如 '.mp3')的所有文件的完整路径列表。提示:'os.path' 提供了几个用于操作文件和路径名的有用函数。
- 要识别重复项,可以使用哈希函数,该函数读取文件并生成内容的简短摘要。例如,MD5(消息摘要算法 5)接受任意长度的“消息”,并返回一个 128 位的“校验和”。两个内容不同的文件返回相同校验和的可能性非常小。
您可以在 'wikipedia.org/wiki/Md5' 上阅读有关 MD5 的信息。在 Unix 系统上,可以使用程序 'md5sum' 和管道从 Python 计算校验和。
我编写了一个程序来解析这些文件并将它们拆分为演员姓名、电影标题等。您可以从 'thinkpython.com/code/imdb.py' 下载它。
如果将 'imdb.py' 作为脚本运行,它将读取 'actors.list.gz' 并每行打印一个演员-电影对。或者,如果 'import imdb',则可以使用函数 process_file
来处理文件。参数是文件名、函数对象和可选的要处理的行数。以下是一个示例:
''import imdb
def print_info(actor, date, title, role):
print actor, date, title, role
imdb.process_file('actors.list.gz', print_info)
''
当您调用 process_file
时,它将打开 'filename',读取内容,并针对文件中的每一行调用 print_info
。print_info
以演员、日期、电影标题和角色作为参数,并打印它们。
- 编写一个程序,读取 'actors.list.gz' 和 'actresses.list.gz' 并使用 'shelve' 构建一个数据库,将每个演员映射到他的电影列表。
- 如果两个演员至少共同出演过一部电影,那么他们就是“搭档”。处理上一步中构建的数据库,并构建第二个数据库,将每个演员映射到他的搭档列表。
- 编写一个程序,可以玩“凯文·贝肯六度分离”游戏,您可以在'wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon' 上阅读有关此游戏的更多信息。这个问题很有挑战性,因为它需要您在图中找到最短路径。您可以在 'wikipedia.org/wiki/Shortest_path_problem' 上阅读有关最短路径算法的更多信息。