原文:
annas-archive.org/md5/4b0fd2cf0da7c8edae4b5ecfd40159bf译者:飞龙
第十四章:文件和目录。
我有文件,我有电脑文件,你知道的,在纸上也有文件。但大部分都在我脑子里。所以如果我的脑子出了问题,上帝帮帮我!
乔治·R·R·马丁。
当你刚开始学习编程时,你会反复听到一些词,但不确定它们是否具有特定的技术含义还是随意的说法。文件和目录就是这样的词,它们确实有实际的技术含义。文件是一系列字节,存储在某个文件系统中,并通过文件名访问。目录是文件和可能其他目录的集合。术语文件夹是目录的同义词。它出现在计算机获得图形用户界面时,模仿办公室概念,使事物看起来更加熟悉。
许多文件系统是分层的,通常被称为类似于树。真实的办公室里不会有树,文件夹类比只有在你能够想象出所有子文件夹的情况下才有效。
文件的输入和输出。
最简单的持久性形式是普通的文件,有时称为平面文件。你从文件中读取到内存中,然后从内存中写入到文件中。Python 使得这些工作变得容易。与许多语言一样,它的文件操作在很大程度上是模仿熟悉且受欢迎的 Unix 等效操作。
使用open()创建或打开。
在执行以下操作之前,您需要调用open函数:
-
读取现有文件。
-
写入到一个新文件。
-
追加到现有文件。
-
覆盖现有文件。
*`fileobj`* = open( *`filename`*, *`mode`* )
这里是对这个调用的各部分的简要解释:
-
*
fileobj*是open()返回的文件对象。 -
*
filename*是文件的字符串名称。 -
*
mode*是一个表示文件类型及其操作的字符串。
*mode*的第一个字母表示操作:
-
r表示读取。 -
w表示写入。如果文件不存在,则创建该文件。如果文件存在,则覆盖它。 -
x表示写入,但只有在文件不存在时才会写入。 -
a表示追加(在末尾写入),如果文件存在。
mode的第二个字母表示文件的类型:
-
t(或什么都不写)表示文本。 -
b表示二进制。
打开文件后,您可以调用函数来读取或写入数据;这些将在接下来的示例中展示。
最后,您需要关闭文件以确保任何写入操作都已完成,并且内存已被释放。稍后,您将看到如何使用with来自动化此过程。
此程序打开一个名为oops.txt的文件,并在不写入任何内容的情况下关闭它。这将创建一个空文件:
>>> fout = open('oops.txt', 'wt')
>>> fout.close()
使用print()写入文本文件。
让我们重新创建oops.txt,然后向其中写入一行内容,然后关闭它:
>>> fout = open('oops.txt', 'wt')
>>> print('Oops, I created a file.', file=fout)
>>> fout.close()
我们在上一节创建了一个空的oops.txt文件,所以这只是覆盖它。
我们使用了print函数的file参数。如果没有这个参数,print会将内容写入标准输出,也就是你的终端(除非你已经告诉你的 shell 程序使用>重定向输出到文件或使用|管道传输到另一个程序)。
使用write()写入文本文件。
我们刚刚使用print向文件中写入了一行。我们也可以使用write。
对于我们的多行数据源,让我们使用这首关于狭义相对论的打油诗作为例子:¹
>>> poem = '''There was a young lady named Bright,
... Whose speed was far faster than light;
... She started one day
... In a relative way,
... And returned on the previous night.'''
>>> len(poem)
150
下面的代码一次性将整首诗写入到名为'relativity'的文件中:
>>> fout = open('relativity', 'wt')
>>> fout.write(poem)
150
>>> fout.close()
write函数返回写入的字节数。它不像print那样添加空格或换行符。同样,你也可以使用print将多行字符串写入文本文件:
>>> fout = open('relativity', 'wt')
>>> print(poem, file=fout)
>>> fout.close()
那么,应该使用write还是print?正如你所见,默认情况下,print在每个参数后添加一个空格,并在末尾添加换行符。在前一个示例中,它向relativity文件附加了一个换行符。要使print像write一样工作,将以下两个参数传递给它:
-
sep(分隔符,默认为空格,' ') -
end(结束字符串,默认为换行符,'\n')
我们将使用空字符串来替换这些默认值:
>>> fout = open('relativity', 'wt')
>>> print(poem, file=fout, sep='', end='')
>>> fout.close()
如果你有一个大的源字符串,你也可以写入分片(使用切片),直到源字符串处理完毕:
>>> fout = open('relativity', 'wt')
>>> size = len(poem)
>>> offset = 0
>>> chunk = 100
>>> while True:
... if offset > size:
... break
... fout.write(poem[offset:offset+chunk])
... offset += chunk
...
100
50
>>> fout.close()
这一次在第一次尝试中写入了 100 个字符,下一次写入了最后 50 个字符。切片允许你“超过结尾”而不会引发异常。
如果对我们来说relativity文件很重要,让我们看看使用模式x是否真的保护我们免受覆盖:
>>> fout = open('relativity', 'xt')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileExistsError: [Errno 17] File exists: 'relativity'
你可以将其与异常处理器一起使用:
>>> try:
... fout = open('relativity', 'xt')]
... fout.write('stomp stomp stomp')
... except FileExistsError:
... print('relativity already exists!. That was a close one.')
...
relativity already exists!. That was a close one.
使用read()、readline()或readlines()读取文本文件
你可以不带参数调用read()一次性读取整个文件,就像下面的示例一样(在处理大文件时要小心;一个 1GB 文件将消耗 1GB 内存):
>>> fin = open('relativity', 'rt' )
>>> poem = fin.read()
>>> fin.close()
>>> len(poem)
150
你可以提供一个最大字符数来限制read()一次返回多少内容。让我们一次读取 100 个字符,并将每个块追加到poem字符串以重建原始内容:
>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> chunk = 100
>>> while True:
... fragment = fin.read(chunk)
... if not fragment:
... break
... poem += fragment
...
>>> fin.close()
>>> len(poem)
150
当你读取到结尾后,进一步调用read()会返回一个空字符串(''),这在if not fragment中被视作False。这会跳出while True循环。
你也可以使用readline()一次读取一行。在下一个示例中,我们将每一行追加到poem字符串中以重建原始内容:
>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> while True:
... line = fin.readline()
... if not line:
... break
... poem += line
...
>>> fin.close()
>>> len(poem)
150
对于文本文件,即使是空行也有长度为一(换行符),并且被视作True。当文件被读取完毕时,readline()(和read()一样)也会返回一个空字符串,同样被视作False。
读取文本文件的最简单方法是使用迭代器。它一次返回一行。与前面的示例类似,但代码更少:
>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> for line in fin:
... poem += line
...
>>> fin.close()
>>> len(poem)
150
所有前述示例最终构建了单个字符串poem。readlines()方法逐行读取,返回一个包含每行字符串的列表:
>>> fin = open('relativity', 'rt' )
>>> lines = fin.readlines()
>>> fin.close()
>>> print(len(lines), 'lines read')
5 lines read
>>> for line in lines:
... print(line, end='')
...
There was a young lady named Bright,
Whose speed was far faster than light;
She started one day
In a relative way,
And returned on the previous night.>>>
我们告诉print()不要自动换行,因为前四行已经有了换行。最后一行没有换行,导致交互提示符>>>出现在最后一行之后。
使用write()写入二进制文件
如果在模式字符串中包含 'b',文件将以二进制模式打开。在这种情况下,你读取和写入的是 bytes 而不是字符串。
我们手头没有二进制诗歌,所以我们只会生成从 0 到 255 的 256 个字节值:
>>> bdata = bytes(range(0, 256))
>>> len(bdata)
256
以二进制模式打开文件进行写入,并一次性写入所有数据:
>>> fout = open('bfile', 'wb')
>>> fout.write(bdata)
256
>>> fout.close()
同样,write() 返回写入的字节数。
和文本一样,你可以将二进制数据分块写入:
>>> fout = open('bfile', 'wb')
>>> size = len(bdata)
>>> offset = 0
>>> chunk = 100
>>> while True:
... if offset > size:
... break
... fout.write(bdata[offset:offset+chunk])
... offset += chunk
...
100
100
56
>>> fout.close()
使用 read() 读取二进制文件
这个很简单;你只需要用 'rb' 打开即可:
>>> fin = open('bfile', 'rb')
>>> bdata = fin.read()
>>> len(bdata)
256
>>> fin.close()
使用 with 自动关闭文件
如果你忘记关闭已打开的文件,在不再引用后 Python 会关闭它。这意味着如果你在函数中打开文件但没有显式关闭它,在函数结束时文件会被自动关闭。但你可能在长时间运行的函数或程序的主要部分中打开了文件。应该关闭文件以确保所有未完成的写入被完成。
Python 有上下文管理器来清理诸如打开的文件之类的资源。你可以使用形式 with 表达式 as 变量:
>>> with open('relativity', 'wt') as fout:
... fout.write(poem)
...
就是这样。在上下文管理器(在本例中就是一行代码块)完成(正常完成 或 通过抛出异常)后,文件会自动关闭。
使用 seek() 改变位置
当你读取和写入时,Python 会跟踪你在文件中的位置。tell() 函数返回你当前从文件开头的偏移量,以字节为单位。seek() 函数让你跳到文件中的另一个字节偏移量。这意味着你不必读取文件中的每个字节来读取最后一个字节;你可以 seek() 到最后一个字节并只读取一个字节。
对于这个示例,使用你之前写的 256 字节二进制文件 'bfile':
>>> fin = open('bfile', 'rb')
>>> fin.tell()
0
使用 seek() 跳转到文件末尾前一个字节:
>>> fin.seek(255)
255
读取直到文件末尾:
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255
seek() 也会返回当前偏移量。
你可以给 seek() 调用一个第二参数:seek(*`offset`*, *`origin`*):
-
如果
origin是0(默认值),就从文件开头向后offset字节 -
如果
origin是1,就从当前位置向后offset字节 -
如果
origin是2,就从文件末尾相对offset字节
这些值也在标准的 os 模块中定义:
>>> import os
>>> os.SEEK_SET
0
>>> os.SEEK_CUR
1
>>> os.SEEK_END
2
因此,我们可以用不同的方式读取最后一个字节:
>>> fin = open('bfile', 'rb')
文件末尾前一个字节:
>>> fin.seek(-1, 2)
255
>>> fin.tell()
255
读取直到文件末尾:
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255
注意
你不需要调用 tell() 来让 seek() 工作。我只是想展示它们报告相同的偏移量。
下面是从文件当前位置进行搜索的示例:
>>> fin = open('bfile', 'rb')
这个示例最终会在文件末尾前两个字节处结束:
>>> fin.seek(254, 0)
254
>>> fin.tell()
254
现在向前移动一个字节:
>>> fin.seek(1, 1)
255
>>> fin.tell()
255
最后,读取直到文件末尾:
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255
这些函数对于二进制文件最有用。你可以用它们处理文本文件,但除非文件是 ASCII(每个字符一个字节),否则计算偏移量会很困难。这将取决于文本编码,而最流行的编码(UTF-8)使用不同数量的字节表示每个字符。
内存映射
读取和写入文件的替代方法是使用标准mmap模块将其内存映射。 这使得文件内容在内存中看起来像一个bytearray。 有关详细信息,请参阅文档和一些示例。
文件操作
Python,像许多其他语言一样,根据 Unix 模式化其文件操作。 一些函数,例如chown()和chmod(),具有相同的名称,但还有一些新函数。
首先,我将展示 Python 如何使用os.path模块的函数以及使用较新的pathlib模块处理这些任务。
使用exists()检查存在性。
要验证文件或目录是否确实存在,或者您只是想象了它,您可以提供exists(),并提供相对或绝对路径名,如此示例所示:
>>> import os
>>> os.path.exists('oops.txt')
True
>>> os.path.exists('./oops.txt')
True
>>> os.path.exists('waffles')
False
>>> os.path.exists('.')
True
>>> os.path.exists('..')
True
使用isfile()检查类型。
此部分中的函数检查名称是否引用文件、目录或符号链接(有关链接讨论的示例,请参见后续内容)。
我们将首先查看的第一个函数isfile,它提出一个简单的问题:这是一个普通的老实文件吗?
>>> name = 'oops.txt'
>>> os.path.isfile(name)
True
下面是确定目录的方法:
>>> os.path.isdir(name)
False
单个点(.)是当前目录的简写,两个点(..)代表父目录。 这些始终存在,因此像以下语句将始终报告True:
>>> os.path.isdir('.')
True
os模块包含许多处理路径名(完全合格的文件名,以/开头并包括所有父级)的函数。 其中一个函数isabs()确定其参数是否为绝对路径名。 参数不需要是真实文件的名称:
>>> os.path.isabs(name)
False
>>> os.path.isabs('/big/fake/name')
True
>>> os.path.isabs('big/fake/name/without/a/leading/slash')
False
使用copy()复制。
copy()函数来自另一个模块shutil。 例如,将文件oops.txt复制到文件ohno.txt:
>>> import shutil
>>> shutil.copy('oops.txt', 'ohno.txt')
shutil.move()函数复制文件,然后删除原始文件。
使用rename()函数更改名称。
此函数正如其名称所示。 在此示例中,它将ohno.txt重命名为ohwell.txt:
>>> import os
>>> os.rename('ohno.txt', 'ohwell.txt')
使用link()或symlink()链接。
在 Unix 中,文件存在于一个位置,但可以有多个名称,称为链接。 在低级硬链接中,很难找到给定文件的所有名称。 符号链接是一种替代方法,它将新名称存储为自己的文件,使您可以同时获取原始名称和新名称。 link()调用创建硬链接,symlink()创建符号链接。 islink()函数检查文件是否是符号链接。
下面是如何为现有文件oops.txt创建硬链接到新文件yikes.txt的方法:
>>> os.link('oops.txt', 'yikes.txt')
>>> os.path.isfile('yikes.txt')
True
>>> os.path.islink('yikes.txt')
False
要为现有文件oops.txt创建到新文件jeepers.txt的符号链接,请使用以下命令:
>>> os.symlink('oops.txt', 'jeepers.txt')
>>> os.path.islink('jeepers.txt')
True
使用chmod()更改权限。
在 Unix 系统中,chmod() 改变文件权限。对于用户(通常是你,如果你创建了该文件)、用户所在的主要组和其余世界,有读、写和执行权限。该命令使用紧凑的八进制(基数 8)值,结合用户、组和其他权限。例如,要使 oops.txt 只能由其所有者读取,输入以下内容:
>>> os.chmod('oops.txt', 0o400)
如果你不想处理晦涩的八进制值,而宁愿处理(稍微不那么)晦涩的符号,可以从 stat 模块导入一些常量,并使用如下语句:
>>> import stat
>>> os.chmod('oops.txt', stat.S_IRUSR)
使用 chown() 更改所有权
这个函数同样适用于 Unix/Linux/Mac。你可以通过指定数值用户 ID (uid) 和组 ID (gid) 来改变文件的所有者和/或组所有权:
>>> uid = 5
>>> gid = 22
>>> os.chown('oops', uid, gid)
使用 remove() 删除文件
在这段代码中,我们使用 remove() 函数,告别 oops.txt:
>>> os.remove('oops.txt')
>>> os.path.exists('oops.txt')
False
目录操作
在大多数操作系统中,文件存在于层级结构的目录(通常称为文件夹)中。所有这些文件和目录的容器是一个文件系统(有时称为卷)。标准的 os 模块处理这些操作系统的具体细节,并提供以下函数,用于对它们进行操作。
使用 mkdir() 创建
此示例展示了如何创建一个名为 poems 的目录来存储那些珍贵的诗句:
>>> os.mkdir('poems')
>>> os.path.exists('poems')
True
使用 rmdir() 删除目录
经过重新考虑²,你决定其实根本不需要那个目录。以下是如何删除它的方法:
>>> os.rmdir('poems')
>>> os.path.exists('poems')
False
使用 listdir() 列出内容
好的,重来一次;让我们再次创建 poems,并添加一些内容:
>>> os.mkdir('poems')
现在获取其内容列表(到目前为止还没有):
>>> os.listdir('poems')
[]
接下来,创建一个子目录:
>>> os.mkdir('poems/mcintyre')
>>> os.listdir('poems')
['mcintyre']
在这个子目录中创建一个文件(如果你真的感觉有诗意,才输入所有这些行;确保使用匹配的单引号或三重引号开头和结尾):
>>> fout = open('poems/mcintyre/the_good_man', 'wt')
>>> fout.write('''Cheerful and happy was his mood,
... He to the poor was kind and good,
... And he oft' times did find them food,
... Also supplies of coal and wood,
... He never spake a word was rude,
... And cheer'd those did o'er sorrows brood,
... He passed away not understood,
... Because no poet in his lays
... Had penned a sonnet in his praise,
... 'Tis sad, but such is world's ways.
... ''')
344
>>> fout.close()
最后,让我们看看我们有什么。它最好在那里:
>>> os.listdir('poems/mcintyre')
['the_good_man']
使用 chdir() 更改当前目录
使用此函数,你可以从一个目录切换到另一个目录。让我们离开当前目录,花一点时间在 poems 中:
>>> import os
>>> os.chdir('poems')
>>> os.listdir('.')
['mcintyre']
使用 glob() 列出匹配的文件
glob() 函数使用 Unix shell 规则而非更完整的正则表达式语法来匹配文件或目录名。以下是这些规则:
-
*匹配任何内容(re应该期望.*) -
?匹配一个单字符 -
[abc]匹配字符a、b或c -
[!abc]匹配除了a、b或c之外的任何字符
尝试获取所有以 m 开头的文件或目录:
>>> import glob
>>> glob.glob('m*')
['mcintyre']
任何两个字母的文件或目录如何?
>>> glob.glob('??')
[]
我在想一个以 m 开头、以 e 结尾的八个字母的单词:
>>> glob.glob('m??????e')
['mcintyre']
那么任何以 k、l 或 m 开头、以 e 结尾的内容呢?
>>> glob.glob('[klm]*e')
['mcintyre']
路径名
几乎所有的计算机都使用层次化文件系统,其中目录(“文件夹”)包含文件和其他目录,向下延伸到不同的层级。当您想引用特定的文件或目录时,您需要它的 路径名:到达那里所需的目录序列,可以是 绝对 从顶部(根)或 相对 到您当前目录。
当您指定名称时,您经常会听到人们混淆正斜杠('/',而不是 Guns N’ Roses 的家伙)和反斜杠('\')。³ Unix 和 Mac(以及 Web URL)使用正斜杠作为 路径分隔符,而 Windows 使用反斜杠。⁴
Python 允许您在指定名称时使用斜杠作为路径分隔符。在 Windows 上,您可以使用反斜杠,但是您知道反斜杠在 Python 中是普遍的转义字符,所以您必须在所有地方加倍使用它,或者使用 Python 的原始字符串:
>>> win_file = 'eek\\urk\\snort.txt'
>>> win_file2 = r'eek\urk\snort.txt'
>>> win_file
'eek\\urk\\snort.txt'
>>> win_file2
'eek\\urk\\snort.txt'
当您构建路径名时,您可以做以下操作:
-
使用适当的路径分隔符 (
'/'或'\') -
使用 os.path.join() 构建路径名(参见 “使用 os.path.join() 构建路径名”)
-
使用 pathlib(参见 “使用 pathlib”)
通过 abspath() 获取路径名
这个函数将相对名扩展为绝对名。如果您的当前目录是 /usr/gaberlunzie 并且文件 oops.txt 就在那里,您可以输入以下内容:
>>> os.path.abspath('oops.txt')
'/usr/gaberlunzie/oops.txt'
使用 realpath() 获取符号链接路径名
在较早的某一部分中,我们从新文件 jeepers.txt 创造了对 oops.txt 的符号链接。在这种情况下,您可以使用 realpath() 函数从 jeepers.txt 获取 oops.txt 的名称,如下所示:
>>> os.path.realpath('jeepers.txt')
'/usr/gaberlunzie/oops.txt'
使用 os.path.join() 构建路径名
当您构建一个多部分的路径名时,您可以使用 os.path.join() 将它们成对地组合,使用适合您操作系统的正确路径分隔符:
>>> import os
>>> win_file = os.path.join("eek", "urk")
>>> win_file = os.path.join(win_file, "snort.txt")
如果我在 Mac 或 Linux 系统上运行这个程序,我会得到这个结果:
>>> win_file
'eek/urk/snort.txt'
在 Windows 上运行会产生这个结果:
>>> win_file
'eek\\urk\\snort.txt'
但是如果相同的代码在不同位置运行会产生不同的结果,这可能是一个问题。新的 pathlib 模块是这个问题的一个便携解决方案。
使用 pathlib
Python 在版本 3.4 中添加了 pathlib 模块。它是我刚刚描述的 os.path 模块的一个替代方案。但我们为什么需要另一个模块呢?
它不是把文件系统路径名当作字符串,而是引入了 Path 对象来在稍高一级处理它们。使用 Path() 类创建一个 Path,然后用裸斜线(而不是 '/' 字符)将您的路径编织在一起:
>>> from pathlib import Path
>>> file_path = Path('eek') / 'urk' / 'snort.txt'
>>> file_path
PosixPath('eek/urk/snort.txt')
>>> print(file_path)
eek/urk/snort.txt
这个斜杠技巧利用了 Python 的 “魔术方法”。一个 Path 可以告诉您关于自己的一些信息:
>>> file_path.name
'snort.txt'
>>> file_path.suffix
'.txt'
>>> file_path.stem
'snort'
您可以像对待任何文件名或路径名字符串一样将 file_path 提供给 open()。
您还可以看到如果在另一个系统上运行此程序会发生什么,或者如果需要在您的计算机上生成外国路径名:
>>> from pathlib import PureWindowsPath
>>> PureWindowsPath(file_path)
PureWindowsPath('eek/urk/snort.txt')
>>> print(PureWindowsPath(file_path))
eek\urk\snort.txt
参见文档以获取所有细节。
BytesIO 和 StringIO
你已经学会了如何修改内存中的数据以及如何将数据读取到文件中和从文件中获取数据。如果你有内存中的数据,但想调用一个期望文件的函数(或者反过来),你会怎么做?你想修改数据并传递这些字节或字符,而不是读取和写入临时文件。
你可以使用 io.BytesIO 处理二进制数据(bytes)和 io.StringIO 处理文本数据(str)。使用其中任何一个都可以将数据包装为类文件对象,适用于本章中介绍的所有文件函数。
这种情况的一个用例是数据格式转换。让我们将其应用于 PIL 库(详细信息将在“PIL 和 Pillow”中介绍),该库读取和写入图像数据。其 Image 对象的 open() 和 save() 方法的第一个参数是文件名或类文件对象。示例 14-1 中的代码使用 BytesIO 在内存中读取 并且 写入数据。它从命令行读取一个或多个图像文件,将其图像数据转换为三种不同的格式,并打印这些输出的长度和前 10 个字节。
示例 14-1. convert_image.py
from io import BytesIO
from PIL import Image
import sys
def data_to_img(data):
"""Return PIL Image object, with data from in-memory <data>"""
fp = BytesIO(data)
return Image.open(fp) # reads from memory
def img_to_data(img, fmt=None):
"""Return image data from PIL Image <img>, in <fmt> format"""
fp = BytesIO()
if not fmt:
fmt = img.format # keeps the original format
img.save(fp, fmt) # writes to memory
return fp.getvalue()
def convert_image(data, fmt=None):
"""Convert image <data> to PIL <fmt> image data"""
img = data_to_img(data)
return img_to_data(img, fmt)
def get_file_data(name):
"""Return PIL Image object for image file <name>"""
img = Image.open(name)
print("img", img, img.format)
return img_to_data(img)
if __name__ == "__main__":
for name in sys.argv[1:]:
data = get_file_data(name)
print("in", len(data), data[:10])
for fmt in ("gif", "png", "jpeg"):
out_data = convert_image(data, fmt)
print("out", len(out_data), out_data[:10])
注意
因为它的行为类似于文件,所以你可以像处理普通文件一样使用 seek()、read() 和 write() 方法处理 BytesIO 对象;如果你执行了 seek() 后跟着一个 read(),你将只获得从该 seek 位置到结尾的字节。getvalue() 返回 BytesIO 对象中的所有字节。
这是输出结果,使用了你将在第二十章中看到的输入图像文件:
$ python convert_image.py ch20_critter.png
img <PIL.PngImagePlugin.PngImageFile image mode=RGB size=154x141 at 0x10340CF28> PNG
in 24941 b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00'
out 14751 b'GIF87a\\x9a\\x00\\x8d\\x00'
out 24941 b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00'
out 5914 b'\\xff\xd8\\xff\\xe0\\x00\\x10JFIF'
即将到来
下一章内容稍微复杂一些。它涉及并发(即大约同时执行多个任务的方式)和进程(运行程序)。
要做的事情
14.1 列出当前目录中的文件。
14.2 列出父目录中的文件。
14.3 将字符串 'This is a test of the emergency text system' 赋值给变量 test1,并将 test1 写入名为 test.txt 的文件。
14.4 打开文件 test.txt 并将其内容读取到字符串 test2 中。test1 和 test2 是否相同?
¹ 在本书的第一份手稿中,我说的是广义相对论,被一位物理学家审阅者友善地纠正了。
² 为什么它从来都不是第一个?
³ 一种记忆方法是:斜杠向前倾斜,反斜杠向后倾斜。
⁴ 当 IBM 联系比尔·盖茨,询问他们的第一台个人电脑时,他以 $50,000 购买了操作系统 QDOS,以获得“MS-DOS”。它模仿了使用斜杠作为命令行参数的 CP/M。当 MS-DOS 后来添加了文件夹时,它不得不使用反斜杠。
第十五章:时间上的数据:进程与并发
计算机可以做的一件事情是被密封在纸板箱中并坐在仓库里,这是大多数人类做不到的。
Jack Handey
这一章和接下来的两章比之前的内容稍微有些挑战。在这一章中,我们涵盖了时间上的数据(在单台计算机上的顺序访问和并发访问),接着我们将在第十六章中讨论盒子中的数据(特殊文件和数据库的存储和检索),然后在第十七章中讨论空间中的数据(网络)。
程序与进程
当您运行一个单独的程序时,操作系统会创建一个单独的进程。它使用系统资源(CPU、内存、磁盘空间)和操作系统内核中的数据结构(文件和网络连接、使用统计等)。进程与其他进程隔离——它不能看到其他进程在做什么或者干扰它们。
操作系统会跟踪所有正在运行的进程,为每个进程分配一点运行时间,然后切换到另一个进程,以实现公平地分配工作和对用户响应迅速的双重目标。您可以通过图形界面(如 macOS 的活动监视器、Windows 计算机上的任务管理器或 Linux 中的top命令)查看进程的状态。
您还可以从自己的程序中访问进程数据。标准库的os模块提供了一种常见的访问某些系统信息的方式。例如,以下函数获取运行中 Python 解释器的进程 ID和当前工作目录:
>>> import os
>>> os.getpid()
76051
>>> os.getcwd()
'/Users/williamlubanovic'
这些获取用户 ID和组 ID:
>>> os.getuid()
501
>>> os.getgid()
20
使用子进程创建进程
到目前为止,你所见过的所有程序都是单独的进程。您可以使用标准库的subprocess模块从 Python 启动和停止其他已经存在的程序。如果只想在 shell 中运行另一个程序并获取其生成的所有输出(标准输出和标准错误输出),请使用getoutput()函数。在这里,我们获取 Unix 的date程序的输出:
>>> import subprocess
>>> ret = subprocess.getoutput('date')
>>> ret
'Sun Mar 30 22:54:37 CDT 2014'
在进程结束之前,您将得不到任何返回。如果需要调用可能需要很多时间的内容,请参阅“并发”中关于并发的讨论。由于getoutput()的参数是表示完整 shell 命令的字符串,因此可以包括参数、管道、< 和 > 的 I/O 重定向等:
>>> ret = subprocess.getoutput('date -u')
>>> ret
'Mon Mar 31 03:55:01 UTC 2014'
将该输出字符串管道传递给wc命令计数一行、六个“单词”和 29 个字符:
>>> ret = subprocess.getoutput('date -u | wc')
>>> ret
' 1 6 29'
变体方法称为check_output()接受命令和参数列表。默认情况下,它只返回标准输出作为字节类型而不是字符串,并且不使用 shell:
>>> ret = subprocess.check_output(['date', '-u'])
>>> ret
b'Mon Mar 31 04:01:50 UTC 2014\n'
要显示其他程序的退出状态,getstatusoutput()返回一个包含状态代码和输出的元组:
>>> ret = subprocess.getstatusoutput('date')
>>> ret
(0, 'Sat Jan 18 21:36:23 CST 2014')
如果您不想捕获输出但可能想知道其退出状态,请使用call():
>>> ret = subprocess.call('date')
Sat Jan 18 21:33:11 CST 2014
>>> ret
0
(在类 Unix 系统中,0 通常是成功的退出状态。)
那个日期和时间被打印到输出中,但没有在我们的程序中捕获。因此,我们将返回代码保存为 ret。
你可以以两种方式运行带参数的程序。第一种是在一个字符串中指定它们。我们的示例命令是 date -u,它会打印当前的日期和时间(协调世界时):
>>> ret = subprocess.call('date -u', shell=True)
Tue Jan 21 04:40:04 UTC 2014
你需要 shell=True 来识别命令行 date -u,将其拆分为单独的字符串,并可能扩展任何通配符字符,比如 *(在这个示例中我们没有使用任何通配符)。
第二种方法是将参数列表化,因此不需要调用 shell:
>>> ret = subprocess.call(['date', '-u'])
Tue Jan 21 04:41:59 UTC 2014
使用 multiprocessing 创建一个进程
你可以将一个 Python 函数作为一个独立的进程运行,甚至使用 multiprocessing 模块创建多个独立的进程。示例 15-1 中的示例代码简短而简单;将其保存为 mp.py,然后通过输入 python mp.py 运行它:
示例 15-1. mp.py
import multiprocessing
import os
def whoami(what):
print("Process %s says: %s" % (os.getpid(), what))
if __name__ == "__main__":
whoami("I'm the main program")
for n in range(4):
p = multiprocessing.Process(target=whoami,
args=("I'm function %s" % n,))
p.start()
当我运行这个时,我的输出看起来像这样:
Process 6224 says: I'm the main program
Process 6225 says: I'm function 0
Process 6226 says: I'm function 1
Process 6227 says: I'm function 2
Process 6228 says: I'm function 3
Process() 函数生成了一个新进程,并在其中运行 do_this() 函数。因为我们在一个有四次循环的循环中执行了这个操作,所以我们生成了四个执行 do_this() 然后退出的新进程。
multiprocessing 模块比一个喜剧团的小丑还要多。它真的是为那些需要将某些任务分配给多个进程以节省总体时间的时候而设计的;例如,下载网页进行爬取,调整图像大小等。它包括了排队任务、启用进程间通信以及等待所有进程完成的方法。“并发性” 探讨了其中的一些细节。
使用 terminate() 杀死一个进程
如果你创建了一个或多个进程,并且想出于某种原因终止其中一个(也许它陷入了循环,或者你感到无聊,或者你想成为一个邪恶的霸主),使用 terminate()。在 示例 15-2 中,我们的进程会计数到一百万,每一步都会睡眠一秒,并打印一个恼人的消息。然而,我们的主程序在五秒内失去耐心,然后将其从轨道上摧毁。
示例 15-2. mp2.py
import multiprocessing
import time
import os
def whoami(name):
print("I'm %s, in process %s" % (name, os.getpid()))
def loopy(name):
whoami(name)
start = 1
stop = 1000000
for num in range(start, stop):
print("\tNumber %s of %s. Honk!" % (num, stop))
time.sleep(1)
if __name__ == "__main__":
whoami("main")
p = multiprocessing.Process(target=loopy, args=("loopy",))
p.start()
time.sleep(5)
p.terminate()
当我运行这个程序时,我得到了以下输出:
I'm main, in process 97080
I'm loopy, in process 97081
Number 1 of 1000000\. Honk!
Number 2 of 1000000\. Honk!
Number 3 of 1000000\. Honk!
Number 4 of 1000000\. Honk!
Number 5 of 1000000\. Honk!
使用 os 获取系统信息
标准的 os 包提供了关于你的系统的许多详细信息,并且如果以特权用户(root 或管理员)身份运行你的 Python 脚本,还可以控制其中的一些内容。除了在 第十四章 中介绍的文件和目录函数外,它还有像这样的信息函数(在 iMac 上运行):
>>> import os
>>> os.uname()
posix.uname_result(sysname='Darwin',
nodename='iMac.local',
release='18.5.0',
version='Darwin Kernel Version 18.5.0: Mon Mar 11 20:40:32 PDT 2019;
root:xnu-4903.251.3~3/RELEASE_X86_64',
machine='x86_64')
>>> os.getloadavg()
(1.794921875, 1.93115234375, 2.2587890625)
>>> os.cpu_count()
4
一个有用的函数是 system(),它会执行一个命令字符串,就像你在终端上输入一样:
>>> import os
>>> os.system('date -u')
Tue Apr 30 13:10:09 UTC 2019
0
这是一个大杂烩。查看 文档 以获取有趣的小知识。
使用 psutil 获取进程信息
第三方包 psutil 还为 Linux、Unix、macOS 和 Windows 系统提供了系统和进程信息。
你可以猜测如何安装它:
$ pip install psutil
覆盖范围包括以下内容:
系统
CPU、内存、磁盘、网络、传感器
进程
ID、父 ID、CPU、内存、打开的文件、线程
我们已经在前面的os讨论中看到,我的计算机有四个 CPU。它们已经使用了多少时间(以秒为单位)?
>>> import psutil
>>> psutil.cpu_times(True)
[scputimes(user=62306.49, nice=0.0, system=19872.71, idle=256097.64),
scputimes(user=19928.3, nice=0.0, system=6934.29, idle=311407.28),
scputimes(user=57311.41, nice=0.0, system=15472.99, idle=265485.56),
scputimes(user=14399.49, nice=0.0, system=4848.84, idle=319017.87)]
它们现在有多忙?
>>> import psutil
>>> psutil.cpu_percent(True)
26.1
>>> psutil.cpu_percent(percpu=True)
[39.7, 16.2, 50.5, 6.0]
也许你永远不需要这种类型的数据,但知道在哪里查找是很好的。
命令自动化
你经常从 shell 中运行命令(要么手动输入命令,要么使用 shell 脚本),但 Python 有多个良好的第三方管理工具。
一个相关的主题,任务队列,在“队列”中讨论。
Invoke
fabric工具的第一个版本允许您使用 Python 代码定义本地和远程(网络)任务。开发人员将此原始包拆分为fabric2(远程)和invoke(本地)。
通过运行以下命令安装invoke:
$ pip install invoke
invoke的一个用途是将函数作为命令行参数提供。让我们创建一个tasks.py文件,其中包含示例 15-3 中显示的行。
示例 15-3. tasks.py
from invoke import task
@task
def mytime(ctx):
import time
now = time.time()
time_str = time.asctime(time.localtime(now))
print("Local time is", timestr)
(那个ctx参数是每个任务函数的第一个参数,但它仅在invoke内部使用。你可以随意命名它,但必须有一个参数在那里。)
$ invoke mytime
Local time is Thu May 2 13:16:23 2019
使用参数-l或--list来查看可用的任务:
$ invoke -l
Available tasks:
mytime
任务可以有参数,你可以从命令行同时调用多个任务(类似于 shell 脚本中的&&使用)。
其他用途包括:
-
使用
run()函数运行本地 shell 命令 -
响应程序的字符串输出模式
这只是一个简短的一瞥。详细信息请参阅文档。
其他命令助手
这些 Python 包在某种程度上类似于invoke,但在需要时可能有一个或多个更适合:
并发
官方 Python 网站总结了一般的并发概念以及标准库中的并发。这些页面包含许多链接到各种包和技术;在本章中,我们展示了最有用的一些链接。
在计算机中,如果你在等待什么东西,通常是有两个原因:
I/O 绑定
这是目前最常见的情况。计算机 CPU 速度非常快 - 比计算机内存快数百倍,比磁盘或网络快数千倍。
CPU 绑定
CPU 保持繁忙。这发生在像科学或图形计算这样的数字计算任务中。
另外两个与并发相关的术语是:
同步
事物紧随其后,就像一行幼鹅跟随它们的父母。
异步
任务是独立的,就像随机的鹅在池塘里溅水一样。
随着您从简单系统和任务逐渐过渡到现实生活中的问题,您在某个时候将需要处理并发性。以网站为例。您通常可以相当快地为 web 客户端提供静态和动态页面。一秒钟的时间被认为是交互式的,但如果显示或交互需要更长时间,人们会变得不耐烦。像 Google 和 Amazon 这样的公司进行的测试表明,如果页面加载速度稍慢,流量会迅速下降。
但是,如果有些事情花费很长时间,比如上传文件、调整图像大小或查询数据库,你又无能为力怎么办?你不能再在同步的 web 服务器代码中做了,因为有人在等待。
在单台计算机上,如果要尽可能快地执行多个任务,就需要使它们相互独立。慢任务不应该阻塞其他所有任务。
本章前面展示了如何利用多进程在单台计算机上重叠工作。如果您需要调整图像大小,您的 web 服务器代码可以调用一个单独的、专用的图像调整进程来异步和并发地运行。它可以通过调用多个调整大小的进程来扩展您的应用程序。
诀窍在于让它们彼此协同工作。任何共享的控制或状态意味着会有瓶颈。更大的诀窍是处理故障,因为并发计算比常规计算更难。许多事情可能会出错,你成功的几率会更低。
好的。什么方法可以帮助您应对这些复杂性?让我们从管理多个任务的好方法开始:队列。
队列
队列类似于列表:东西从一端添加,从另一端取走。最常见的是所谓的FIFO(先进先出)。
假设你正在洗盘子。如果你被困在整个工作中,你需要洗每个盘子,擦干它,并把它收起来。你可以用多种方式做到这一点。你可能先洗第一只盘子,然后擦干,然后把它收起来。然后你重复第二只盘子,依此类推。或者,您可以批量操作,洗所有的盘子,擦干它们,然后把它们收起来;这意味着您在水槽和沥干架上有足够的空间来存放每一步积累的所有盘子。这些都是同步方法——一个工人,一次做一件事。
作为替代方案,您可以找一个或两个帮手。如果您是洗碗工,您可以把每个洗净的盘子交给擦干工,擦干工再把每个擦干的盘子交给收拾工。只要每个人的工作速度一样,你们应该比一个人做快得多。
然而,如果你洗碗比烘干快怎么办?湿碟子要么掉在地上,要么堆在你和烘干机之间,或者你只是走音哼着歌等待烘干机准备好。如果最后一个人比烘干机慢,干燥好的碟子最终可能会掉在地上,或者堆在一起,或者烘干机开始哼歌。你有多个工人,但整体任务仍然是同步的,只能按照最慢的工人的速度进行。
众人拾柴火焰高,古语如是说(我一直以为这是阿米什人的,因为它让我想到了建造谷仓)。增加工人可以建造谷仓或者更快地洗碗。这涉及到队列。
通常,队列传输消息,可以是任何类型的信息。在这种情况下,我们对分布式任务管理的队列感兴趣,也称为工作队列、作业队列或任务队列。水池中的每个碟子都交给一个可用的洗碗机,洗碗机洗完后交给第一个可用的烘干机,烘干机烘干后交给一个放置者。这可以是同步的(工人等待处理一个碟子和另一个工人来接收它),也可以是异步的(碟子在不同速度的工人之间堆积)。只要你有足够的工人,并且他们跟得上碟子的速度,事情就会快得多。
进程
你可以用许多方法实现队列。对于单台机器,标准库的 multiprocessing 模块(前面你见过)包含一个 Queue 函数。让我们模拟只有一个洗碗机和多个烘干进程(稍后有人会把碟子放好),以及一个中间的 dish_queue。将这个程序称为 dishes.py(Example 15-4)。
Example 15-4. dishes.py
import multiprocessing as mp
def washer(dishes, output):
for dish in dishes:
print('Washing', dish, 'dish')
output.put(dish)
def dryer(input):
while True:
dish = input.get()
print('Drying', dish, 'dish')
input.task_done()
dish_queue = mp.JoinableQueue()
dryer_proc = mp.Process(target=dryer, args=(dish_queue,))
dryer_proc.daemon = True
dryer_proc.start()
dishes = ['salad', 'bread', 'entree', 'dessert']
washer(dishes, dish_queue)
dish_queue.join()
运行你的新程序,像这样:
$ python dishes.py
Washing salad dish
Washing bread dish
Washing entree dish
Washing dessert dish
Drying salad dish
Drying bread dish
Drying entree dish
Drying dessert dish
这个队列看起来很像一个简单的 Python 迭代器,产生一系列的碟子。实际上,它启动了独立的进程以及洗碗机和烘干机之间的通信。我使用了 JoinableQueue 和最终的 join() 方法来告诉洗碗机所有的碟子已经干燥好了。在 multiprocessing 模块中还有其他的队列类型,你可以阅读 文档 获取更多例子。
线程
线程在一个进程中运行,并可以访问进程中的所有内容,类似于多重人格。multiprocessing 模块有一个名为 threading 的表兄弟,它使用线程而不是进程(实际上,multiprocessing 是它基于进程的对应物)。让我们用线程重新做我们的进程示例,如 Example 15-5 所示。
Example 15-5. thread1.py
import threading
def do_this(what):
whoami(what)
def whoami(what):
print("Thread %s says: %s" % (threading.current_thread(), what))
if __name__ == "__main__":
whoami("I'm the main program")
for n in range(4):
p = threading.Thread(target=do_this,
args=("I'm function %s" % n,))
p.start()
这是我的打印输出:
Thread <_MainThread(MainThread, started 140735207346960)> says: I'm the main
program
Thread <Thread(Thread-1, started 4326629376)> says: I'm function 0
Thread <Thread(Thread-2, started 4342157312)> says: I'm function 1
Thread <Thread(Thread-3, started 4347412480)> says: I'm function 2
Thread <Thread(Thread-4, started 4342157312)> says: I'm function 3
我们可以通过线程重新复制我们基于进程的洗碟子示例,如 Example 15-6 所示。
Example 15-6. thread_dishes.py
import threading, queue
import time
def washer(dishes, dish_queue):
for dish in dishes:
print ("Washing", dish)
time.sleep(5)
dish_queue.put(dish)
def dryer(dish_queue):
while True:
dish = dish_queue.get()
print ("Drying", dish)
time.sleep(10)
dish_queue.task_done()
dish_queue = queue.Queue()
for n in range(2):
dryer_thread = threading.Thread(target=dryer, args=(dish_queue,))
dryer_thread.start()
dishes = ['salad', 'bread', 'entree', 'dessert']
washer(dishes, dish_queue)
dish_queue.join()
multiprocessing和threading之间的一个区别是,threading没有terminate()函数。没有简单的方法来终止运行中的线程,因为它可能会在您的代码中引发各种问题,甚至可能影响时空连续体本身。
线程可能是危险的。就像 C 和 C++等语言中的手动内存管理一样,它们可能会导致极难发现,更不用说修复的错误。要使用线程,程序中的所有代码(以及它使用的外部库中的代码)都必须是线程安全的。在前面的示例代码中,线程没有共享任何全局变量,因此它们可以独立运行而不会出错。
假设你是一个在闹鬼的房子里进行超自然调查的调查员。鬼魂在走廊里游荡,但彼此并不知道对方的存在,随时都可以查看、添加、删除或移动房子里的任何物品。
你戒备地穿过房子,用你那令人印象深刻的仪器进行测量。突然间,你注意到你刚刚走过的烛台不见了。
房子里的内容就像程序中的变量一样。鬼魂是进程(房子)中的线程。如果鬼魂只是偶尔瞥一眼房子的内容,那就没有问题。就像一个线程读取常量或变量的值而不试图改变它一样。
然而,某些看不见的实体可能会拿走你的手电筒,往你的脖子上吹冷风,把弹珠放在楼梯上,或点燃壁炉。真正微妙的鬼魂会改变你可能永远不会注意到的其他房间里的东西。
尽管你有花哨的仪器,但你要弄清楚谁做了什么,怎么做的,什么时候做的,以及在哪里做的,是非常困难的。
如果您使用多个进程而不是线程,那就像每个房子只有一个(活着的)人一样。如果您把白兰地放在壁炉前,一个小时后它仍会在那里——有些会因蒸发而丢失,但位置不变。
当不涉及全局数据时,线程可能是有用且安全的。特别是,在等待某些 I/O 操作完成时,线程可节省时间。在这些情况下,它们不必争夺数据,因为每个线程都有完全独立的变量。
但线程有时确实有充分理由更改全局数据。事实上,启动多个线程的一个常见原因是让它们分配某些数据的工作,因此预期对数据进行一定程度的更改。
安全共享数据的通常方法是在修改线程中的变量之前应用软件锁定。这样在进行更改时可以阻止其他线程进入。这就像让一个捉鬼者守卫你想保持清静的房间一样。不过,诀窍在于你需要记得解锁它。而且,锁定可以嵌套:如果另一个捉鬼者也在监视同一个房间,或者是房子本身呢?锁的使用是传统的但难以做到完全正确。
注:
在 Python 中,由于标准 Python 系统中的一个实现细节,线程不会加速 CPU 密集型任务,这称为全局解释器锁(GIL)。这存在是为了避免 Python 解释器中的线程问题,但实际上可能使多线程程序比其单线程版本或甚至多进程版本更慢。
因此,对于 Python,建议如下:
-
对于 I/O 密集型问题,请使用线程
-
对于 CPU 密集型问题,请使用进程、网络或事件(在下一节中讨论)
concurrent.futures
正如您刚刚看到的,使用线程或多进程涉及许多细节。concurrent.futures 模块已添加到 Python 3.2 标准库中,以简化这些操作。它允许您调度异步工人池,使用线程(当 I/O 密集型时)或进程(当 CPU 密集型时)。您将得到一个 future 来跟踪它们的状态并收集结果。
示例 15-7 包含一个测试程序,您可以将其保存为 cf.py。任务函数 calc() 睡眠一秒钟(我们模拟忙于某事),计算其参数的平方根,并返回它。程序可以接受一个可选的命令行参数,表示要使用的工人数,默认为 3。它在线程池中启动此数量的工人,然后在进程池中启动,然后打印经过的时间。values 列表包含五个数字,逐个发送给 calc() 在工人线程或进程中。
示例 15-7. cf.py
from concurrent import futures
import math
import time
import sys
def calc(val):
time.sleep(1)
result = math.sqrt(float(val))
return result
def use_threads(num, values):
t1 = time.time()
with futures.ThreadPoolExecutor(num) as tex:
results = tex.map(calc, values)
t2 = time.time()
return t2 - t1
def use_processes(num, values):
t1 = time.time()
with futures.ProcessPoolExecutor(num) as pex:
results = pex.map(calc, values)
t2 = time.time()
return t2 - t1
def main(workers, values):
print(f"Using {workers} workers for {len(values)} values")
t_sec = use_threads(workers, values)
print(f"Threads took {t_sec:.4f} seconds")
p_sec = use_processes(workers, values)
print(f"Processes took {p_sec:.4f} seconds")
if __name__ == '__main__':
workers = int(sys.argv[1])
values = list(range(1, 6)) # 1 .. 5
main(workers, values)
这里是我得到的一些结果:
$ python cf.py 1
Using 1 workers for 5 values
Threads took 5.0736 seconds
Processes took 5.5395 seconds
$ python cf.py 3
Using 3 workers for 5 values
Threads took 2.0040 seconds
Processes took 2.0351 seconds
$ python cf.py 5
Using 5 workers for 5 values
Threads took 1.0052 seconds
Processes took 1.0444 seconds
那一秒钟的 sleep() 强制每个工人对每个计算都花费一秒钟:
-
只有一个工人同时工作,一切都是串行的,总时间超过五秒。
-
五个工人与被测试值的大小匹配,所以经过的时间略多于一秒。
-
使用三个工人,我们需要两次运行来处理所有五个值,所以经过了两秒。
在程序中,我忽略了实际的 results(我们计算的平方根),以突出显示经过的时间。此外,使用 map() 来定义池会导致我们在返回 results 之前等待所有工人完成。如果您希望在每次完成时获取每个结果,让我们尝试另一个测试(称为 cf2.py),在该测试中,每个工人在计算完值及其平方根后立即返回该值(示例 15-8)。
示例 15-8. cf2.py
from concurrent import futures
import math
import sys
def calc(val):
result = math.sqrt(float(val))
return val, result
def use_threads(num, values):
with futures.ThreadPoolExecutor(num) as tex:
tasks = [tex.submit(calc, value) for value in values]
for f in futures.as_completed(tasks):
yield f.result()
def use_processes(num, values):
with futures.ProcessPoolExecutor(num) as pex:
tasks = [pex.submit(calc, value) for value in values]
for f in futures.as_completed(tasks):
yield f.result()
def main(workers, values):
print(f"Using {workers} workers for {len(values)} values")
print("Using threads:")
for val, result in use_threads(workers, values):
print(f'{val} {result:.4f}')
print("Using processes:")
for val, result in use_processes(workers, values):
print(f'{val} {result:.4f}')
if __name__ == '__main__':
workers = 3
if len(sys.argv) > 1:
workers = int(sys.argv[1])
values = list(range(1, 6)) # 1 .. 5
main(workers, values)
我们的 use_threads() 和 use_processes() 函数现在是生成器函数,每次迭代调用 yield 返回。在我的机器上运行一次,您可以看到工人不总是按顺序完成 1 到 5:
$ python cf2.py 5
Using 5 workers for 5 values
Using threads:
3 1.7321
1 1.0000
2 1.4142
4 2.0000
5 2.2361
Using processes:
1 1.0000
2 1.4142
3 1.7321
4 2.0000
5 2.2361
您可以在任何时候使用 concurrent.futures 启动一堆并发任务,例如以下内容:
-
爬取网页上的 URL
-
处理文件,如调整图像大小
-
调用服务 API
如往常一样,文档 提供了额外的详细信息,但更加技术性。
绿色线程和 gevent
正如你所见,开发者传统上通过将程序中的慢点运行在单独的线程或进程中来避免慢点。Apache 网络服务器就是这种设计的一个例子。
一个替代方案是基于事件的编程。一个基于事件的程序运行一个中央事件循环,分发任何任务,并重复该循环。NGINX 网络服务器遵循这种设计,并且通常比 Apache 更快。
gevent库是基于事件的,并完成了一个巧妙的技巧:你编写普通的命令式代码,它会神奇地将部分代码转换为协程。这些协程类似于可以相互通信并跟踪其位置的生成器。gevent修改了 Python 许多标准对象如socket,以使用其机制而不是阻塞。这不能与 Python 中用 C 编写的插件代码一起工作,比如一些数据库驱动。
你可以使用pip安装gevent:
$ pip install gevent
这里是在gevent网站的示例代码的变体。在即将到来的 DNS 部分中,你会看到socket模块的gethostbyname()函数。这个函数是同步的,所以你要等待(可能很多秒),而它在世界各地的名称服务器中查找地址。但你可以使用gevent版本来独立查找多个站点。将其保存为gevent_test.py(示例 15-9)。
示例 15-9. gevent_test.py
import gevent
from gevent import socket
hosts = ['www.crappytaxidermy.com', 'www.walterpottertaxidermy.com',
'www.antique-taxidermy.com']
jobs = [gevent.spawn(gevent.socket.gethostbyname, host) for host in hosts]
gevent.joinall(jobs, timeout=5)
for job in jobs:
print(job.value)
在前面的示例中有一个单行的 for 循环。每个主机名依次提交给gethostbyname()调用,但它们可以异步运行,因为这是gevent版本的gethostbyname()。
运行gevent_test.py:
$ python gevent_test.py
66.6.44.4
74.125.142.121
78.136.12.50
gevent.spawn()创建一个greenlet(有时也称为绿色线程或微线程)来执行每个gevent.socket.gethostbyname(url)。
与普通线程的区别在于它不会阻塞。如果发生了本应该阻塞普通线程的事件,gevent会切换控制到其他的 greenlet。
gevent.joinall()方法等待所有生成的作业完成。最后,我们会输出这些主机名对应的 IP 地址。
你可以使用它的富有表现力的名为monkey-patching的函数,而不是gevent版本的socket。这些函数修改标准模块如socket,以使用绿色线程而不是调用模块的gevent版本。当你希望gevent被应用到所有代码,甚至是无法访问的代码时,这是非常有用的。
在你的程序顶部添加以下调用:
from gevent import monkey
monkey.patch_socket()
这里将gevent套接字插入到任何地方普通的socket被调用的地方,即使是在你的程序中的标准库中。再次强调,这仅适用于 Python 代码,而不适用于用 C 编写的库。
另一个函数 monkey-patches 更多的标准库模块:
from gevent import monkey
monkey.patch_all()
在你的程序顶部使用这个来获取尽可能多的gevent加速。
将此程序保存为gevent_monkey.py(示例 15-9)。
示例 15-10. gevent_monkey.py
import gevent
from gevent import monkey; monkey.patch_all()
import socket
hosts = ['www.crappytaxidermy.com', 'www.walterpottertaxidermy.com',
'www.antique-taxidermy.com']
jobs = [gevent.spawn(socket.gethostbyname, host) for host in hosts]
gevent.joinall(jobs, timeout=5)
for job in jobs:
print(job.value)
再次运行程序:
$ python gevent_monkey.py
66.6.44.4
74.125.192.121
78.136.12.50
使用gevent存在潜在风险。与任何基于事件的系统一样,每段执行的代码应该相对迅速。虽然它是非阻塞的,但执行大量工作的代码仍然慢。
monkey-patching 的概念使一些人感到不安。然而,像 Pinterest 这样的大型网站使用gevent显著加速他们的网站。就像药瓶上的小字一样,请按照指示使用gevent。
欲知更多示例,请参阅这个详尽的gevent教程。
注意
你可能也考虑使用tornado或者gunicorn,这两个流行的事件驱动框架提供了低级事件处理和快速的 Web 服务器。如果你想构建一个快速的网站而不想麻烦传统的 Web 服务器如 Apache,它们值得一试。
twisted
twisted是一个异步的、事件驱动的网络框架。你可以将函数连接到诸如数据接收或连接关闭等事件,当这些事件发生时,这些函数就会被调用。这是一种回调设计,如果你之前写过 JavaScript 代码,这种方式可能很熟悉。如果你还不熟悉,它可能看起来有些反直觉。对于一些开发者来说,基于回调的代码在应用程序增长时变得更难管理。
通过以下命令安装它:
$ pip install twisted
twisted是一个庞大的包,支持多种基于 TCP 和 UDP 的互联网协议。简单说,我们展示了一个从twisted 示例改编的小小的“敲门”服务器和客户端。首先,让我们看看服务器,knock_server.py:(示例 15-11)。
示例 15-11. knock_server.py
from twisted.internet import protocol, reactor
class Knock(protocol.Protocol):
def dataReceived(self, data):
print('Client:', data)
if data.startswith("Knock knock"):
response = "Who's there?"
else:
response = data + " who?"
print('Server:', response)
self.transport.write(response)
class KnockFactory(protocol.Factory):
def buildProtocol(self, addr):
return Knock()
reactor.listenTCP(8000, KnockFactory())
reactor.run()
现在让我们快速浏览它的可靠伴侣,knock_client.py(示例 15-12)。
示例 15-12. knock_client.py
from twisted.internet import reactor, protocol
class KnockClient(protocol.Protocol):
def connectionMade(self):
self.transport.write("Knock knock")
def dataReceived(self, data):
if data.startswith("Who's there?"):
response = "Disappearing client"
self.transport.write(response)
else:
self.transport.loseConnection()
reactor.stop()
class KnockFactory(protocol.ClientFactory):
protocol = KnockClient
def main():
f = KnockFactory()
reactor.connectTCP("localhost", 8000, f)
reactor.run()
if __name__ == '__main__':
main()
首先启动服务器:
$ python knock_server.py
然后,启动客户端:
$ python knock_client.py
服务器和客户端交换消息,服务器打印对话:
Client: Knock knock
Server: Who's there?
Client: Disappearing client
Server: Disappearing client who?
我们的恶作剧客户端然后结束,让服务器等待笑话的结尾。
如果你想进入twisted的世界,请尝试一些它文档中的其他示例。
asyncio
Python 在 3.4 版本中加入了asyncio库。它是使用新的async和await功能定义并发代码的一种方式。这是一个涉及许多细节的大课题。为了避免在本章节中过多涉及,我已将有关asyncio和相关主题的讨论移至附录 C。
Redis
我们之前关于洗碗的代码示例,使用进程或线程,在单台机器上运行。让我们再来看一种可以在单台机器或跨网络运行的队列方法。即使有多个歌唱进程和跳舞线程,有时一台机器还不够,你可以把这一节当作单台(一台机器)和多台并发之间的桥梁。
要尝试本节中的示例,您需要一个 Redis 服务器及其 Python 模块。您可以在“Redis”中找到获取它们的位置。在那一章中,Redis 的角色是数据库。在这里,我们展示它的并发性格。
使用 Redis 列表是快速创建队列的方法。Redis 服务器运行在一台机器上;这可以是与其客户端相同的机器,或者是客户端通过网络访问的另一台机器。无论哪种情况,客户端通过 TCP 与服务器通信,因此它们是网络化的。一个或多个提供者客户端将消息推送到列表的一端。一个或多个客户端工作进程使用阻塞弹出操作监视此列表。如果列表为空,它们就会坐在那里打牌。一旦有消息到达,第一个渴望的工作进程就会获取到它。
像我们早期基于进程和线程的示例一样,redis_washer.py生成一系列的菜品(示例 15-13)。
示例 15-13. redis_washer.py
import redis
conn = redis.Redis()
print('Washer is starting')
dishes = ['salad', 'bread', 'entree', 'dessert']
for dish in dishes:
msg = dish.encode('utf-8')
conn.rpush('dishes', msg)
print('Washed', dish)
conn.rpush('dishes', 'quit')
print('Washer is done')
循环生成四条包含菜品名称的消息,然后是一条说“quit”的最终消息。它将每条消息追加到 Redis 服务器中的名为dishes的列表中,类似于追加到 Python 列表中。
一旦第一道菜准备好,redis_dryer.py就开始工作(示例 15-14)。
示例 15-14. redis_dryer.py
import redis
conn = redis.Redis()
print('Dryer is starting')
while True:
msg = conn.blpop('dishes')
if not msg:
break
val = msg[1].decode('utf-8')
if val == 'quit':
break
print('Dried', val)
print('Dishes are dried')
该代码等待第一个令牌为“dishes”的消息,并打印每一个干燥。它通过结束循环遵循quit消息。
先启动烘干机,然后启动洗碗机。在命令末尾使用&将第一个程序置于后台;它会继续运行,但不再接受键盘输入。这适用于 Linux、macOS 和 Windows,尽管您可能在下一行看到不同的输出。在这种情况下(macOS),它是关于后台烘干机进程的一些信息。然后,我们正常启动洗碗机进程(前台)。您将看到两个进程的混合输出:
$ python redis_dryer.py &
[2] 81691
Dryer is starting
$ python redis_washer.py
Washer is starting
Washed salad
Dried salad
Washed bread
Dried bread
Washed entree
Dried entree
Washed dessert
Washer is done
Dried dessert
Dishes are dried
[2]+ Done python redis_dryer.py
一旦从洗碗机进程到达 Redis 的菜品 ID 开始,我们勤劳的烘干机进程就开始将它们取回。每个菜品 ID 都是一个数字,除了最后的sentinel值,即字符串'quit'。当烘干机进程读取到quit菜品 ID 时,它就会退出,并且一些更多的后台进程信息会打印到终端(同样依赖系统)。您可以使用一个标志(一个否则无效的值)来指示数据流本身的某些特殊情况,例如,我们已经完成了。否则,我们需要添加更多的程序逻辑,比如以下内容:
-
预先同意一些最大的菜品编号,这实际上会成为一个标志。
-
进行一些特殊的out-of-band(不在数据流中的)进程间通信。
-
在一段时间内没有新数据时,设置超时。
让我们做一些最后的更改:
-
创建多个
dryer进程。 -
给每个烘干机添加超时,而不是寻找一个标志。
新的redis_dryer2.py显示在示例 15-15 中。
示例 15-15. redis_dryer2.py
def dryer():
import redis
import os
import time
conn = redis.Redis()
pid = os.getpid()
timeout = 20
print('Dryer process %s is starting' % pid)
while True:
msg = conn.blpop('dishes', timeout)
if not msg:
break
val = msg[1].decode('utf-8')
if val == 'quit':
break
print('%s: dried %s' % (pid, val))
time.sleep(0.1)
print('Dryer process %s is done' % pid)
import multiprocessing
DRYERS=3
for num in range(DRYERS):
p = multiprocessing.Process(target=dryer)
p.start()
在后台启动烘干进程,然后在前台启动洗碗机进程:
$ python redis_dryer2.py &
Dryer process 44447 is starting
Dryer process 44448 is starting
Dryer process 44446 is starting
$ python redis_washer.py
Washer is starting
Washed salad
44447: dried salad
Washed bread
44448: dried bread
Washed entree
44446: dried entree
Washed dessert
Washer is done
44447: dried dessert
一个更干燥的过程读取quit ID 并退出:
Dryer process 44448 is done
20 秒后,其他烘干程序从它们的blpop调用中获取到None的返回值,表示它们已超时。它们说出它们的最后一句话并退出:
Dryer process 44447 is done
Dryer process 44446 is done
在最后一个烘干子进程退出后,主要的烘干程序就结束了:
[1]+ Done python redis_dryer2.py
超越队列
随着更多的零部件移动,我们可爱的装配线被打断的可能性也更多。如果我们需要洗一顿宴会的盘子,我们有足够的工人吗?如果烘干机喝醉了怎么办?如果水槽堵塞了怎么办?担心,担心!
如何应对这一切?常见的技术包括以下几种:
火而忘
只需传递事物,不要担心后果,即使没有人在那里。这就是盘子掉在地上的方法。
请求-响应
洗碗机收到烘干机的确认,烘干机收到收拾碗盘者的确认,每个管道中的盘子都会这样。
回压或节流
如果下游的某个人跟不上,这种技术会让快速工人放松点。
在实际系统中,您需要确保工人们能够跟上需求;否则,您会听到盘子掉在地上的声音。您可以将新任务添加到待处理列表中,而某些工作进程则会弹出最新消息并将其添加到正在处理列表中。当消息完成时,它将从正在处理列表中删除,并添加到已完成列表中。这让您知道哪些任务失败或花费太长时间。您可以自己使用 Redis 来完成这一过程,或者使用已经编写和测试过的系统。一些基于 Python 的队列包可以增加这种额外的管理水平,包括:
队列讨论了队列软件,基于 Python 和其他语言。
即将到来
在本章中,我们将数据流经过程。在下一章中,您将看到如何在各种文件格式和数据库中存储和检索数据。
要做的事情
15.1 使用multiprocessing创建三个单独的进程。使每个进程在零到一秒之间等待一个随机数,打印当前时间,然后退出。
第十六章:盒子中的数据:持久存储
在获得数据之前进行理论化是一个重大的错误。
亚瑟·柯南·道尔
活跃的程序访问存储在随机存取存储器(RAM)中的数据。RAM 非常快速,但价格昂贵,并且需要持续的电源供应;如果电源中断,内存中的所有数据都将丢失。磁盘驱动器比 RAM 慢,但容量更大,成本更低,并且即使有人绊倒电源线后,也可以保留数据。因此,计算机系统中的大量工作已经致力于在磁盘和 RAM 之间进行最佳权衡。作为程序员,我们需要持久性:使用非易失性介质(如磁盘)存储和检索数据。
本章讨论了为不同目的优化的数据存储的不同类型:平面文件、结构化文件和数据库。除了输入和输出之外的文件操作在第十四章中有所涵盖。
记录是指一块相关数据的术语,由各个字段组成。
平面文本文件
最简单的持久性是普通的平面文件。如果您的数据结构非常简单并且在磁盘和内存之间交换所有数据,则此方法非常有效。纯文本数据可能适合这种处理方式。
填充文本文件
在这种格式中,记录中的每个字段都有固定的宽度,并且在文件中(通常用空格字符)填充到该宽度,使得每行(记录)具有相同的宽度。程序员可以使用seek()来在文件中跳转,并且仅读取和写入需要的记录和字段。
表格式文本文件
对于简单的文本文件,唯一的组织级别是行。有时,您可能需要比这更多的结构。您可能希望将数据保存供程序稍后使用,或者将数据发送到另一个程序。
有许多格式,这里是如何区分它们的方式:
-
分隔符或分隔符字符,如制表符(
'\t')、逗号(',')或竖线('|')。这是逗号分隔值(CSV)格式的一个例子。 -
'<'和'>'围绕标签。例如 XML 和 HTML。 -
标点符号。一个例子是 JavaScript 对象表示法(JSON)。
-
缩进。一个例子是 YAML(递归定义为“YAML 不是标记语言”)。
-
杂项,例如程序的配置文件。
这些结构化文件格式中的每一个都可以由至少一个 Python 模块读取和写入。
CSV
分隔文件通常用作电子表格和数据库的交换格式。您可以手动读取 CSV 文件,一次读取一行,在逗号分隔符处拆分每行成字段,并将结果添加到诸如列表和字典之类的数据结构中。但最好使用标准的csv模块,因为解析这些文件可能比您想象的要复杂得多。在处理 CSV 时需要记住以下几个重要特征:
-
有些分隔符除了逗号以外还有:
'|'和'\t'(制表符)是常见的。 -
有些有转义序列。如果定界符字符可以出现在字段内,则整个字段可能用引号字符括起来或者在某些转义字符之前。
-
文件具有不同的行尾字符。Unix 使用
'\n',Microsoft 使用'\r\n',而 Apple 曾使用'\r',但现在使用'\n'。 -
第一行可能包含列名。
首先,我们看看如何读取和写入包含列列表的行列表:
>>> import csv
>>> villains = [
... ['Doctor', 'No'],
... ['Rosa', 'Klebb'],
... ['Mister', 'Big'],
... ['Auric', 'Goldfinger'],
... ['Ernst', 'Blofeld'],
... ]
>>> with open('villains', 'wt') as fout: # a context manager
... csvout = csv.writer(fout)
... csvout.writerows(villains)
这将创建包含这些行的文件villains:
Doctor,No
Rosa,Klebb
Mister,Big
Auric,Goldfinger
Ernst,Blofeld
现在,我们试着再次读取它:
>>> import csv
>>> with open('villains', 'rt') as fin: # context manager
... cin = csv.reader(fin)
... villains = [row for row in cin] # a list comprehension
...
>>> print(villains)
[['Doctor', 'No'], ['Rosa', 'Klebb'], ['Mister', 'Big'],
['Auric', 'Goldfinger'], ['Ernst', 'Blofeld']]
我们利用了reader()函数创建的结构。它在cin对象中创建了可以在for循环中提取的行。
使用reader()和writer()及其默认选项,列由逗号分隔,行由换行符分隔。
数据可以是字典列表而不是列表列表。让我们再次使用新的DictReader()函数和指定的列名读取villains文件:
>>> import csv
>>> with open('villains', 'rt') as fin:
... cin = csv.DictReader(fin, fieldnames=['first', 'last'])
... villains = [row for row in cin]
...
>>> print(villains)
[OrderedDict([('first', 'Doctor'), ('last', 'No')]),
OrderedDict([('first', 'Rosa'), ('last', 'Klebb')]),
OrderedDict([('first', 'Mister'), ('last', 'Big')]),
OrderedDict([('first', 'Auric'), ('last', 'Goldfinger')]),
OrderedDict([('first', 'Ernst'), ('last', 'Blofeld')])]
那个 OrderedDict 用于兼容 Python 3.6 之前的版本,当时字典默认保持其顺序。
让我们使用新的 DictWriter() 函数重新编写 CSV 文件。我们还调用 writeheader() 来向 CSV 文件写入初始列名行:
import csv
villains = [
{'first': 'Doctor', 'last': 'No'},
{'first': 'Rosa', 'last': 'Klebb'},
{'first': 'Mister', 'last': 'Big'},
{'first': 'Auric', 'last': 'Goldfinger'},
{'first': 'Ernst', 'last': 'Blofeld'},
]
with open('villains.txt', 'wt') as fout:
cout = csv.DictWriter(fout, ['first', 'last'])
cout.writeheader()
cout.writerows(villains)
这创建了一个带有头行的villains.csv文件(示例 16-1)。
示例 16-1. villains.csv
first,last
Doctor,No
Rosa,Klebb
Mister,Big
Auric,Goldfinger
Ernst,Blofeld
现在,让我们读取它。在DictReader()调用中省略fieldnames参数,告诉它使用文件的第一行的值(first,last)作为列标签和匹配的字典键:
>>> import csv
>>> with open('villains.csv', 'rt') as fin:
... cin = csv.DictReader(fin)
... villains = [row for row in cin]
...
>>> print(villains)
[OrderedDict([('first', 'Doctor'), ('last', 'No')]),
OrderedDict([('first', 'Rosa'), ('last', 'Klebb')]),
OrderedDict([('first', 'Mister'), ('last', 'Big')]),
OrderedDict([('first', 'Auric'), ('last', 'Goldfinger')]),
OrderedDict([('first', 'Ernst'), ('last', 'Blofeld')])]
XML
分隔文件仅传达两个维度:行(行)和列(行内字段)。如果要在程序之间交换数据结构,需要一种方法将层次结构、序列、集合和其他结构编码为文本。
XML 是一种突出显示的标记格式,它使用标签来界定数据,就像这个样本menu.xml文件中所示。
<?xml version="1.0"?>
<menu>
<breakfast hours="7-11">
<item price="$6.00">breakfast burritos</item>
<item price="$4.00">pancakes</item>
</breakfast>
<lunch hours="11-3">
<item price="$5.00">hamburger</item>
</lunch>
<dinner hours="3-10">
<item price="8.00">spaghetti</item>
</dinner>
</menu>
以下是 XML 的一些重要特征:
-
标签以
<字符开始。这个示例中的标签是menu、breakfast、lunch、dinner和item。 -
空白符将被忽略。
-
通常像
<menu>这样的开始标签后面跟着其他内容,然后是最终匹配的结束标签,例如</menu>。 -
标签可以在其他标签内嵌套到任何级别。在此示例中,
item标签是breakfast、lunch和dinner标签的子标签;它们反过来是menu的子标签。 -
可选的属性可以出现在开始标签内。在这个例子中,
price是item的一个属性。 -
标签可以包含值。在这个例子中,每个
item都有一个值,比如第二个早餐项目的pancakes。 -
如果名为
thing的标签没有值或子项,则可以通过在闭合尖括号之前包含一个斜杠来表示为单个标签,例如<thing/>,而不是开始和结束标签,像<thing></thing>。 -
将数据放在哪里的选择——属性、值、子标签——有些是任意的。例如,我们可以将最后一个
item标签写成<item price="$8.00" food="spaghetti"/>。
XML 通常用于数据 feeds 和 messages,并具有诸如 RSS 和 Atom 之类的子格式。一些行业有许多专门的 XML 格式,比如 金融领域。
XML 的超级灵活性启发了多个 Python 库,这些库在方法和功能上有所不同。
在 Python 中解析 XML 的最简单方法是使用标准的 ElementTree 模块。下面是一个简单的程序,用于解析 menu.xml 文件并打印一些标签和属性:
>>> import xml.etree.ElementTree as et
>>> tree = et.ElementTree(file='menu.xml')
>>> root = tree.getroot()
>>> root.tag
'menu'
>>> for child in root:
... print('tag:', child.tag, 'attributes:', child.attrib)
... for grandchild in child:
... print('\ttag:', grandchild.tag, 'attributes:', grandchild.attrib)
...
tag: breakfast attributes: {'hours': '7-11'}
tag: item attributes: {'price': '$6.00'}
tag: item attributes: {'price': '$4.00'}
tag: lunch attributes: {'hours': '11-3'}
tag: item attributes: {'price': '$5.00'}
tag: dinner attributes: {'hours': '3-10'}
tag: item attributes: {'price': '8.00'}
>>> len(root) # number of menu sections
3
>>> len(root[0]) # number of breakfast items
2
对于嵌套列表中的每个元素,tag 是标签字符串,attrib 是其属性的字典。ElementTree 有很多其他搜索 XML 派生数据,修改它的方法,甚至编写 XML 文件的方法。ElementTree 文档 上有详细信息。
其他标准的 Python XML 库包括以下内容:
xml.dom
文件对象模型(DOM),对于 JavaScript 开发者来说很熟悉,它将 web 文档表示为分层结构。这个模块将整个 XML 文件加载到内存中,并允许您平等地访问所有部分。
xml.sax
简单的 XML API,或 SAX,可以实时解析 XML,因此它不必一次加载所有内容到内存中。因此,如果您需要处理非常大的 XML 流,它可能是一个不错的选择。
XML 安全注意事项
您可以使用本章中描述的所有格式将对象保存到文件中,然后再读取它们。可以利用这个过程来造成安全问题。
例如,下面来自十亿笑话维基百科页面的 XML 片段定义了 10 个嵌套实体,每个实体扩展了低一级 10 次,总扩展量达到了十亿:
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
坏消息:十亿笑话将会使前面提到的所有 XML 库爆炸。Defused XML 列出了这个攻击和其他攻击,以及 Python 库的漏洞。
该链接展示了如何更改许多库的设置以避免这些问题。此外,您可以使用 defusedxml 库作为其他库的安全前端:
>>> # insecure:
>>> from xml.etree.ElementTree import parse
>>> et = parse(xmlfile)
>>> # protected:
>>> from defusedxml.ElementTree import parse
>>> et = parse(xmlfile)
标准的 Python 网站上也有关于 XML 漏洞 的页面。
HTML
庞大的数据以超文本标记语言(HTML)的形式保存,这是 web 的基本文档格式。问题是其中的大部分都不符合 HTML 规则,这可能会使解析变得困难。HTML 比数据交换格式更好地显示格式。因为本章旨在描述相当明确定义的数据格式,我已将有关 HTML 的讨论分离到 第十八章 中。
JSON
JavaScript Object Notation (JSON) 已成为非常流行的数据交换格式,超越了其 JavaScript 的起源。JSON 格式是 JavaScript 的子集,通常也是合法的 Python 语法。它与 Python 的密切配合使其成为程序之间数据交换的良好选择。您将在 第十八章 中看到许多用于 Web 开发的 JSON 示例。
与各种 XML 模块不同,主要的 JSON 模块只有一个,其名称令人难忘,即 json。此程序将数据编码(转储)为 JSON 字符串,并将 JSON 字符串解码(加载)回数据。在下面的示例中,让我们构建一个包含先前 XML 示例中的数据的 Python 数据结构:
>>> menu = \
... {
... "breakfast": {
... "hours": "7-11",
... "items": {
... "breakfast burritos": "$6.00",
... "pancakes": "$4.00"
... }
... },
... "lunch" : {
... "hours": "11-3",
... "items": {
... "hamburger": "$5.00"
... }
... },
... "dinner": {
... "hours": "3-10",
... "items": {
... "spaghetti": "$8.00"
... }
... }
... }
.
接下来,使用 dumps() 将数据结构 (menu) 编码为 JSON 字符串 (menu_json):
>>> import json
>>> menu_json = json.dumps(menu)
>>> menu_json
'{"dinner": {"items": {"spaghetti": "$8.00"}, "hours": "3-10"},
"lunch": {"items": {"hamburger": "$5.00"}, "hours": "11-3"},
"breakfast": {"items": {"breakfast burritos": "$6.00", "pancakes":
"$4.00"}, "hours": "7-11"}}'
现在,让我们通过使用 loads() 将 JSON 字符串 menu_json 转换回 Python 数据结构 (menu2):
>>> menu2 = json.loads(menu_json)
>>> menu2
{'breakfast': {'items': {'breakfast burritos': '$6.00', 'pancakes':
'$4.00'}, 'hours': '7-11'}, 'lunch': {'items': {'hamburger': '$5.00'},
'hours': '11-3'}, 'dinner': {'items': {'spaghetti': '$8.00'}, 'hours': '3-10'}}
menu 和 menu2 都是具有相同键和值的字典。
在尝试编码或解码某些对象时,可能会遇到异常,包括诸如 datetime 的对象(在 第十三章 中有详细说明),如下所示:
>>> import datetime
>>> import json
>>> now = datetime.datetime.utcnow()
>>> now
datetime.datetime(2013, 2, 22, 3, 49, 27, 483336)
>>> json.dumps(now)
Traceback (most recent call last):
# ... (deleted stack trace to save trees)
TypeError: datetime.datetime(2013, 2, 22, 3, 49, 27, 483336)
is not JSON serializable
>>>
这可能是因为 JSON 标准未定义日期或时间类型;它期望您定义如何处理它们。您可以将 datetime 转换为 JSON 理解的内容,如字符串或 epoch 值(见 第十三章):
>>> now_str = str(now)
>>> json.dumps(now_str)
'"2013-02-22 03:49:27.483336"'
>>> from time import mktime
>>> now_epoch = int(mktime(now.timetuple()))
>>> json.dumps(now_epoch)
'1361526567'
如果 datetime 值可能出现在通常转换的数据类型中间,这些特殊转换可能会令人困扰。您可以通过使用继承修改 JSON 的编码方式来进行修改,详见 第十章。Python 的 JSON 文档 提供了关于复数的示例,这也使 JSON 看起来像是死的。我们修改它以适应 datetime:
>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> class DTEncoder(json.JSONEncoder):
... def default(self, obj):
... # isinstance() checks the type of obj
... if isinstance(obj, datetime.datetime):
... return int(mktime(obj.timetuple()))
... # else it's something the normal decoder knows:
... return json.JSONEncoder.default(self, obj)
...
>>> json.dumps(now, cls=DTEncoder)
'1361526567'
新的类 DTEncoder 是 JSONEncoder 的子类或子类,我们需要重写其唯一的 default() 方法以添加 datetime 处理。继承确保所有其他内容都将由父类处理。
isinstance() 函数检查对象 obj 是否为 datetime.datetime 类的实例。因为 Python 中的所有东西都是对象,所以 isinstance() 在任何地方都适用:
>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> type(now)
<class 'datetime.datetime'>
>>> isinstance(now, datetime.datetime)
True
>>> type(234)
<class 'int'>
>>> isinstance(234, int)
True
>>> type('hey')
<class 'str'>
>>> isinstance('hey', str)
True
注意
对于 JSON 和其他结构化文本格式,您可以从文件加载到数据结构中,而无需事先了解结构的任何信息。然后,您可以使用 isinstance() 和适当类型的方法遍历这些结构以检查其值。例如,如果其中一个项目是字典,您可以通过 keys()、values() 和 items() 提取内容。
在让您以困难的方式处理后,事实证明有一种更简单的方法可以将 datetime 对象转换为 JSON:
>>> import datetime
>>> import json
>>> now = datetime.datetime.utcnow()
>>> json.dumps(now, default=str)
'"2019-04-17 21:54:43.617337"'
default=str 告诉 json.dumps() 对于它不理解的数据类型应用 str() 转换函数。这是因为 datetime.datetime 类的定义包含一个 __str__() 方法。
YAML
类似于 JSON,YAML具有键和值,但处理更多数据类型,如日期和时间。标准的 Python 库尚未包含 YAML 处理,因此您需要安装名为yaml的第三方库来操作它。((("dump() function")))((("load() function")))load()将 YAML 字符串转换为 Python 数据,而dump()则相反。
以下的 YAML 文件,mcintyre.yaml,包含了加拿大诗人詹姆斯·麦金太尔(James McIntyre)的信息,包括他的两首诗:
name:
first: James
last: McIntyre
dates:
birth: 1828-05-25
death: 1906-03-31
details:
bearded: true
themes: [cheese, Canada]
books:
url: http://www.gutenberg.org/files/36068/36068-h/36068-h.htm
poems:
- title: 'Motto'
text: |
Politeness, perseverance and pluck,
To their possessor will bring good luck.
- title: 'Canadian Charms'
text: |
Here industry is not in vain,
For we have bounteous crops of grain,
And you behold on every field
Of grass and roots abundant yield,
But after all the greatest charm
Is the snug home upon the farm,
And stone walls now keep cattle warm.
值如true、false、on和off会转换为 Python 布尔值。整数和字符串会转换为它们的 Python 等效项。其他语法创建列表和字典:
>>> import yaml
>>> with open('mcintyre.yaml', 'rt') as fin:
>>> text = fin.read()
>>> data = yaml.load(text)
>>> data['details']
{'themes': ['cheese', 'Canada'], 'bearded': True}
>>> len(data['poems'])
2
创建的数据结构与 YAML 文件中的相匹配,这在某些情况下是多层次的。您可以使用这个字典/列表/字典引用获取第二首诗的标题:
>>> data['poems'][1]['title']
'Canadian Charms'
警告
PyYAML 可以从字符串加载 Python 对象,这是危险的。如果您导入不信任的 YAML,请使用safe_load()而不是load()。更好的做法是,总是使用safe_load()。阅读 Ned Batchelder 的博客文章“War is Peace”了解未受保护的 YAML 加载如何危及 Ruby on Rails 平台。
Tablib
在阅读所有先前的章节之后,有一个第三方包可以让您导入、导出和编辑 CSV、JSON 或 YAML 格式的表格数据,¹还有 Microsoft Excel、Pandas DataFrame 和其他几个。您可以用熟悉的叠歌(pip install tablib)安装它,并查看文档。
Pandas
这是介绍pandas的好地方——一个用于结构化数据的 Python 库。它是处理现实生活数据问题的优秀工具:
-
读写许多文本和二进制文件格式:
-
文本,字段由逗号(CSV)、制表符(TSV)或其他字符分隔
-
固定宽度文本
-
Excel
-
JSON
-
HTML 表格
-
SQL
-
HDF5
-
和其他。
-
-
分组、拆分、合并、索引、切片、排序、选择、标记
-
转换数据类型
-
更改大小或形状
-
处理丢失的数据
-
生成随机值
-
管理时间序列
读取函数返回一个DataFrame对象,Pandas 的标准表示形式,用于二维数据(行和列)。在某些方面类似于电子表格或关系数据库表。它的一维小兄弟是Series。
示例 16-2 演示了一个简单的应用程序,从示例 16-1 中读取我们的villains.csv文件。
示例 16-2. 使用 Pandas 读取 CSV
>>> import pandas
>>>
>>> data = pandas.read_csv('villains.csv')
>>> print(data)
first last
0 Doctor No
1 Rosa Klebb
2 Mister Big
3 Auric Goldfinger
4 Ernst Blofeld
变量data刚刚展示的是一个DataFrame。它比基本的 Python 字典有更多的技巧。它特别适用于使用 NumPy 进行大量数字工作和为机器学习准备数据。
参考 Pandas 文档的“入门指南”部分以及“10 分钟入门 Pandas”中的工作示例。
让我们举个小日历例子使用 Pandas——列出 2019 年前三个月的第一天的列表:
>>> import pandas
>>> dates = pandas.date_range('2019-01-01', periods=3, freq='MS')
>>> dates
DatetimeIndex(['2019-01-01', '2019-02-01', '2019-03-01'],
dtype='datetime64[ns]', freq='MS')
你可以编写一些代码来实现这一点,使用第十三章中描述的时间和日期函数,但这需要更多的工作——特别是调试(日期和时间常常令人沮丧)。Pandas 还处理许多特殊的日期/时间细节,如业务月和年。
后面当我谈论映射时,Pandas 会再次出现(“Geopandas”)以及科学应用(“Pandas”)。
配置文件
大多数程序提供各种options或settings。动态的可以作为程序参数提供,但长期的需要保存在某个地方。定义自己快速而脏的config file格式的诱惑力很强——但要抵制它。它经常变得脏乱,但并不快速。您需要维护编写程序和读取程序(有时称为parser)。有很多好的替代方案可以直接插入您的程序,包括前面的部分中提到的那些。
这里,我们将使用标准configparser模块,它处理 Windows 风格的*.ini文件。这些文件有key* = value定义的部分。这是一个最小的settings.cfg文件:
[english]
greeting = Hello
[french]
greeting = Bonjour
[files]
home = /usr/local
# simple interpolation:
bin = %(home)s/bin
这是将其读入 Python 数据结构的代码:
>>> import configparser
>>> cfg = configparser.ConfigParser()
>>> cfg.read('settings.cfg')
['settings.cfg']
>>> cfg
<configparser.ConfigParser object at 0x1006be4d0>
>>> cfg['french']
<Section: french>
>>> cfg['french']['greeting']
'Bonjour'
>>> cfg['files']['bin']
'/usr/local/bin'
还有其他选择,包括更高级的插值。参见configparser的文档。如果需要比两级更深的嵌套,请尝试 YAML 或 JSON。
二进制文件
有些文件格式设计用于存储特定的数据结构,但既不是关系型数据库也不是 NoSQL 数据库。接下来的部分介绍了其中的一些。
填充的二进制文件和内存映射
这些类似于填充的文本文件,但内容可能是二进制的,填充字节可能是\x00而不是空格字符。每个记录和记录内的每个字段都有固定的大小。这使得在文件中通过seek()查找所需的记录和字段变得更简单。数据的每一项操作都是手动的,因此这种方法通常仅在非常低级别(例如接近硬件)的情况下使用。
这种形式的数据可以使用标准的mmap库进行memory mapped。参见一些示例和标准的文档。
电子表格
电子表格,特别是 Microsoft Excel,是广泛使用的二进制数据格式。如果可以将电子表格保存为 CSV 文件,可以使用早期描述的标准csv模块进行读取。这对于二进制的xls文件、xlrd或tablib(在“Tablib”早些时候提到)都适用。
HDF5
HDF5 是用于多维或层次化数值数据的二进制数据格式。它主要用于科学领域,其中快速访问大数据集(从几千兆字节到几太字节)是常见需求。尽管在某些情况下,HDF5 可能是数据库的良好替代品,但由于某些原因,HDF5 在商业界几乎不为人知。它最适合于WORM(写入一次/多次读取)应用程序,这种应用程序不需要数据库对抗冲突写入。以下是一些可能对你有用的模块:
这两者都是讨论 Python 在第二十二章科学应用程序方面的应用。我在这里提到 HDF5 是因为你可能需要存储和检索大量数据,并愿意考虑除了传统数据库解决方案以外的其他选择。一个很好的例子是百万首歌数据集,其中包含以 HDF5 和 SQLite 格式提供的可下载歌曲数据。
TileDB
用于密集或稀疏数组存储的最新后继者是TileDB。通过运行 pip install tiledb 安装Python 接口(包括 TileDB 库本身)。这专为科学数据和应用程序设计。
关系数据库
关系数据库虽然只有大约 40 年的历史,但在计算世界中无处不在。你几乎肯定会在某个时候必须处理它们。当你这样做时,你会感谢它们提供的:
-
多个同时用户访问数据
-
受用户防止损坏
-
高效存储和检索数据的方法
-
由模式定义的数据和受约束限制
-
连接以找到跨多种类型数据的关系
-
一种声明性(而不是命令性)查询语言:SQL(结构化查询语言)
这些被称为关系数据库,因为它们展示了不同类型数据之间的关系,以矩形表格的形式。例如,在我们之前的菜单示例中,每个项目与其价格之间存在关系。
表格是列(数据字段)和行(单个数据记录)的矩形网格,类似于电子表格。行和列的交集是表格的单元格。要创建表格,需要命名它并指定其列的顺序、名称和类型。每行都具有相同的列,尽管可以定义某列允许在单元格中包含缺失数据(称为nulls)。在菜单示例中,你可以为每个出售的项目创建一个包含价格等列的表格。
表的列或一组列通常是表的主键;其值在表中必须是唯一的。这样可以防止将相同的数据多次添加到表中。此键被索引以便在查询期间快速查找。索引的工作方式有点像书籍索引,使得快速找到特定行。
每个表都位于父数据库中,就像目录中的文件一样。两个层次的层次结构有助于使事物组织得更好一些。
注意
是的,单词数据库以多种方式使用:作为服务器,表容器和其中存储的数据。如果您同时提到它们所有,可能有助于将它们称为数据库服务器,数据库和数据。
如果要通过某些非关键列值查找行,请在该列上定义二级索引。否则,数据库服务器必须执行表扫描—对每一行进行匹配列值的 brute-force 搜索。
表可以通过外键相互关联,并且列值可以受限于这些键。
SQL
SQL 不是 API 或协议,而是声明性语言:您说您想要什么而不是如何做。这是关系数据库的通用语言。SQL 查询是由客户端发送到数据库服务器的文本字符串,数据库服务器然后确定如何处理它们。
有各种各样的 SQL 标准定义,所有数据库供应商都添加了自己的调整和扩展,导致了许多 SQL 方言。如果您将数据存储在关系型数据库中,SQL 可以提供一些可移植性。然而,方言和操作差异可能会使您将数据移动到另一种类型的数据库变得困难。SQL 语句有两个主要类别:
DDL(数据定义语言)
处理表,数据库和用户的创建,删除,约束和权限。
DML(数据操纵语言)
处理数据插入,选择,更新和删除。
表 16-1 列出了基本的 SQL DDL 命令。
表 16-1. 基本的 SQL DDL 命令
| 操作 | SQL 模式 | SQL 示例 |
|---|---|---|
| 创建数据库 | CREATE DATABASE dbname | CREATE DATABASE d |
| 选择当前数据库 | USE dbname | USE d |
| 删除数据库及其表 | DROP DATABASE dbname | DROP DATABASE d |
| 创建表 | CREATE TABLE tbname ( coldefs ) | CREATE TABLE t (id INT, count INT) |
| 删除表 | DROP TABLE tbname | DROP TABLE t |
| 从表中删除所有行 | TRUNCATE TABLE tbname | TRUNCATE TABLE t |
注意
为什么所有的大写字母?SQL 不区分大小写,但在代码示例中大声喊出关键字是一种传统(不要问我为什么),以区分它们和列名。
关系数据库的主要 DML 操作通常以 CRUD 缩写而闻名:
-
使用 SQL
INSERT语句Create -
R通过
SELECT读取 -
U通过
UPDATE更新 -
D通过
DELETE删除
表 16-2 查看了可用于 SQL DML 的命令。
表 16-2. 基本 SQL DML 命令
| 操作 | SQL 模式 | SQL 示例 |
|---|---|---|
| 添加一行 | INSERT INTO tbname VALUES( … ) | INSERT INTO t VALUES(7, 40) |
| 选择所有行和列 | SELECT * FROM tbname | SELECT * FROM t |
| 选择所有行,某些列 | SELECT cols FROM tbname | SELECT id, count FROM t |
| 选择某些行,某些列 | SELECT cols FROM tbname WHERE condition | SELECT id, count from t WHERE count > 5 AND id = 9 |
| 更改某列中的一些行 | UPDATE tbname SET col = value WHERE condition | UPDATE t SET count=3 WHERE id=5 |
| 删除某些行 | DELETE FROM tbname WHERE condition | DELETE FROM t WHERE count <= 10 OR id = 16 |
DB-API
应用程序编程接口(API)是一组可以调用以访问某些服务的函数。DB-API 是 Python 访问关系型数据库的标准 API。使用它,你可以编写一个程序,可以与多种关系型数据库一起工作,而不是为每种数据库编写单独的程序。它类似于 Java 的 JDBC 或 Perl 的 dbi。
它的主要功能如下:
connect()
建立与数据库的连接;这可以包括用户名、密码、服务器地址等参数。
cursor()
创建一个 cursor 对象来管理查询。
execute() 和 executemany()
对数据库运行一个或多个 SQL 命令。
fetchone()、fetchmany() 和 fetchall()
从 execute() 获取结果。
接下来的章节中的 Python 数据库模块符合 DB-API,通常具有扩展和一些细节上的差异。
SQLite
SQLite 是一个优秀、轻量、开源的关系型数据库。它作为标准的 Python 库实现,并且将数据库存储在普通文件中。这些文件可以跨机器和操作系统移植,使 SQLite 成为简单关系数据库应用程序的非常便携的解决方案。虽然不如 MySQL 或 PostgreSQL 功能全面,但它支持 SQL,并且能够管理多个同时用户。Web 浏览器、智能手机和其他应用程序都将 SQLite 用作嵌入式数据库。
首先通过 connect() 连接到你要使用或创建的本地 SQLite 数据库文件。这个文件相当于其他服务器中父表所在的类似目录的数据库。特殊字符串 ':memory:' 仅在内存中创建数据库;这对测试很快并且很有用,但在程序终止或计算机关机时会丢失数据。
作为下一个示例,让我们创建一个名为 enterprise.db 的数据库和一个名为 zoo 的表,以管理我们蓬勃发展的路边宠物动物园业务。表的列如下:
critter
变长字符串,以及我们的主键。
count
这种动物的当前库存的整数计数。
damages
我们当前因动物与人类互动而造成的损失金额。
>>> import sqlite3
>>> conn = sqlite3.connect('enterprise.db')
>>> curs = conn.cursor()
>>> curs.execute('''CREATE TABLE zoo
(critter VARCHAR(20) PRIMARY KEY,
count INT,
damages FLOAT)''')
<sqlite3.Cursor object at 0x1006a22d0>
Python 的三引号在创建长字符串(如 SQL 查询)时很方便。
现在,向动物园添加一些动物:
>>> curs.execute('INSERT INTO zoo VALUES("duck", 5, 0.0)')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.execute('INSERT INTO zoo VALUES("bear", 2, 1000.0)')
<sqlite3.Cursor object at 0x1006a22d0>
有一种更安全的方式可以插入数据,即使用占位符:
>>> ins = 'INSERT INTO zoo (critter, count, damages) VALUES(?, ?, ?)'
>>> curs.execute(ins, ('weasel', 1, 2000.0))
<sqlite3.Cursor object at 0x1006a22d0>
这次,我们在 SQL 中使用了三个问号来表示我们打算插入三个值,然后将这三个值作为元组传递给 execute() 函数。占位符处理繁琐的细节,如引用。它们保护您免受SQL 注入的攻击,这是一种将恶意 SQL 命令插入系统的外部攻击(在网络上很常见)。
现在,让我们看看是否可以再次把我们所有的动物都放出来:
>>> curs.execute('SELECT * FROM zoo')
<sqlite3.Cursor object at 0x1006a22d0>
>>> rows = curs.fetchall()
>>> print(rows)
[('duck', 5, 0.0), ('bear', 2, 1000.0), ('weasel', 1, 2000.0)]
让我们再次获取它们,但按计数排序:
>>> curs.execute('SELECT * from zoo ORDER BY count')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('weasel', 1, 2000.0), ('bear', 2, 1000.0), ('duck', 5, 0.0)]
嘿,我们希望它们按降序排列:
>>> curs.execute('SELECT * from zoo ORDER BY count DESC')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('duck', 5, 0.0), ('bear', 2, 1000.0), ('weasel', 1, 2000.0)]
哪种类型的动物给我们花费最多?
>>> curs.execute('''SELECT * FROM zoo WHERE
... damages = (SELECT MAX(damages) FROM zoo)''')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('weasel', 1, 2000.0)]
你可能会认为是熊。最好检查一下实际数据。
在我们离开 SQLite 之前,我们需要清理一下。如果我们打开了连接和游标,那么在完成时我们需要关闭它们:
>>> curs.close()
>>> conn.close()
MySQL
MySQL 是一个非常流行的开源关系型数据库。与 SQLite 不同,它是一个实际的服务器,因此客户端可以从网络上的不同设备访问它。
表 16-3 列出了您可以使用的驱动程序,以从 Python 访问 MySQL。有关所有 Python MySQL 驱动程序的更多详细信息,请参见python.org wiki。
表 16-3. MySQL 驱动程序
| Name | Link | Pypi package | Import as | Notes |
|---|---|---|---|---|
| mysqlclient | https://https://mysqlclient.readthedocs.io | mysql-connector-python | MySQLdb | |
| MySQL Connector | http://bit.ly/mysql-cpdg | mysql-connector-python | mysql.connector | |
| PYMySQL | https://github.com/petehunt/PyMySQL | pymysql | pymysql | |
| oursql | http://pythonhosted.org/oursql | oursql | oursql | 需要 MySQL C 客户端库 |
PostgreSQL
PostgreSQL 是一个功能齐全的开源关系型数据库。事实上,在许多方面,它比 MySQL 更先进。表 16-4 列出了您可以用来访问它的 Python 驱动程序。
表 16-4. PostgreSQL 驱动程序
| Name | Link | Pypi package | Import as | Notes |
|---|---|---|---|---|
| psycopg2 | http://initd.org/psycopg | psycopg2 | psycopg2 | 需要来自 PostgreSQL 客户端工具的 pg_config |
| py-postgresql | https://pypi.org/project/py-postgresql | py-postgresql | postgresql |
最受欢迎的驱动程序是 psycopg2,但它的安装需要 PostgreSQL 客户端库。
SQLAlchemy
对于所有关系数据库,SQL 并不完全相同,而 DB-API 也只能带你走到这一步。每个数据库都实现了一个反映其特性和哲学的特定方言。许多库试图以一种或另一种方式弥合这些差异。最流行的跨数据库 Python 库是 SQLAlchemy。
它不在标准库中,但它是众所周知的,并被许多人使用。你可以通过使用这个命令在你的系统上安装它:
$ pip install sqlalchemy
你可以在几个层面上使用 SQLAlchemy:
-
最低级别管理数据库连接池,执行 SQL 命令并返回结果。这是最接近 DB-API 的层次。
-
接下来是 SQL 表达式语言,它允许你以更加面向 Python 的方式表达查询。
-
最高级别是 ORM(对象关系模型)层,它使用 SQL 表达语言并将应用程序代码与关系数据结构绑定。
随着我们的进行,你会理解在这些层次中术语的含义。SQLAlchemy 与前面章节中记录的数据库驱动程序一起使用。你不需要导入驱动程序;你提供给 SQLAlchemy 的初始连接字符串将决定它。这个字符串看起来像这样:
*`dialect`* + *`driver`* :// *`user`* : *`password`* @ *`host`* : *`port`* / *`dbname`*
你在这个字符串中放入的值如下:
dialect
数据库类型
driver
你想要用于该数据库的特定驱动程序
user 和 password
您的数据库认证字符串
host 和 port
数据库服务器的位置(仅在不是该服务器的标准端口时需要)
dbname
最初连接到服务器的数据库
表 16-5 列出了方言和驱动程序。
表 16-5. SQLAlchemy 连接
| 方言 | 驱动程序 |
|---|---|
sqlite | pysqlite(或省略) |
mysql | mysqlconnector |
mysql | pymysql |
mysql | oursql |
postgresql | psycopg2 |
postgresql | pypostgresql |
另请参阅关于 MySQL、SQLite、PostgreSQL 和 其他数据库 的 SQLAlchemy 详细信息。
引擎层
首先,让我们尝试 SQLAlchemy 的最低级别,它只比基本的 DB-API 函数多做一些事情。
我们尝试使用内置到 Python 中的 SQLite。SQLite 的连接字符串省略了 host、port、user 和 password。dbname 告诉 SQLite 要使用哪个文件来存储你的数据库。如果省略 dbname,SQLite 将在内存中构建一个数据库。如果 dbname 以斜杠 (/) 开头,它是计算机上的绝对文件名(如在 Linux 和 macOS 中;例如,在 Windows 中是 C:\)。否则,它相对于当前目录。
下面的段落都属于一个程序的一部分,这里分开说明。
要开始,您需要导入所需内容。以下是一个 导入别名 的示例,它允许我们使用字符串 sa 来引用 SQLAlchemy 方法。我主要这样做是因为 sa 比 sqlalchemy 更容易输入:
>>> import sqlalchemy as sa
连接到数据库并在内存中创建存储它的位置(参数字符串 'sqlite:///:memory:' 也适用):
>>> conn = sa.create_engine('sqlite://')
创建一个名为 zoo 的数据库表,包含三列:
>>> conn.execute('''CREATE TABLE zoo
... (critter VARCHAR(20) PRIMARY KEY,
... count INT,
... damages FLOAT)''')
<sqlalchemy.engine.result.ResultProxy object at 0x1017efb10>
运行 conn.execute() 会返回一个称为 ResultProxy 的 SQLAlchemy 对象。您很快就会看到如何处理它。
顺便说一句,如果您以前从未制作过数据库表,请恭喜。在您的待办清单中打勾。
现在,将三组数据插入到您的新空表中:
>>> ins = 'INSERT INTO zoo (critter, count, damages) VALUES (?, ?, ?)'
>>> conn.execute(ins, 'duck', 10, 0.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017efb50>
>>> conn.execute(ins, 'bear', 2, 1000.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef090>
>>> conn.execute(ins, 'weasel', 1, 2000.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef450>
接下来,向数据库请求我们刚刚放入的所有内容:
>>> rows = conn.execute('SELECT * FROM zoo')
在 SQLAlchemy 中,rows 不是一个列表;它是我们无法直接打印的那个特殊的 ResultProxy 东西:
>>> print(rows)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef9d0>
但是,您可以像迭代列表一样迭代它,因此我们可以逐行获取:
>>> for row in rows:
... print(row)
...
('duck', 10, 0.0)
('bear', 2, 1000.0)
('weasel', 1, 2000.0)
这几乎与您之前看到的 SQLite DB-API 示例相同。一个优点是我们不需要在顶部导入数据库驱动程序;SQLAlchemy 从连接字符串中找到了这一点。只需更改连接字符串,即可将此代码移植到另一种类型的数据库。另一个优点是 SQLAlchemy 的 连接池,您可以在其 文档站点 上阅读有关它的信息。
SQL 表达式语言
上一级是 SQLAlchemy 的 SQL 表达式语言。它引入了函数来创建各种操作的 SQL。表达式语言处理的 SQL 方言差异比底层引擎层更多。对于关系数据库应用程序来说,它可以是一个方便的中间途径。
下面是如何创建和填充 zoo 表的方法。同样,这些是单个程序的连续片段。
导入和连接与之前相同:
>>> import sqlalchemy as sa
>>> conn = sa.create_engine('sqlite://')
要定义 zoo 表,我们开始使用一些表达式语言,而不是 SQL:
>>> meta = sa.MetaData()
>>> zoo = sa.Table('zoo', meta,
... sa.Column('critter', sa.String, primary_key=True),
... sa.Column('count', sa.Integer),
... sa.Column('damages', sa.Float)
... )
>>> meta.create_all(conn)
查看前面示例中多行调用中的括号。Table() 方法的结构与表的结构匹配。正如我们的表包含三列一样,在 Table() 方法调用的括号内有三次对 Column() 的调用。
与此同时,zoo 是一个魔术对象,连接了 SQL 数据库世界和 Python 数据结构世界。
使用更多表达式语言函数插入数据:
... conn.execute(zoo.insert(('bear', 2, 1000.0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017ea910>
>>> conn.execute(zoo.insert(('weasel', 1, 2000.0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017eab10>
>>> conn.execute(zoo.insert(('duck', 10, 0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017eac50>
接下来,创建 SELECT 语句(zoo.select() 选择由 zoo 对象表示的表中的所有内容,就像在普通 SQL 中执行 SELECT * FROM zoo 一样):
>>> result = conn.execute(zoo.select())
最后,获取结果:
>>> rows = result.fetchall()
>>> print(rows)
[('bear', 2, 1000.0), ('weasel', 1, 2000.0), ('duck', 10, 0.0)]
对象关系映射器(ORM)
在前一节中,zoo对象是 SQL 和 Python 之间的中间连接。在 SQLAlchemy 的顶层,对象关系映射器(ORM)使用 SQL 表达语言,但尝试使实际的数据库机制变得不可见。您定义类,ORM 处理如何将它们的数据进出数据库。复杂短语“对象关系映射器”的基本思想是,您可以在代码中引用对象,从而保持接近 Python 喜欢操作的方式,同时仍然使用关系数据库。
我们将定义一个Zoo类,并将其与 ORM 连接起来。这次,我们让 SQLite 使用文件zoo.db,以便确认 ORM 的工作。
与前两节类似,接下来的片段实际上是一个程序,由解释分隔开。如果您对某些内容不理解也不要担心。SQLAlchemy 文档中有所有细节——这些内容可能会变得复杂。我只是希望您了解做这件事情需要多少工作,这样您可以决定本章讨论的哪种方法最适合您。
初始导入是相同的,但这次我们还需要另外一些东西:
>>> import sqlalchemy as sa
>>> from sqlalchemy.ext.declarative import declarative_base
在这里,我们建立连接:
>>> conn = sa.create_engine('sqlite:///zoo.db')
现在,我们进入 SQLAlchemy 的 ORM。我们定义Zoo类,并关联其属性与表列:
>>> Base = declarative_base()
>>> class Zoo(Base):
... __tablename__ = 'zoo'
... critter = sa.Column('critter', sa.String, primary_key=True)
... count = sa.Column('count', sa.Integer)
... damages = sa.Column('damages', sa.Float)
... def __init__(self, critter, count, damages):
... self.critter = critter
... self.count = count
... self.damages = damages
... def __repr__(self):
... return "<Zoo({}, {}, {})>".format(self.critter, self.count,
... self.damages)
下面的代码神奇地创建了数据库和表:
>>> Base.metadata.create_all(conn)
您可以通过创建 Python 对象来插入数据。ORM 在内部管理这些数据:
>>> first = Zoo('duck', 10, 0.0)
>>> second = Zoo('bear', 2, 1000.0)
>>> third = Zoo('weasel', 1, 2000.0)
>>> first
<Zoo(duck, 10, 0.0)>
接下来,我们让 ORM 带我们进入 SQL 世界。我们创建一个会话来与数据库交互:
>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker(bind=conn)
>>> session = Session()
在会话中,我们将创建的三个对象写入数据库。add()函数添加一个对象,而add_all()添加一个列表:
>>> session.add(first)
>>> session.add_all([second, third])
最后,我们需要强制完成所有操作:
>>> session.commit()
它是否起作用?好吧,它在当前目录下创建了一个zoo.db文件。您可以使用命令行sqlite3程序来检查:
$ sqlite3 zoo.db
SQLite version 3.6.12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
zoo
sqlite> select * from zoo;
duck|10|0.0
bear|2|1000.0
weasel|1|2000.0
本节的目的是展示 ORM 是什么以及它如何在高层次上工作。SQLAlchemy 的作者写了一个完整的教程。阅读后,请决定以下哪种层次最适合您的需求:
-
就像之前的 SQLite 部分中的普通 DB-API 一样
-
SQLAlchemy 引擎
-
SQLAlchemy 表达语言
-
SQLAlchemy ORM
使用 ORM 似乎是避免 SQL 复杂性的自然选择。您应该使用 ORM 吗?有些人认为应该避免使用 ORM,但其他人认为这种批评是过度的。不管谁是对的,ORM 都是一种抽象,所有抽象都会泄漏,并在某些时候出现问题。当 ORM 不能按您的意愿工作时,您必须弄清楚它的工作原理以及如何在 SQL 中修复它。借用互联网迷因:
有些人面对问题时会想:“我知道了,我会使用 ORM。”现在他们有两个问题。
对于简单的应用程序或将数据相当直接映射到数据库表的应用程序,请使用 ORM。如果应用程序如此简单,您可以考虑使用纯 SQL 或 SQL 表达式语言。
其他数据库访问包
如果您正在寻找能处理多个数据库的 Python 工具,具有比纯 db-api 更多功能但少于 SQLAlchemy 的功能,那么这些工具值得一看:
-
dataset声称其目标是“懒惰人的数据库”。它构建在 SQLAlchemy 之上,并为 SQL、JSON 和 CSV 存储提供了一个简单的 ORM。 -
records自称为“人类的 SQL”。它仅支持 SQL 查询,内部使用 SQLAlchemy 处理 SQL 方言问题、连接池和其他细节。它与tablib的集成(在“Tablib”中提到)允许您将数据导出为 CSV、JSON 和其他格式。
NoSQL 数据存储
关系表是矩形的,但数据有多种形状,可能非常难以适应,需要进行大量努力和扭曲。这是一个方孔/圆孔问题。
一些非关系数据库已被编写,允许更灵活的数据定义,以及处理非常大的数据集或支持自定义数据操作。它们被统称为NoSQL(原意为no SQL,现在是更不具对抗性的not only SQL)。
最简单类型的 NoSQL 数据库是键值存储。一个流行的排名展示了我在以下章节中涵盖的一些数据库。
dbm 家族
dbm格式在NoSQL标签被创造之前就存在了。它们是简单的键值存储,通常嵌入在诸如 Web 浏览器之类的应用程序中,用于维护各种设置。dbm 数据库类似于 Python 字典的以下方面:
-
您可以为键分配一个值,并且它会自动保存到数据库中。
-
您可以查询键的值。
以下是一个快速示例。以下open()方法的第二个参数是'r'表示读取,'w'表示写入,'c'表示两者,如果文件不存在则创建:
>>> import dbm
>>> db = dbm.open('definitions', 'c')
要创建键值对,只需像创建字典一样将值分配给键:
>>> db['mustard'] = 'yellow'
>>> db['ketchup'] = 'red'
>>> db['pesto'] = 'green'
让我们停下来检查一下我们到目前为止所做的:
>>> len(db)
3
>>> db['pesto']
b'green'
现在关闭,然后重新打开以查看它是否实际保存了我们给予的内容:
>>> db.close()
>>> db = dbm.open('definitions', 'r')
>>> db['mustard']
b'yellow'
键和值被存储为bytes。您不能对数据库对象db进行迭代,但可以使用len()来获取键的数量。get()和setdefault()的工作方式与字典相同。
Memcached
memcached是一个快速的内存中键值缓存服务器。它经常被放在数据库前面,或用于存储 Web 服务器会话数据。
您可以在Linux 和 macOS以及Windows下载版本。如果您想尝试本节,您需要一个正在运行的 memcached 服务器和 Python 驱动程序。
有许多 Python 驱动程序;与 Python 3 兼容的一个是 python3-memcached,您可以使用以下命令安装它:
$ pip install python-memcached
要使用它,请连接到 memcached 服务器,之后您可以执行以下操作:
-
为键设置和获取值
-
通过使用
incr或decr增加或减少值 -
删除一个键
数据键和值不是持久的,之前写入的数据可能会消失。这是 memcached 的固有特性——它是一个缓存服务器,而不是数据库,并通过丢弃旧数据来避免内存耗尽。
您可以同时连接多个 memcached 服务器。在下一个示例中,我们仅与同一台计算机上的一个服务器通信:
>>> import memcache
>>> db = memcache.Client(['127.0.0.1:11211'])
>>> db.set('marco', 'polo')
True
>>> db.get('marco')
'polo'
>>> db.set('ducks', 0)
True
>>> db.get('ducks')
0
>>> db.incr('ducks', 2)
2
>>> db.get('ducks')
2
Redis
Redis是一个数据结构服务器。它处理键及其值,但是与其他键值存储中的值相比,值更丰富。与 memcached 一样,Redis 服务器中的所有数据都应该适合内存。不同于 memcached,Redis 可以执行以下操作:
-
将数据保存到磁盘以保证可靠性和重新启动
-
保留旧数据
-
提供比简单字符串更多的数据结构
Redis 的数据类型与 Python 的非常接近,并且 Redis 服务器可以成为一个或多个 Python 应用程序共享数据的有用中介。我发现它非常有用,因此在这里额外进行一些覆盖是值得的。
Python 驱动程序 redis-py 在 GitHub 上有其源代码和测试,以及 文档。您可以使用以下命令安装它:
$ pip install redis
Redis 服务器有很好的文档。如果在本地计算机上安装并启动 Redis 服务器(使用网络别名localhost),您可以尝试以下部分的程序。
字符串
一个具有单个值的键是 Redis 字符串。简单的 Python 数据类型会自动转换。连接到某个主机上的 Redis 服务器(默认为localhost)和端口(默认为6379):
>>> import redis
>>> conn = redis.Redis()
连接到redis.Redis('localhost')或redis.Redis('localhost', 6379)将给出相同的结果。
列出所有键(目前没有):
>>> conn.keys('*')
[]
设置一个简单的字符串(键'secret')、整数(键'carats')和浮点数(键'fever'):
>>> conn.set('secret', 'ni!')
True
>>> conn.set('carats', 24)
True
>>> conn.set('fever', '101.5')
True
通过键获取值(作为 Python byte 值):
>>> conn.get('secret')
b'ni!'
>>> conn.get('carats')
b'24'
>>> conn.get('fever')
b'101.5'
在这里,setnx()方法仅在键不存在时设置一个值:
>>> conn.setnx('secret', 'icky-icky-icky-ptang-zoop-boing!')
False
因为我们已经定义了'secret',所以失败了:
>>> conn.get('secret')
b'ni!'
getset()方法返回旧值,并同时设置为新值:
>>> conn.getset('secret', 'icky-icky-icky-ptang-zoop-boing!')
b'ni!'
不要急于前进。这有用吗?
>>> conn.get('secret')
b'icky-icky-icky-ptang-zoop-boing!'
现在,使用getrange()获取子字符串(与 Python 中一样,偏移量0表示起始,-1表示结尾):
>>> conn.getrange('secret', -6, -1)
b'boing!'
使用setrange()替换子字符串(使用从零开始的偏移量):
>>> conn.setrange('secret', 0, 'ICKY')
32
>>> conn.get('secret')
b'ICKY-icky-icky-ptang-zoop-boing!'
接下来,使用mset()一次设置多个键:
>>> conn.mset({'pie': 'cherry', 'cordial': 'sherry'})
True
通过使用mget()一次获取多个值:
>>> conn.mget(['fever', 'carats'])
[b'101.5', b'24']
使用delete()方法删除一个键:
>>> conn.delete('fever')
True
使用incr()或incrbyfloat()命令进行增量,并使用decr()进行减量:
>>> conn.incr('carats')
25
>>> conn.incr('carats', 10)
35
>>> conn.decr('carats')
34
>>> conn.decr('carats', 15)
19
>>> conn.set('fever', '101.5')
True
>>> conn.incrbyfloat('fever')
102.5
>>> conn.incrbyfloat('fever', 0.5)
103.0
没有decrbyfloat()。使用负增量来减少发烧:
>>> conn.incrbyfloat('fever', -2.0)
101.0
列表
Redis 列表只能包含字符串。当您进行第一次插入时,列表被创建。通过使用lpush()在开头插入:
>>> conn.lpush('zoo', 'bear')
1
在开头插入多个项目:
>>> conn.lpush('zoo', 'alligator', 'duck')
3
通过使用linsert()在值之前或之后插入:
>>> conn.linsert('zoo', 'before', 'bear', 'beaver')
4
>>> conn.linsert('zoo', 'after', 'bear', 'cassowary')
5
通过使用lset()在偏移处插入(列表必须已经存在):
>>> conn.lset('zoo', 2, 'marmoset')
True
通过使用rpush()在末尾插入:
>>> conn.rpush('zoo', 'yak')
6
通过使用lindex()按偏移量获取值:
>>> conn.lindex('zoo', 3)
b'bear'
通过使用lrange()获取偏移范围内的值(0 到-1 获取所有):
>>> conn.lrange('zoo', 0, 2)
[b'duck', b'alligator', b'marmoset']
使用ltrim()修剪列表,仅保留偏移范围内的元素:
>>> conn.ltrim('zoo', 1, 4)
True
通过使用lrange()获取值的范围(使用0到-1获取所有):
>>> conn.lrange('zoo', 0, -1)
[b'alligator', b'marmoset', b'bear', b'cassowary']
第十五章展示了如何使用 Redis 列表和发布-订阅来实现作业队列。
哈希
Redis 哈希类似于 Python 字典,但只能包含字符串。此外,您只能深入到一级,不能创建深度嵌套的结构。以下是创建和操作名为song的 Redis 哈希的示例:
通过使用hmset()一次设置哈希song中的字段do和re:
>>> conn.hmset('song', {'do': 'a deer', 're': 'about a deer'})
True
通过使用hset()在哈希中设置单个字段值:
>>> conn.hset('song', 'mi', 'a note to follow re')
1
通过使用hget()获取一个字段的值:
>>> conn.hget('song', 'mi')
b'a note to follow re'
通过使用hmget()获取多个字段值:
>>> conn.hmget('song', 're', 'do')
[b'about a deer', b'a deer']
通过使用hkeys()获取哈希的所有字段键:
>>> conn.hkeys('song')
[b'do', b're', b'mi']
通过使用hvals()获取哈希的所有字段值:
>>> conn.hvals('song')
[b'a deer', b'about a deer', b'a note to follow re']
通过使用hlen()获取哈希中字段的数量:
>>> conn.hlen('song')
3
通过使用hgetall()获取哈希中的所有字段键和值:
>>> conn.hgetall('song')
{b'do': b'a deer', b're': b'about a deer', b'mi': b'a note to follow re'}
如果其键不存在,则使用hsetnx()设置字段:
>>> conn.hsetnx('song', 'fa', 'a note that rhymes with la')
1
集合
Redis 集合与 Python 集合类似,您将在以下示例中看到。
向集合添加一个或多个值:
>>> conn.sadd('zoo', 'duck', 'goat', 'turkey')
3
获取集合的值的数量:
>>> conn.scard('zoo')
3
获取集合的所有值:
>>> conn.smembers('zoo')
{b'duck', b'goat', b'turkey'}
从集合中移除一个值:
>>> conn.srem('zoo', 'turkey')
True
让我们再建立一个集合来展示一些集合操作:
>>> conn.sadd('better_zoo', 'tiger', 'wolf', 'duck')
0
求交集(获取zoo和better_zoo集合的共同成员):
>>> conn.sinter('zoo', 'better_zoo')
{b'duck'}
获取zoo和better_zoo的交集,并将结果存储在集合fowl_zoo中:
>>> conn.sinterstore('fowl_zoo', 'zoo', 'better_zoo')
1
里面有谁?
>>> conn.smembers('fowl_zoo')
{b'duck'}
获取zoo和better_zoo的并集(所有成员):
>>> conn.sunion('zoo', 'better_zoo')
{b'duck', b'goat', b'wolf', b'tiger'}
将联合结果存储在集合fabulous_zoo中:
>>> conn.sunionstore('fabulous_zoo', 'zoo', 'better_zoo')
4
>>> conn.smembers('fabulous_zoo')
{b'duck', b'goat', b'wolf', b'tiger'}
zoo有什么,better_zoo没有?使用sdiff()获取集合的差集,并使用sdiffstore()将其保存在zoo_sale集合中:
>>> conn.sdiff('zoo', 'better_zoo')
{b'goat'}
>>> conn.sdiffstore('zoo_sale', 'zoo', 'better_zoo')
1
>>> conn.smembers('zoo_sale')
{b'goat'}
排序集合
最多用途的 Redis 数据类型之一是排序集合,或zset。它是一组唯一值,但每个值都有一个关联的浮点数分数。您可以通过其值或分数访问每个项目。排序集合有许多用途:
-
排行榜
-
二级索引
-
时间序列,使用时间戳作为分数
我们展示了最后一个用例,通过时间戳跟踪用户登录。我们使用 Python time()函数返回的 Unix epoch值(更多详情请参见第十五章):
>>> import time
>>> now = time.time()
>>> now
1361857057.576483
让我们添加第一个看起来紧张的客人:
>>> conn.zadd('logins', 'smeagol', now)
1
五分钟后,另一位客人:
>>> conn.zadd('logins', 'sauron', now+(5*60))
1
两小时后:
>>> conn.zadd('logins', 'bilbo', now+(2*60*60))
1
一天后,不要着急:
>>> conn.zadd('logins', 'treebeard', now+(24*60*60))
1
bilbo以什么顺序到达?
>>> conn.zrank('logins', 'bilbo')
2
那是什么时候?
>>> conn.zscore('logins', 'bilbo')
1361864257.576483
让我们按登录顺序查看每个人:
>>> conn.zrange('logins', 0, -1)
[b'smeagol', b'sauron', b'bilbo', b'treebeard']
请附上它们的时间:
>>> conn.zrange('logins', 0, -1, withscores=True)
[(b'smeagol', 1361857057.576483), (b'sauron', 1361857357.576483),
(b'bilbo', 1361864257.576483), (b'treebeard', 1361943457.576483)]
缓存和过期
所有的 Redis 键都有一个到期时间,默认情况下是永久的。我们可以使用expire()函数来指示 Redis 保留键的时间长度。该值是以秒为单位的数字:
>>> import time
>>> key = 'now you see it'
>>> conn.set(key, 'but not for long')
True
>>> conn.expire(key, 5)
True
>>> conn.ttl(key)
5
>>> conn.get(key)
b'but not for long'
>>> time.sleep(6)
>>> conn.get(key)
>>>
expireat()命令在给定的纪元时间点过期键。键的过期对于保持缓存新鲜和限制登录会话非常有用。类比:在你的杂货店货架后面的冷藏室中,当牛奶达到保质期时,店员们就会将那些加仑装出货。
文档数据库
文档数据库是一种 NoSQL 数据库,它以不同的字段存储数据。与关系表(每行具有相同列的矩形表)相比,这样的数据是“参差不齐”的,每行具有不同的字段(列),甚至是嵌套字段。你可以使用 Python 字典和列表在内存中处理这样的数据,或者将其存储为 JSON 文件。要将这样的数据存储在关系数据库表中,你需要定义每个可能的列,并使用 null 来表示缺失的数据。
ODM可以代表对象数据管理器或对象文档映射器(至少它们同意“O”部分)。ODM 是文档数据库的关系数据库 ORM 对应物。一些流行的文档数据库和工具(驱动程序和 ODM)列在表 16-6 中。
表 16-6. 文档数据库
| 数据库 | Python API |
|---|---|
| Mongo | tools |
| DynamoDB | boto3 |
| CouchDB | couchdb |
注意
PostgreSQL 可以做一些文档数据库可以做的事情。它的一些扩展允许它逃离关系规范,同时保留事务、数据验证和外键等特性:1)多维数组 - 在表单元格中存储多个值;2)jsonb - 在单元格中存储 JSON 数据,并进行完整的索引和查询。
时间序列数据库
时间序列数据可以在固定间隔(如计算机性能指标)或随机时间收集,这导致了许多存储方法。其中许多中的一些,一些具有 Python 支持的方法列在表 16-7 中。
表 16-7. 时间数据库
| 数据库 | Python API |
|---|---|
| InfluxDB | influx-client |
| kdb+ | PyQ |
| Prometheus | prometheus_client |
| TimescaleDB | (PostgreSQL clients) |
| OpenTSDB | potsdb |
| PyStore | PyStore |
图数据库
对于需要有自己数据库类别的最后一种数据案例,我们有 图形:节点(数据)通过 边缘 或 顶点(关系)相连。一个个体 Twitter 用户 可以是一个节点,与其他用户的关系如 关注 和 被关注 为边。
随着社交媒体的增长,图形数据变得更加明显,价值在于连接而非内容本身。一些流行的图数据库在 表 16-8 中概述。
表 16-8. 图数据库
| 数据库 | Python API |
|---|---|
| Neo4J | py2neo |
| OrientDB | pyorient |
| ArangoDB | pyArango |
其他 NoSQL
这里列出的 NoSQL 服务器处理比内存更大的数据,并且许多使用多台计算机。表 16-9 展示了显著的服务器及其 Python 库。
表 16-9. NoSQL 数据库
| 数据库 | Python API |
|---|---|
| Cassandra | pycassa |
| CouchDB | couchdb-python |
| HBase | happybase |
| Kyoto Cabinet | kyotocabinet |
| MongoDB | mongodb |
| Pilosa | python-pilosa |
| Riak | riak-python-client |
全文搜索数据库
最后,还有一个专门用于 全文 搜索的数据库类别。它们索引所有内容,因此您可以找到那些讲述风车和巨大芝士轮的诗歌。您可以在 表 16-10 中看到一些流行的开源示例及其 Python API。
表 16-10. 全文搜索数据库
| 网站 | Python API |
|---|---|
| Lucene | pylucene |
| Solr | SolPython |
| ElasticSearch | elasticsearch |
| Sphinx | sphinxapi |
| Xapian | xappy |
| Whoosh | (用 Python 编写,包含 API) |
即将出现
前一章讨论了在时间上交错使用代码(并发)。接下来的章节将介绍如何在空间中移动数据(网络),这不仅可以用于并发,还有其他用途。
待办事项
16.1 将以下文本行保存到名为 books.csv 的文件中(注意,如果字段由逗号分隔,如果字段包含逗号,你需要用引号括起来):
author,book
J R R Tolkien,The Hobbit
Lynne Truss,"Eats, Shoots & Leaves"
16.2 使用 csv 模块及其 DictReader 方法读取 books.csv 并存入变量 books。打印 books 中的值。DictReader 是否处理了第二本书标题中的引号和逗号?
16.3 使用以下代码创建一个名为 books2.csv 的 CSV 文件:
title,author,year
The Weirdstone of Brisingamen,Alan Garner,1960
Perdido Street Station,China Miéville,2000
Thud!,Terry Pratchett,2005
The Spellman Files,Lisa Lutz,2007
Small Gods,Terry Pratchett,1992
16.4 使用 sqlite3 模块创建一个名为 books.db 的 SQLite 数据库,并创建一个名为 books 的表,具有以下字段:title(文本)、author(文本)和 year(整数)。
16.5 读取 books2.csv 并将其数据插入到 book 表中。
16.6 按字母顺序选择并打印 book 表中的 title 列。
16.7 按照出版顺序选择并打印 book 表中的所有列。
16.8 使用 sqlalchemy 模块连接到你在练习 16.4 中刚刚创建的 sqlite3 数据库 books.db。如同 16.6,按字母顺序选择并打印 book 表中的 title 列。
16.9 在你的计算机上安装 Redis 服务器和 Python redis 库(pip install redis)。创建一个名为 test 的 Redis 哈希,具有字段 count(1)和 name('Fester Bestertester')。打印 test 的所有字段。
16.10 增加 test 的 count 字段并打印它。
¹ 啊,还没到 XML 的时候。
9312

被折叠的 条评论
为什么被折叠?



