十一、文件之类的东西
到目前为止,我们主要处理驻留在解释器本身中的数据结构。我们的程序通过input
和print
与外界进行了很少的互动。在这一章中,我们更进一步,让我们的程序瞥见一个更大的世界:文件和流的世界。本章描述的函数和对象将使您能够在程序调用之间存储数据,并处理来自其他程序的数据。
打开文件
您可以使用open
函数打开文件,该函数位于io
模块中,但会自动为您导入。它将文件名作为唯一的强制参数,并返回一个 file 对象。假设您在当前目录中存储了一个名为somefile.txt
的文本文件(可能是用您的文本编辑器创建的),您可以像这样打开它:
>>> f = open('somefile.txt')
如果文件位于其他位置,您也可以指定文件的完整路径。但是,如果它不存在,您将看到一个异常回溯,如下所示:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'somefile.txt'
如果您想通过向其中写入文本来创建文件,这并不完全令人满意。解决方案在open
的第二个参数中找到。
文件模式
如果只使用文件名作为参数的话,你会得到一个可以读取的文件对象。如果您想要写入文件,您必须明确地声明,提供一个模式。open
函数的模式参数可以有几个值,如表 11-1 所示。
表 11-1。
Most Common Values for the Mode Argument of the open Function
| 价值 | 描述 | | --- | --- | | `'r'` | 读取模式(默认) | | `'w'` | 写入模式 | | `'x'` | 独占写入模式 | | `'a'` | 附加方式 | | `'b'` | 二进制模式(添加到其他模式) | | `'t'` | 文本模式(默认,添加到其他模式) | | `'+'` | 读/写模式(添加到其他模式) |显式指定读取模式与根本不提供模式字符串具有相同的效果。写入模式允许您写入文件,如果文件不存在,将创建该文件。独占写模式更进一步,如果文件已经存在,则引发一个FileExistsError
。如果以写模式打开现有文件,现有内容将被删除或截断,并从文件的开头重新开始写。如果您想一直写到现有文件的末尾,请使用 append 模式。
可以将'+'
添加到任何其他模式中,以指示允许读取和写入。所以,例如,'r+'
可以在打开一个文本文件进行读写时使用。(为了有用,你可能也想使用seek
;请参阅本章后面的侧栏“随机访问”。)注意,'r+'
和'w+'
有一个重要区别:后者会截断文件,而前者不会。
默认模式是'rt'
,这意味着您的文件被视为编码的 Unicode 文本。然后自动执行解码和编码,默认编码为 UTF-8。其他编码和 Unicode 错误处理策略可以使用encoding
和errors
关键字参数来设置。(有关 Unicode 的更多信息,请参见第一章。)还有一些换行符的自动翻译。默认情况下,行以'\n'
结束。其他行尾('\r'
或'\r\n'
)会在阅读时自动替换。在写入时,'\n'
被替换为系统默认的行尾(os.linesep
)。
通常,Python 使用所谓的通用换行符模式,在这种模式下,任何有效的换行符('\n'
、'\r'
或'\r\n'
)都会被识别,例如,通过后面讨论的readlines
方法。如果您希望保持这种模式,但希望防止自动翻译到'\n'
,您可以向newline
关键字参数提供一个空字符串,如open(name, newline='')
所示。如果您想指定只有'\r'
或'\r\n'
被视为有效的行尾,请提供您喜欢的行尾。这种情况下,读的时候行尾不翻译,写的时候会用合适的行尾替换'\n'
。
如果您的文件包含非文本的二进制数据,如声音剪辑或图像,您肯定不希望执行这些自动转换。在这种情况下,您只需使用二进制模式('rb'
)来关闭任何特定于文本的功能。
还有一些更高级的可选参数,用于控制缓冲和更直接地处理文件描述符。参见 Python 文档,或者在交互式解释器中运行help(open)
来了解更多信息。
基本文件方法
现在你知道如何打开文件了。下一步是用它们做一些有用的事情。在本节中,您将了解文件对象的一些基本方法,以及其他一些类似文件的对象,有时称为流。类似文件的对象只是支持一些与文件相同的方法,最明显的是读或写或者两者都支持。由urlopen
返回的对象(见第十四章)就是一个很好的例子。例如,它们支持像read
和readline
这样的方法,但是不支持像write
和isatty
这样的方法。
Three Standard Streams
在第十章中,在关于sys
模块的部分,我提到了三个标准流。这些是类似文件的对象,您可以对它们应用您对文件的大部分了解。
数据输入的标准来源是sys.stdin
。当程序从标准输入中读取时,您可以通过键入文本来提供文本,或者您可以使用管道将其与另一个程序的标准输出相链接,如“管道输出”一节中所示
您输入给print
的文本会出现在sys.stdout
中。input
的提示也在那里。写入sys.stdout
的数据通常出现在你的屏幕上,但是可以通过管道重新路由到另一个程序的标准输入,如上所述。
错误消息(如堆栈跟踪)被写入sys.stderr
,这与sys.stdout
类似,但可以单独重新路由。
阅读和写作
文件最重要的功能是提供和接收数据。如果你有一个名为f
的类文件对象,你可以用f.write
写数据,用f.read
读数据。和大多数 Python 功能一样,使用什么作为数据也有一定的灵活性,但是使用的基本类是str
和bytes
,分别用于文本和二进制模式。
每次调用f.write(string)
时,您提供的字符串会在您之前写入的字符串之后写入文件。
>>> f = open('somefile.txt', 'w')
>>> f.write('Hello, ')
7
>>> f.write('World!')
6
>>> f.close()
注意,当我处理完文件时,我调用了close
方法。您将在本章后面的“关闭文件”一节中了解更多信息。读书也一样简单。只要记住告诉流你想要读取多少个字符(或者字节,在二进制模式下)。这里有一个例子(从我停止的地方继续):
>>> f = open('somefile.txt', 'r')
>>> f.read(4)
'Hell'
>>> f.read()
'o, World!'
首先,我指定要读取多少个字符(4),然后我简单地读取文件的其余部分(不提供数字)。请注意,我本可以从对open
的调用中删除模式说明,因为'r'
是默认值。
管道输出
在 bash 这样的 shell 中,您可以一个接一个地编写几个命令,用管道连接在一起,如下例所示:
$ cat somefile.txt | python somescript.py | sort
这个管道由三个命令组成。
cat somefile.txt
:该命令只是将文件somefile.txt
的内容写入标准输出(sys.stdout
)。python somescript.py
:该命令执行 Python 脚本somescript
。该脚本可能从其标准输入中读取数据,并将结果写入标准输出。sort
:该命令从标准输入(sys.stdin
)中读取所有文本,按字母顺序对行进行排序,并将结果写入标准输出。
但是这些管道字符(|
)有什么意义,somescript.py
又是做什么的呢?管道将一个命令的标准输出与下一个命令的标准输入连接起来。聪明吧。所以你可以有把握地猜测,somescript.py
从它的sys.stdin
(这是cat somefile.txt
写的)读取数据,并将一些结果写到它的sys.stdout
(这是sort
获取数据的地方)。
清单 11-1 中显示了一个使用sys.stdin
的简单脚本(somescript.py
)。文件somefile.txt
的内容如清单 11-2 所示。
# somescript.py
import sys
text = sys.stdin.read()
words = text.split()
wordcount = len(words)
print('Wordcount:', wordcount)
Listing 11-1.Simple Script That Counts the Words in sys.stdin
Your mother was a hamster and your
father smelled of elderberries.
Listing 11-2.A File Containing Some Nonsensical Text
下面是cat somefile.txt | python somescript.py
的结果:
Wordcount: 11
Random Access
在这一章中,我只把文件当作流——你只能严格按照顺序从头到尾读取数据。事实上,您还可以移动文件,通过使用两个文件对象方法seek
和tell
,只访问您感兴趣的部分(称为随机访问)。
方法seek(offset[, whence])
将当前位置(执行读或写的位置)移动到offset
描述的位置,whence. offset
是一个字节(字符)计数。whence
默认为io.SEEK_SET
或0
,表示从文件开始偏移(偏移量必须为非负)。whence
也可以设置为io.SEEK_CUR
或1
(相对于当前位置移动;偏移量可能为负)或io.SEEK_END
或2
(相对于文件末尾移动)。考虑这个例子:
>>> f = open(r'C:\text\somefile.txt', 'w')
>>> f.write('01234567890123456789')
20
>>> f.seek(5)
5
>>> f.write('Hello, World!')
13
>>> f.close()
>>> f = open(r'C:\text\somefile.txt')
>>> f.read()
'01234Hello, World!89'
方法tell()
返回当前文件位置,如下例所示:
>>> f = open(r'C:\text\somefile.txt')
>>> f.read(3)
'012'
>>> f.read(2)
'34'
>>> f.tell()
5
读写线
其实我到现在一直在做的事情有点不切实际。我可以像一个字母一个字母地读一样地读一行行的文字。您可以使用readline
方法读取一行(从您到目前为止的位置开始,直到并包括您遇到的第一个行分隔符的文本)。您可以在没有任何参数的情况下使用这个方法(在这种情况下,只读取并返回一行),或者使用一个非负整数,这是允许readline
读取的最大字符数。所以如果some_file.readline()
返回'Hello, World!\n'
,那么some_file.readline(5)
返回'Hello'
。要读取一个文件的所有行并将它们作为一个列表返回,使用readlines
方法。
方法writelines
与readlines
相反:给它一个字符串列表(或者,事实上,任何序列或可迭代对象),它将所有字符串写入文件(或流)。请注意,没有添加换行符;你需要自己添加这些。此外,没有writeline
方法,因为你可以只使用write
。
关闭文件
你应该记得通过调用它们的close
方法来关闭你的文件。通常,当你退出程序时(可能在此之前),file 对象会自动关闭,不关闭你正在读取的文件并不重要。然而,关闭这些文件没有坏处,而且可能有助于避免在某些操作系统和设置中保持文件被无用地“锁定”以防修改。这也避免了耗尽系统中打开文件的配额。
您应该总是关闭您已经写入的文件,因为 Python 可能会缓冲(出于效率原因,暂时存储在某个地方)您已经写入的数据,如果您的程序由于某种原因崩溃,数据可能根本不会写入文件。安全的做法是在你看完文件后关闭它们。如果您想重置缓冲并使您的更改在磁盘上的实际文件中可见,但您还不想关闭文件,您可以使用flush
方法。但是,请注意,flush
可能不允许同时运行的其他程序访问该文件,因为锁定的考虑取决于您的操作系统和设置。只要您可以方便地关闭文件,这是更可取的。
如果你想确定你的文件已经关闭,你可以使用一个try
/ finally
语句,在finally
子句中调用close
。
# Open your file here
try:
# Write data to your file
finally:
file.close()
事实上,有一种说法是专门为这种情况设计的——with
说法。
with open("somefile.txt") as somefile:
do_something(somefile)
with
语句允许您打开一个文件,并为其指定一个变量名(在本例中为somefile
)。然后,在语句体中向文件中写入数据(可能还会做其他事情),当到达语句结尾时,文件会自动关闭,即使这是由异常引起的。
Context Managers
with
语句实际上是一个非常通用的构造,允许您使用所谓的上下文管理器。上下文管理器是一个支持两种方法的对象:__enter__
和__exit__
。
__enter__
方法没有参数。它在进入with
语句时被调用,返回值被绑定到as
关键字后的变量。
__exit__
方法有三个参数:异常类型、异常对象和异常回溯。当离开方法时调用它(通过参数提供任何引发的异常)。如果__exit__
返回 false,任何异常都将被抑制。
文件可以用作上下文管理器。它们的__enter__
方法返回文件对象本身,而它们的__exit__
方法关闭文件。有关这个强大但相当高级的特性的更多信息,请查看 Python 参考手册中对上下文管理器的描述。另请参见 Python 库参考中关于上下文管理器类型和contextlib
的章节。
使用基本文件方法
假设somefile.txt
包含清单 11-3 中的文本。你能用它做什么?
Welcome to this file
There is nothing here except
This stupid haiku
Listing 11-3.A Simple Text File
让我们试试你知道的方法,从read(n)
开始。
>>> f = open(r'C:\text\somefile.txt')
>>> f.read(7)
'Welcome'
>>> f.read(4)
' to '
>>> f.close()
接下来是read()
:
>>> f = open(r'C:\text\somefile.txt')
>>> print(f.read())
Welcome to this file
There is nothing here except
This stupid haiku
>>> f.close()
这里是readline()
:
>>> f = open(r'C:\text\somefile.txt')
>>> for i in range(3):
print(str(i) + ': ' + f.readline(), end='')
0: Welcome to this file
1: There is nothing here except
2: This stupid haiku
>>> f.close()
这里是readlines()
:
>>> import pprint
>>> pprint.pprint(open(r'C:\text\somefile.txt').readlines())
['Welcome to this file\n',
'There is nothing here except\n',
'This stupid haiku']
注意,在这个例子中,我依赖于文件对象被自动关闭。现在让我们试着写作,从write(string)
开始。
>>> f = open(r'C:\text\somefile.txt', 'w')
>>> f.write('this\nis no\nhaiku')
13
>>> f.close()
运行这个之后,文件包含清单 11-4 中的文本。
this
is no
haiku
Listing 11-4.The Modified Text File
最后,这里是writelines(list)
:
>>> f = open(r'C:\text\somefile.txt')
>>> lines = f.readlines()
>>> f.close()
>>> lines[1] = "isn't a\n"
>>> f = open(r'C:\text\somefile.txt', 'w')
>>> f.writelines(lines)
>>> f.close()
运行这个之后,文件包含清单 11-5 中的文本。
this
isn't a
haiku
Listing 11-5.The Text File, Modified Again
迭代文件内容
现在您已经看到了文件对象呈现给我们的一些方法,并且您已经学习了如何获取这样的文件对象。对文件的一个常见操作是迭代它们的内容,在进行过程中重复执行一些操作。有很多方法可以做到这一点,你当然可以找到你最喜欢的,并坚持下去。但是,其他人可能做得不一样,要理解他们的程序,你应该知道所有的基本技术。
在本节的所有示例中,我使用一个名为process
的虚构函数来表示每个字符或行的处理。你可以随意用你喜欢的任何方式来实现它。这里有一个简单的例子:
def process(string):
print('Processing:', string)
更有用的实现可以将数据存储在数据结构中,计算总和,用re
模块替换模式,或者添加行号。
此外,为了试验这些示例,您应该将变量filename
设置为某个实际文件的名称。
一次一个字符(或字节)
迭代文件内容的一个最基本(但可能是最不常见)的方法是在一个while
循环中使用read
方法。例如,您可能想要循环文件中的每个字符(或者,在二进制模式下,每个字节)。你可以这样做,如清单 11-6 所示。如果你想读几个字符或字节的块,提供期望的长度给read
。
with open(filename) as f:
char = f.read(1)
while char:
process(char)
char = f.read(1)
Listing 11-6.Looping over Characters with read
这个程序之所以有效,是因为当到达文件末尾时,read
方法返回一个空字符串,但在此之前,该字符串始终包含一个字符(因此布尔值为 true)。只要char
为真,你就知道自己还没完。
如你所见,我已经重复了赋值char = f.read(1)
,代码重复通常被认为是一件坏事。(懒惰是一种美德,记得吗?)为了避免这种情况,我们可以使用第五章中介绍的while True
/ break
技术。结果代码如清单 11-7 所示。
with open(filename) as f:
while True:
char = f.read(1)
if not char: break
process(char)
Listing 11-7.Writing the Loop Differently
正如在第五章中提到的,你不应该太频繁地使用break
语句(因为它会使代码更难理解)。即便如此,清单 11-7 中所示的方法通常比清单 11-6 中所示的方法更受欢迎,这正是因为您避免了重复代码。
一次一行
当处理文本文件时,您通常感兴趣的是遍历文件中的行,而不是每个字符。你可以像我们处理字符一样,使用readline
方法(之前在“读写行”一节中描述过),很容易地做到这一点,如清单 11-8 所示。
with open(filename) as f:
while True:
line = f.readline()
if not line: break
process(line)
Listing 11-8.Using readline in a while Loop
阅读一切
如果文件不太大,您可以使用不带参数的read
方法(将整个文件作为一个字符串读取)或readlines
方法(将文件读入一个字符串列表,其中每个字符串为一行)一次性读取整个文件。清单 11-9 和 11-10 展示了当您像这样阅读文件时,遍历字符和行是多么容易。注意,像这样将文件的内容读入字符串或列表,除了迭代之外,对其他事情也很有用。例如,您可以对字符串应用正则表达式,或者将行列表存储在某个数据结构中以备将来使用。
with open(filename) as f:
for char in f.read():
process(char)
Listing 11-9.Iterating over Characters with read
with open(filename) as f:
for line in f.readlines():
process(line)
Listing 11-10.Iterating over Lines with readlines
使用 fileinput 的惰性行迭代
有时你需要迭代一个非常大的文件中的行,而readlines
会使用太多的内存。当然,您可以将while
循环与readline
一起使用,但是在 Python 中,当for
循环可用时,最好使用它们。恰好他们在这种情况下。你可以使用一种叫做懒惰线迭代的方法——它之所以懒惰,是因为它只读取文件中实际需要的部分(或多或少)。
你已经在第十章中遇到了fileinput
。清单 11-11 展示了你可能如何使用它。注意,fileinput
模块负责打开文件。你只需要给它一个文件名。
import fileinput
for line in fileinput.input(filename):
process(line)
Listing 11-11.Iterating over Lines with fileinput
文件迭代器
是时候采用最酷(也是最常见)的技术了。文件实际上是可迭代的,这意味着您可以在for
循环中直接使用它们来迭代它们的行。参见清单 11-12 中的示例。
with open(filename) as f:
for line in f:
process(line)
Listing 11-12.Iterating over a File
在这些迭代例子中,我使用文件作为上下文管理器,以确保我的文件是关闭的。虽然这通常是一个好主意,但它不是绝对关键的,只要我不写入文件。如果你愿意让 Python 来处理收尾工作,你可以进一步简化这个例子,如清单 11-13 所示。这里,我没有将打开的文件赋给一个变量(就像我在其他例子中使用的变量f
),因此我没有办法显式关闭它。
for line in open(filename):
process(line)
Listing 11-13.Iterating over a File Without Storing the File Object in a Variable
注意sys.stdin
是可迭代的,就像其他文件一样,所以如果您想迭代标准输入中的所有行,您可以使用以下形式:
import sys
for line in sys.stdin:
process(line)
同样,你可以做所有你可以用迭代器做的事情,比如把它们转换成字符串列表(通过使用list(open(filename))
),这就相当于使用readlines
。
>>> f = open('somefile.txt', 'w')
>>> print('First', 'line', file=f)
>>> print('Second', 'line', file=f)
>>> print('Third', 'and final', 'line', file=f)
>>> f.close()
>>> lines = list(open('somefile.txt'))
>>> lines
['First line\n', 'Second line\n', 'Third and final line\n']
>>> first, second, third = open('somefile.txt')
>>> first
'First line\n'
>>> second
'Second line\n'
>>> third
'Third and final line\n'
在本例中,请务必注意以下几点:
- 我已经使用了
print
来写入文件。这将自动在我提供的字符串后添加新行。 - 我对打开的文件使用序列解包,将每一行放在一个单独的变量中。(这并不是很常见的做法,因为您通常不知道文件中的行数,但是它展示了 file 对象的“可迭代性”。)
- 我在写入文件后关闭它,以确保数据被刷新到磁盘。(你也看到了,我从里面看完就没关。草率,也许,但不是关键。)
快速总结
在本章中,你已经看到了如何通过文件和类似文件的对象与环境交互,这是 Python 中 I/O 最重要的技术之一。以下是本章的一些亮点:
- 类似文件的对象:类似文件的对象(非正式地)是支持一组方法的对象,比如
read
和readline
(可能还有write
和writelines
)。 - 打开和关闭文件:通过提供文件名,用
open
函数打开一个文件。如果你想确保你的文件被关闭,即使出了问题,你也可以使用with
语句。 - 模式和文件类型:当打开一个文件时,你也可以提供一个模式,比如
'r'
表示读模式,或者'w'
表示写模式。通过将'b'
添加到您的模式中,您可以将文件作为二进制文件打开,并关闭 Unicode 编码和换行符。 - 标准流:三个标准文件(
stdin
、stdout
和stderr
,在sys
模块中找到)是实现 UNIX 标准 I/O 机制(在 Windows 中也可用)的类似文件的对象。 - 读写:使用方法
read
从文件或类似文件的对象中读取。你用方法write
写。 - 读写行:可以使用
readline
和readlines
从文件中读取行。你可以用writelines
写文件。 - 迭代文件内容:有许多方法可以迭代文件内容。最常见的是迭代一个文本文件的行,你可以通过简单地迭代文件本身来实现。还有其他方法,比如使用
readlines
,它们与旧版本的 Python 兼容。
本章的新功能
| 功能 | 描述 | | --- | --- | | `open(name, ...)` | 打开一个文件并返回一个 file 对象 |什么现在?
现在你知道了如何通过文件与环境交互,但是如何与用户交互呢?到目前为止,我们只使用了input
和print
,除非用户在你的程序可以读取的文件中写了一些东西,否则你真的没有任何其他工具来创建用户界面。这在下一章会有所改变,我将介绍带有窗口、按钮等的图形用户界面。
十二、图形用户界面
在这相当短的一章中,您将学习如何为您的 Python 程序制作图形用户界面(GUI)的基础知识——您知道,带有按钮和文本字段之类的东西的窗口。Python 事实上的标准 GUI 工具包是 Tkinter,它是标准 Python 发行版的一部分。但是,还有其他几个工具包可用。这有它的优点(更大的选择自由)和缺点(其他人不能使用你的程序,除非他们安装了相同的 GUI 工具包)。幸运的是,Python 可用的各种 GUI 工具包之间没有冲突,所以您可以安装任意多的不同 GUI 工具包。
本章简要介绍了 Tkinter 的使用,我们将在第二十八章对此进行介绍。Tkinter 很容易使用,但如果你想使用它的所有功能,还有很多东西要学。我在这里只讲一些皮毛,以便让你继续下去;关于更多的细节,你应该参考标准图书馆参考中关于图形用户界面的章节。在那里,您可以找到 Tkinter 的文档,以及到具有更深入信息的站点的链接,以及对其他 GUI 包使用的建议。
构建一个示例 GUI 应用
为了演示如何使用 Tkinter,我将向您展示如何构建一个简单的 GUI 应用。你的任务是编写一个基本程序,使你能够编辑文本文件。我们不打算写一个完全成熟的文本编辑器,而是坚持要点。毕竟,目标是演示 Python 中 GUI 编程的基本机制。
这个最小文本编辑器的要求如下:
- 它必须允许你打开文本文件,给定它们的文件名。
- 它必须允许您编辑文本文件。
- 它必须允许您保存文本文件。
- 它必须允许你退出。
当编写一个 GUI 程序时,画一个你想要的草图通常是很有用的。图 12-1 显示了一个简单的布局,它满足了我们的文本编辑器的需求。
图 12-1。
A sketch of the text editor
该界面的元素可以按如下方式使用:
- 在按钮左侧的文本字段中键入文件名,然后点按“打开”以打开文件。
文件中包含的文本放在底部的文本字段中。
- 您可以在大文本栏中随心所欲地编辑文本。
- 如果要保存更改,请单击 save 按钮,该按钮将再次使用包含文件名的文本字段,并将大文本字段的内容写入文件。
- 没有退出按钮——我们将只使用默认 Tkinter 菜单中的退出命令。
这可能看起来有点令人生畏的任务,但它真的是小菜一碟。
初步探索
首先,您必须导入tkinter
。为了保持其名称空间的独立性并节省一些输入,您可能需要对其进行重命名。
import tkinter as tk
不过,如果您愿意的话,只导入它的所有内容也没什么坏处。对于一些初步的探索,让我们只使用交互式解释器。
>>> from tkinter import *
要启动 GUI,我们可以创建一个顶层组件或小部件,它将充当我们的主窗口。我们通过实例化一个Tk
对象来做到这一点。
>>> top = Tk()
此时,应该会出现一个窗口。在普通程序中,我们会在这里插入对函数mainloop
的调用,以进入 Tkinter 主事件循环,而不是简单地退出程序。在交互式解释器中不需要这样做,但是请随意尝试。
>>> mainloop()
当 GUI 仍在工作时,解释器似乎会挂起。要继续,请退出 GUI 并重新启动解释器。
各种小部件都有很明显的名字。例如,要创建一个按钮,需要实例化Button
类。如果没有Tk
实例,创建一个小部件也会实例化Tk
,所以你可以直接进入。
>>> from tkinter import *
>>> btn = Button()
此时按钮不可见—您需要使用布局管理器(也称为几何管理器)来告诉 Tkinter 放置它的位置。我们将使用包管理器,它最简单的形式就是调用pack
方法。
>>> btn.pack()
小部件有各种属性,我们可以用它们来修改它们的外观和行为。这些属性就像字典字段一样可用,所以如果我们想给按钮一些文本,所需要的只是一个赋值。
>>> btn['text'] = 'Click me!'
现在,您应该有一个类似如下的窗口:
向按钮添加一些行为也很简单。
>>> def clicked():
... print('I was clicked!')
...
>>> btn['command'] = clicked
如果您现在单击按钮,您应该会看到打印出来的消息。
您可以使用config
方法一次设置几个属性,而不是单独分配。
>>> btn.config(text='Click me!', command=clicked)
您还可以使用其构造函数来配置小部件。
>>> Button(text='Click me too!', command=clicked).pack()
布局
当我们在一个小部件上调用pack
时,它被放置在它的父小部件或主部件中。主窗口小部件可以作为可选的第一个参数提供给构造函数;如果我们不提供,将使用主顶级窗口,如下面的代码片段所示:
Label(text="I'm in the first window!").pack()
second = Toplevel()
Label(second, text="I'm in the second window!").pack()
Toplevel
类代表主窗口之外的顶层窗口,Label
只是一个文本标签。
如果没有任何参数,pack 将简单地将窗口小部件堆叠在一个单独的居中的列中,从窗口的顶部开始。例如,下面的代码将生成一个又高又薄的窗口,其中只有一列按钮:
for i in range(10):
Button(text=i).pack()
幸运的是,您可以调整小部件的位置和拉伸。你打包一个小部件的面是由side
参数给定的,你提供LEFT
、RIGHT
、TOP
或BOTTOM
。如果您希望小部件填充 x 或 y 方向上分配给它的空间,您可以指定一个值为X
、Y
或BOTH
的fill
。如果您希望它随着父窗口(在本例中是窗口)的增长而增长,您可以将expand
设置为 true。还有其他选项,用于指定锚定和填充,虽然我不会在这里使用它们。要获得快速概览,您可以使用以下内容:
>>> help(Pack.config)
还有其他布局管理器选项,可能更适合您的口味,即grid
和place
。你在布局的小部件上调用这些方法,就像使用pack
一样。为了避免麻烦,你应该为一个容器使用一个布局管理器,比如一个窗口。
grid
方法允许您通过将对象放置在不可见表格的单元格中来对其进行布局;如果小部件跨越多行或多列,您可以通过指定一个row
和column
以及可能的一个rowspan
或columnspan
来做到这一点。通过指定坐标x
和y
,以及小部件的height
和weight
,方法允许您手动放置小部件。这很少是一个好主意,但有时可能需要。这两个几何图形管理器还具有附加参数,您可以使用以下命令找到这些参数:
>>> help(Grid.configure)
>>> help(Place.config)
事件处理
如您所见,我们可以通过设置command
属性为按钮提供一个动作。这是事件处理的一种特殊形式,对此 Tkinter 也有一种更通用的机制:bind
方法。您可以在想要处理给定类型事件的小部件上调用这个函数,指定事件的名称和要使用的函数。这里有一个例子:
>>> from tkinter import *
>>> top = Tk()
>>> def callback(event):
... print(event.x, event.y)
...
>>> top.bind('<Button-1>', callback)
'4322424456callback'
这里,<Button-1>
是使用左键(按钮 1)的鼠标点击(或等效操作)的名称。我们将它绑定到回调函数,每当我们在顶部窗口中单击时都会调用该函数。事件对象被传递给回调,根据事件的种类,它有不同的属性。例如,对于鼠标点击,它提供 x 和 y 坐标,在本例中打印出来。许多其他种类的事件是可用的。您可以使用以下命令来查找列表
>>> help(Tk.bind)
并且可以通过查阅先前描述的资源来找到进一步的信息。
最终方案
至此,我们已经大致了解了编写程序所需的内容。我们只需要找出用于小文本字段和大文本区域的小部件的名称。快速浏览文档告诉我们,Entry
就是我们想要的单行文本字段。通过组合Text
和Scrollbar
可以构建一个多行、滚动的文本区域,但是在tkinter.scrolledtext
模块中已经有了一个可用的实现。一个Entry
的内容可以使用它的get
方法提取,而对于ScrolledText
对象,我们将使用delete
和insert
方法,用适当的参数来指示文本中的位置。在我们的例子中,我们将使用'1.0'
指定第一行和第零个字符(即在第一个字符之前),使用END
指定文本的结尾,使用INSERT
指定当前插入点。结果程序如清单 12-1 和图 12-2 所示。
图 12-2。
The final text editor
from tkinter import *
from tkinter.scrolledtext import ScrolledText
def load():
with open(filename.get()) as file:
contents.delete('1.0', END)
contents.insert(INSERT, file.read())
def save():
with open(filename.get(), 'w') as file:
file.write(contents.get('1.0', END))
top = Tk()
top.title("Simple Editor")
contents = ScrolledText()
contents.pack(side=BOTTOM, expand=True, fill=BOTH)
filename = Entry()
filename.pack(side=LEFT, expand=True, fill=X)
Button(text='Open', command=load).pack(side=LEFT)
Button(text='Save', command=save).pack(side=LEFT)
mainloop()
Listing 12-1.Simple GUI Text Editor
您可以使用以下步骤尝试编辑器:
- 运行程序。您应该得到一个类似于前面运行中的窗口。
- 在大文本区域中键入一些内容(例如,
Hello, world!
)。 - 在小文本字段中键入文件名(例如,
hello.txt
)。请确保该文件不存在,否则将被覆盖。 - 单击保存按钮。
- 退出程序。
- 重启程序。
- 在小文本字段中键入相同的文件名。
- 单击打开按钮。文件的文本应该重新出现在大文本区域。
- 随心所欲地编辑文件,然后再次保存。
现在你可以继续打开、编辑和保存,直到你厌倦为止。然后你就可以开始考虑改进了。例如,允许你的程序用urllib
模块下载文件怎么样?
当然,你也可以考虑在你的程序中使用更多的面向对象的设计。例如,您可能希望将主应用作为一个定制应用类的实例来管理,该类具有设置各种小部件和绑定的方法。参见第二十八章中的一些例子。和任何 GUI 包一样,Tkinter 有很多小部件和其他类供您使用。您应该使用help(tkinter)
或查阅文档来获取您想要使用的任何图形元素的信息。
用别的东西
大多数 GUI 工具包的基本原理大致相同。然而,不幸的是,当学习如何使用一个新的软件包时,需要花时间来找到通过所有细节的方法,使您能够准确地做您想要做的事情。因此,在决定使用哪个包之前,您应该慢慢来(例如,参见 Python 标准库参考中关于其他 GUI 包的部分),然后沉浸在它的文档中并开始编写代码。我希望这一章已经提供了理解该文档所需的基本概念。
快速总结
让我们再次回顾一下我们在本章中讲述的内容:
- 图形用户界面(GUI):GUI 有助于使你的程序更加用户友好。不是所有的程序都需要图形用户界面,但是每当你的程序与用户交互时,图形用户界面可能会有所帮助。
- Tkinter: Tkinter 是一个成熟的、广泛可用的 Python 跨平台 GUI 工具包。
- 布局:通过直接指定组件的几何图形,可以非常简单地定位组件。然而,要使它们在调整包含它们的窗口大小时行为正常,您需要使用某种布局管理器。
- 事件处理:用户执行的动作触发 GUI 工具包中的事件。为了有用,您的程序可能会被设置为对这些事件做出反应;否则,用户将无法与之交互。在 Tkinter 中,使用
bind
方法将事件处理程序添加到组件中。
什么现在?
就这样。你现在知道如何编写可以通过文件和图形用户界面与外界交互的程序。在下一章,你将学习许多程序系统的另一个重要组成部分:数据库。
十三、数据库支持
使用简单的纯文本文件只能做到这一步。是的,它们可以让你走得很远,但在某些时候,你可能需要一些额外的功能。你可能想要一些自动化连载,你可以求助于shelve
(见第十章)和pickle
(是shelve
的近亲)。但是你可能想要比这更好的功能。例如,您可能希望自动支持对数据的并发访问,也就是说,允许多个用户读写基于磁盘的数据,而不会导致任何损坏的文件等。或者您可能希望能够同时使用许多数据字段或属性来执行复杂的搜索,而不是简单的shelve
单键查找。有大量的解决方案可供选择,但是如果您希望这可以扩展到大量的数据,并且希望该解决方案易于被其他程序员理解,那么选择一种相对标准的数据库形式可能是一个好主意。
本章讨论了 Python 数据库 API,这是一种连接 SQL 数据库的标准化方法,并演示了如何使用该 API 执行一些基本的 SQL。最后一节还讨论了一些可供选择的数据库技术。
我不会给你一个关于关系数据库或 SQL 语言的教程。大多数数据库(如 PostgreSQL 或 MySQL,或本章中使用的 SQLite)的文档应该涵盖您需要了解的内容。如果你以前没有使用过关系数据库,你可能想看看 www.sqlcourse.com
(或者只是在网上搜索一下这个主题)或者《SQL 查询入门》,第二版。,作者克莱尔·丘奇(Apress,2016)。
到目前为止,本章中使用的简单数据库(SQLite)当然不是唯一的选择。有几种流行的商业选择(如 Oracle 或 Microsoft SQL Server),以及一些可靠且广泛使用的开源数据库(如 MySQL、PostgreSQL 和 Firebird)。关于 Python 包支持的一些其他数据库的列表,请查看 https://wiki.python.org/moin/DatabaseInterfaces
。当然,关系(SQL)数据库并不是唯一的一种。有对象数据库,如 Zope 对象数据库(ZODB, http://zodb.org
),基于紧凑表的数据库,如 Metakit ( http://equi4.com/metakit
),甚至更简单的键值数据库,如 UNIX DBM ( https://docs.python.org/3/library/dbm.html
)。还有各种各样越来越流行的 NoSQL 数据库,比如 MongoDB ( http://mongodb.com
)、Cassandra ( http://cassandra.apache.org
)和 Redis ( http://redis.io
),都可以从 Python 访问。
虽然这一章关注的是底层的数据库交互,但是您可以找到一些高级的库来帮助您抽象掉一些麻烦(例如,参见 http://sqlalchemy.org
或 http://sqlobject.org
,或者在 Web 上搜索其他所谓的 Python 对象关系映射器)。
Python 数据库 API
正如我提到的,您可以从各种 SQL 数据库中进行选择,其中许多数据库在 Python 中都有相应的客户端模块(有些数据库甚至有几个)。所有数据库的大部分基本功能都是相同的,所以为使用其中一个数据库而编写的程序可能很容易——理论上——用于另一个数据库。在提供相同功能(或多或少)的不同模块之间切换的问题通常是它们的接口(API)不同。为了解决 Python 中数据库模块的这一问题,已经就标准数据库 API (DB API)达成一致。API 的当前版本(2.0)在 PEP 249,Python 数据库 API 规范 v2.0 中定义(可从 http://python.org/peps/pep-0249.html
获得)。
本节将为您提供基础知识的概述。我不会讨论 API 的可选部分,因为它们并不适用于所有数据库。你可以在上面提到的 PEP 或者官方 Python Wiki 中的数据库编程指南中找到更多信息(可以从 http://wiki.python.org/moin/DatabaseProgramming
获得)。如果您对所有 API 细节不感兴趣,可以跳过这一节。
全局变量
任何兼容的数据库模块(兼容,即与 DB API 版本 2.0 兼容)必须有三个全局变量,它们描述了模块的特性。这是因为 API 被设计得非常灵活,可以与几种不同的底层机制一起工作,而不需要太多的包装。如果您想让您的程序与几个不同的数据库一起工作,这可能是一件麻烦的事情,因为您需要涵盖许多不同的可能性。在许多情况下,更现实的做法是简单地检查这些变量,看看给定的数据库模块是否能被您的程序接受。如果不是,您可以简单地退出,给出一个适当的错误消息,或者引发一些异常。表 13-1 总结了全局变量。
表 13-1。
The Module Properties of the Python DB API
| 变量名 | 使用 | | --- | --- | | `apilevel` | 正在使用的 Python DB API 的版本 | | `threadsafety` | 模块的线程安全程度 | | `paramstyle` | SQL 查询中使用了哪种参数样式 |API 级别(apilevel
)只是一个字符串常量,给出了正在使用的 API 版本。根据 DB API 版,它可能有值'1.0'
或值'2.0'
。如果变量不存在,则模块不符合 2.0,并且您应该(根据 API)假设 DB API 版本 1.0 是有效的。在这里编写允许其他值的代码也不会有什么坏处(谁知道 DB API 的 3.0 版本什么时候会出来呢?).
线程安全级别(threadsafety
)是从 0 到 3 的整数,包括 0 和 3。0 表示线程可能根本不共享模块,3 表示模块是完全线程安全的。值 1 表示线程可以共享模块本身,但不能共享连接(请参阅本章后面的“连接和游标”),值 2 表示线程可以共享模块和连接,但不能共享游标。如果你不使用线程(大多数时候,你可能不会),你根本不用担心这个变量。
参数样式(paramstyle
)表示当您让数据库执行多个类似的查询时,如何将参数拼接到 SQL 查询中。值'format'
表示标准的字符串格式(使用基本格式代码),因此,例如,您可以在想要拼接参数的地方插入%s
。值'pyformat'
表示扩展格式代码,用于老式的字典拼接,如%(foo)s
。除了这些 Pythonic 风格之外,还有三种编写拼接字段的方式:'qmark'
表示使用问号,'numeric'
表示形式为:1
或:2
的字段(其中数字是参数的数字),'named'
表示类似于:foobar
的字段,其中foobar
是参数名称。如果参数样式看起来令人困惑,不要担心。对于 basic 程序,你不会需要它们,如果你需要了解一个特定的数据库接口是如何处理参数的,相关文档大概会有解释。
例外
API 定义了几个异常,使得细粒度的错误处理成为可能。然而,它们是在层次结构中定义的,所以您也可以用一个except
块捕获几种类型的异常。(当然,如果您希望一切都运行良好,并且不介意万一出现问题时关闭程序,那么您可以完全忽略异常。)
异常等级如表 13-2 所示。异常应该在给定的数据库模块中全局可用。有关这些异常的更深入的描述,请参见 API 规范(前面提到的 PEP)。
表 13-2。
Exceptions Specified in the Python DB API
| 例外 | 超类 | 描述 | | --- | --- | --- | | `StandardError` | | 所有异常的一般超类 | | `Warning` | `StandardError` | 发生非致命问题时引发 | | `Error` | `StandardError` | 所有错误条件的一般超类 | | `InterfaceError` | `Error` | 与界面而非数据库相关的错误 | | `DatabaseError` | `Error` | 与数据库相关的错误的超类 | | `DataError` | `DatabaseError` | 与数据相关的问题;例如,超出范围的值 | | `OperationalError` | `DatabaseError` | 数据库操作的内部错误 | | `IntegrityError` | `DatabaseError` | 关系完整性受损;例如,密钥检查失败 | | `InternalError` | `DatabaseError` | 数据库中的内部错误;例如无效光标 | | `ProgrammingError` | `DatabaseError` | 用户编程错误;例如,未找到表 | | `NotSupportedError` | `DatabaseError` | 请求了不支持的功能(例如回滚) |连接和光标
为了使用底层数据库系统,您必须首先连接到它。为此,您可以使用名副其实的函数connect
。它需要几个参数;具体哪一个取决于数据库。API 将表 13-3 中的参数定义为指南。建议将它们用作关键字参数,并遵循表中给出的顺序。参数应该都是字符串。
表 13-3。
Common Parameters of the connect Function
| 参数名称 | 描述 | 可选? | | --- | --- | --- | | `dsn` | 数据源名称。具体含义取决于数据库。 | 不 | | `user` | 用户名。 | 是 | | `password` | 用户密码。 | 是 | | `host` | 主机名。 | 是 | | `database` | 数据库名称。 | 是 |你会在本章后面的“入门”一节以及第二十六章中看到使用connect
功能的具体例子。
connect
函数返回一个连接对象。这表示您当前与数据库的会话。连接对象支持表 13-4 所示的方法。
表 13-4。
Connection Object Methods
| 方法名称 | 描述 | | --- | --- | | `close()` | 关闭连接。连接对象及其光标现在不可用。 | | `commit()` | 如果支持,提交挂起的事务;否则,不执行任何操作。 | | `rollback()` | 回滚挂起的事务(可能不可用)。 | | `cursor()` | 返回连接的光标对象。 |rollback
方法可能不可用,因为并非所有数据库都支持事务。(事务只是动作的序列。)如果它存在,它将“撤销”任何尚未提交的事务。
commit
方法总是可用的,但是如果数据库不支持事务,它实际上什么也不做。如果您关闭了一个连接,但仍有事务未提交,它们将隐式回滚—但前提是数据库支持回滚!所以如果你不想依赖这个,你应该在关闭连接之前提交。如果你提交了,你可能不需要太担心关闭你的连接;垃圾回收时会自动关闭。不过,如果你想安全起见,调用close
不会让你损失那么多按键。
cursor
方法将我们引向另一个主题:光标对象。您使用游标来执行 SQL 查询并检查结果。游标比连接支持更多的方法,可能会在你的程序中更加突出。表 13-5 给出了光标方法的概述,表 13-6 给出了属性的概述。
表 13-6。
Cursor Object Attributes
| 名字 | 描述 | | --- | --- | | `description` | 结果列描述的序列。只读。 | | `rowcount` | 结果中的行数。只读。 | | `arraysize` | 在`fetchmany`中返回多少行。默认值为 1。 |表 13-5。
Cursor Object Methods
| 名字 | 描述 | | --- | --- | | `callproc(name[, params])` | 用给定的名称和参数调用命名的数据库过程(可选)。 | | `close()` | 关闭光标。光标现在不可用。 | | `execute(oper[, params])` | 执行 SQL 操作,可能带有参数。 | | `executemany(oper, pseq)` | 对序列中的每个参数集执行 SQL 操作。 | | `fetchone()` | 获取查询结果集的下一行作为一个序列,或`None`。 | | `fetchmany([size])` | 获取查询结果集的几行。默认大小为`arraysize`。 | | `fetchall()` | 将所有(剩余)行作为一系列序列提取。 | | `nextset()` | 跳到下一个可用的结果集(可选)。 | | `setinputsizes(sizes)` | 用于预定义参数的存储区域。 | | `setoutputsize(size[, col])` | 设置提取大数据值的缓冲区大小。 |这些方法中的一些将在接下来的文本中更详细地解释,而一些(如setinputsizes
和setoutputsizes
)将不被讨论。更多详情请咨询 PEP。
类型
为了正确地与底层 SQL 数据库进行互操作(这可能会对插入到某些类型的列中的值提出各种要求), DB API 定义了用于特殊类型和值的某些构造函数和常量(singletons)。例如,如果您想要向数据库添加一个日期,它应该用(例如)相应数据库连接模块的Date
构造函数来构造。这允许连接模块在后台执行任何必要的转换。每个模块都需要实现表 13-7 中所示的构造函数和特殊值。有些模块可能不完全兼容。例如,sqlite3
模块(下面讨论)不输出表 13-7 中的特殊值(STRING
到ROWID
)。
表 13-7。
DB API Constructors and Special Values
| 名字 | 描述 | | --- | --- | | `Date(year, month, day)` | 创建保存日期值的对象 | | `Time(hour, minute, second)` | 创建保存时间值的对象 | | `Timestamp(y, mon, d, h, min, s)` | 创建保存时间戳值的对象 | | `DateFromTicks(ticks)` | 创建一个对象,该对象保存自纪元以来刻度的日期值 | | `TimeFromTicks(ticks)` | 创建一个保存刻度时间值的对象 | | `TimestampFromTicks(ticks)` | 从 ticks 创建一个保存时间戳值的对象 | | `Binary(string)` | 创建保存二进制字符串值的对象 | | `STRING` | 描述基于字符串的列类型(如`CHAR`) | | `BINARY` | 描述二进制列(如`LONG`或`RAW`) | | `NUMBER` | 描述数字列 | | `DATETIME` | 描述日期/时间列 | | `ROWID` | 描述行 ID 列 |SQLite 和 PySQLite
如前所述,许多 SQL 数据库引擎都有相应的 Python 模块。大多数数据库引擎都是作为服务器程序运行的,即使安装它们也需要管理员权限。为了降低使用 Python DB API 的门槛,我选择使用一个名为 SQLite 的小型数据库引擎,它不需要作为独立的服务器运行,可以直接处理本地文件,而不是使用某种集中式数据库存储机制。
在最近的 Python 版本(从 2.5 开始)中,SQLite 的优势在于它的包装器(PySQLite,以sqlite3
模块的形式)包含在标准库中。除非您自己从源代码编译 Python,否则很可能数据库本身也包括在内。您可能只想尝试“入门”一节中的程序片段如果它们可以工作,您就不需要费心分别安装 PySQLite 和 SQLite。
Note
如果您没有使用 PySQLite 的标准库版本,您可能需要修改 import 语句。有关更多信息,请参考相关文档。
Getting Pysqlite
如果您使用的是旧版本的 Python,则需要安装 PySQLite,然后才能使用 SQLite 数据库。可以从 https://github.com/ghaering/pysqlite
下载。
对于带有包管理器系统的 Linux 系统,您可以直接从包管理器获得 PySQLite 和 SQLite。你也可以使用 Python 自己的包管理器pip
。也可以获得 PySQLite 和 SQLite 的源码包,自己编译。
如果您使用的是 Python 的最新版本,那么您肯定会使用 PySQLite。如果缺少什么,那就是数据库本身,SQLite(但同样,它也可能是可用的)。你可以从 SQLite 的网页上获取源码, http://sqlite.org
。(确保您获得了一个已经执行了自动代码生成的源包。)编译 SQLite 基本上就是按照附带的自述文件中的说明进行操作。当随后编译 PySQLite 时,您需要确保编译过程可以访问 SQLite 库和包含文件。如果您已经在某个标准位置安装了 SQLite,那么很可能 PySQLite 发行版中的安装脚本可以自己找到它。在这种情况下,您只需执行以下命令:
python setup.py build
python setup.py install
您可以简单地使用后一个命令,它将自动执行构建。如果这给你一堆错误信息,安装脚本可能没有找到需要的文件。确保您知道包含文件和库的安装位置,并明确地将它们提供给安装脚本。假设我在名为/home/mlh/sqlite/current
的目录中就地编译了 SQLite 那么头文件可以在/home/mlh/sqlite/current/src
中找到,库可以在/home/mlh/sqlite/current/build/lib
中找到。为了让安装过程使用这些路径,编辑安装脚本setup.py
。在这个文件中,你需要设置变量include_dirs
和library_dirs
。
include_dirs = ['/home/mlh/sqlite/current/src']
library_dirs = ['/home/mlh/sqlite/current/build/lib']
重新绑定这些变量后,前面描述的安装过程应该可以正常工作,不会出现错误。
入门指南
您可以将 SQLite 作为一个模块导入,命名为sqlite3
(如果您使用的是 Python 标准库中的模块)。然后,您可以通过提供文件名(可以是文件的相对或绝对路径)直接创建到数据库文件的连接,如果数据库文件不存在,将会创建该连接。
>>> import sqlite3
>>> conn = sqlite3.connect('somedatabase.db')
然后,您可以从这个连接中获得一个光标。
>>> curs = conn.cursor()
然后,可以使用该游标来执行 SQL 查询。完成后,如果您做了任何更改,请确保提交它们,以便它们实际上保存到文件中。
>>> conn.commit()
您可以(也应该)在每次修改数据库时提交,而不仅仅是在准备关闭数据库时。当您准备关闭它时,只需使用close
方法。
>>> conn.close()
一个示例数据库应用
作为一个例子,我将演示如何基于美国农业部(USDA)农业研究服务中心( https://www.ars.usda.gov
)的数据构建一个小型营养数据库。它们的链接往往会移动一点,但您应该能够找到如下相关数据集。在他们的网页上,找到数据库和数据集页面(应该可以从研究下拉菜单中找到),并跟随链接到营养数据实验室。在这个页面上,你可以找到一个到美国农业部国家营养数据库的链接,在那里你可以找到许多不同的纯文本(ASCII)格式的数据文件,这正是我们喜欢的方式。点击下载链接,下载标题为“缩写”的 ASCII 链接所引用的 zip 文件。您现在应该得到一个 zip 文件,其中包含一个名为ABBREV.txt
的文本文件,以及一个描述其内容的 PDF 文件。如果你很难找到这个特定的文件,任何旧的数据都可以。只需修改源代码以适应。
ABBREV.txt
文件中的数据每行有一个数据记录,各字段用脱字符号(^
)分隔。数值型字段直接包含数字,而文本型字段的字符串值用波浪符号(∼
)括起来。以下是一个示例行,为简洁起见删除了一些部分:
∼07276∼^∼HORMEL SPAM ... PORK W/ HAM MINCED CND∼^ ... ^∼1 serving∼^^∼∼⁰
将这样一行代码解析成单独的字段就像使用line.split('^')
一样简单。如果一个字段以波浪号开头,您知道它是一个字符串,可以使用field.strip('∼')
来获取它的内容。对于其他(数字)字段,float(field)
应该可以做到这一点,当然,当字段为空时除外。在下面几节中开发的程序将把这个 ASCII 文件中的数据转移到您的 SQL 数据库中,并让您对它们执行一些(半)有趣的查询。
Note
这个示例程序非常简单。关于 Python 中数据库使用的更高级的例子,参见第二十六章。
创建和填充表格
要实际创建数据库的表并填充它们,编写一个完全独立的一次性程序可能是最简单的解决方案。您可以运行这个程序一次,然后忘记它和原始数据源(ABBREV.txt
文件),尽管保留它们可能是个好主意。
清单 13-1 中所示的程序创建一个名为food
的表,其中包含一些适当的字段,读取文件ABBREV.txt
,解析它(通过拆分行并使用实用函数convert
转换各个字段),并在对curs.execute
的调用中使用 SQL INSERT
语句将从文本字段读取的值插入数据库。
Note
本来可以使用curs.executemany
,提供从数据文件中提取的所有行的列表。在这种情况下,这可能会带来较小的加速,但如果使用联网的客户机/服务器 SQL 系统,可能会带来更大的加速。
import sqlite3
def convert(value):
if value.startswith('∼'):
return value.strip('∼')
if not value:
value = '0'
return float(value)
conn = sqlite3.connect('food.db')
curs = conn.cursor()
curs.execute('''
CREATE TABLE food (
id TEXT PRIMARY KEY,
desc TEXT,
water FLOAT,
kcal FLOAT,
protein FLOAT,
fat FLOAT,
ash FLOAT,
carbs FLOAT,
fiber FLOAT,
sugar FLOAT
)
''')
query = 'INSERT INTO food VALUES (?,?,?,?,?,?,?,?,?,?)'
field_count = 10
for line in open('ABBREV.txt'):
fields = line.split('^')
vals = [convert(f) for f in fields[:field_count]]
curs.execute(query, vals)
conn.commit()
conn.close()
Listing 13-1.Importing Data into the Database (importdata.py)
Note
在清单 13-1 中,我使用了paramstyle
的“qmark”版本,即一个问号作为字段标记。如果您使用的是旧版本的 PySQLite,您可能需要使用%
字符来代替。
当你运行这个程序时(与ABBREV.txt
在同一个目录下),它会创建一个名为food.db
的新文件,包含数据库的所有数据。
我鼓励您使用这个例子,使用其他输入,添加print
语句,等等。
搜索和处理结果
使用数据库真的很简单。同样,您创建一个连接并从该连接中获取一个光标。使用execute
方法执行 SQL 查询,并使用例如fetchall
方法提取结果。清单 13-2 显示了一个小程序,它将 SQL SELECT
条件作为命令行参数,并以记录格式打印出返回的行。您可以使用如下命令行进行尝试:
$ python food_query.py "kcal <= 100 AND fiber >= 10 ORDER BY sugar"
运行这个程序时,您可能会注意到一个问题。第一排,生橘皮,好像一点糖都没有。这是因为数据文件中缺少该字段。您可以改进导入脚本来检测这种情况,并插入None
而不是一个实际值,以指示丢失的数据。那么您可以使用如下条件:
"kcal <= 100 AND fiber >= 10 AND sugar ORDER BY sugar"
这要求 sugar 字段在任何返回的行中都有真实数据。碰巧的是,这种策略也适用于当前数据库,在当前数据库中,这种条件将丢弃血糖水平为零的行。
您可能想尝试使用 ID 搜索特定食品的条件,比如用08323
搜索可可豆。问题是 SQLite 以一种相当不标准的方式处理它的值。实际上,在内部,所有的值都是字符串,并且在数据库和 Python API 之间进行一些转换和检查。通常,这很好,但是这是一个你可能会遇到麻烦的例子。如果您提供值08323
,它将被解释为数字8323
,并随后被转换为字符串"8323"
——一个不存在的 ID。人们可能已经预料到这里会出现一条错误消息,而不是这种令人惊讶且毫无帮助的行为,但是如果您小心谨慎,并且首先使用字符串"08323"
,您就不会有问题。
import sqlite3, sys
conn = sqlite3.connect('food.db')
curs = conn.cursor()
query = 'SELECT * FROM food WHERE ' + sys.argv[1]
print(query)
curs.execute(query)
names = [f[0] for f in curs.description]
for row in curs.fetchall():
for pair in zip(names, row):
print('{}: {}'.format(*pair))
print()
Listing 13-2.
Food Database Query Program
(food_query.py)
Caution
这个程序接受用户的输入,并将其拼接成一个 SQL 查询。只要用户是您,并且您没有输入任何奇怪的内容,这就没问题。然而,使用这种输入偷偷插入恶意 SQL 代码来扰乱数据库是破解计算机系统的一种常见方式,称为 SQL 注入。除非您知道自己在做什么,否则不要将您的数据库或任何其他东西暴露给原始用户输入。
快速总结
本章简要介绍了如何让 Python 程序与关系数据库进行交互。这很简单,因为如果你掌握了 Python 和 SQL,那么这两者之间的耦合,以 Python DB API 的形式,是很容易掌握的。以下是本章涉及的一些概念:
- Python DB API:这个 API 提供了一个简单的、标准化的接口,数据库包装器模块应该遵循这个接口,这样就可以更容易地编写适用于多种不同数据库的程序。
- connections:connection 对象表示与 SQL 数据库的通信链接。通过使用
cursor
方法,您可以从中获得单个光标。您还可以使用 connection 对象来提交或回滚事务。完成数据库后,可以关闭连接。 - 游标:游标用于执行查询和检查结果。可以一个接一个或一次检索多个(或全部)结果行。
- 类型和特殊值:DB API 指定一组构造函数和特殊值的名称。构造函数处理日期和时间对象,以及二进制数据对象。特殊值代表关系数据库的类型,如
STRING
、NUMBER
和DATETIME
。 - SQLite:这是一个小型的嵌入式 SQL 数据库,它的 Python 包装器包含在标准 Python 发行版中,名为
sqlite3
。它使用起来又快又简单,而且不需要设置单独的服务器。
本章的新功能
| 功能 | 描述 | | --- | --- | | `connect(...)` | 连接到一个数据库并返回一个连接对象 1 |什么现在?
持久性和数据库处理是许多(如果不是大多数)大型程序系统的重要部分。许多这样的系统共有的另一个组成部分是网络,这将在下一章中讨论。
Footnotes 1
connect
函数的参数取决于数据库。
十四、网络编程
在这一章中,我给出了 Python 可以帮助你编写程序的各种方式的一个例子,这些程序使用网络,比如互联网,作为一个重要的组成部分。Python 是一个非常强大的网络编程工具。有许多用于公共网络协议和它们之上的各种抽象层的库可用,因此您可以专注于程序的逻辑,而不是在线路上混排位。此外,很容易编写代码来处理可能没有现有代码的各种协议格式,因为 Python 非常擅长处理字节流中的模式(您已经在以各种方式处理文本文件中看到了这一点)。
因为 Python 有如此丰富的网络工具可供您使用,所以在这里我只能简单地介绍一下它的网络功能。你可以在这本书的其他地方找到一些例子。第十五章包含了对面向 web 的网络编程的讨论,后面章节中的几个项目使用网络模块来完成这项工作。如果您想了解更多关于 Python 中网络编程的知识,我可以热情地推荐 John Goerzen 的《Python 网络编程基础》( Apress,2004 ),该书非常透彻地论述了这个主题。
在本章中,我将向您概述 Python 标准库中可用的一些网络模块。接下来是对SocketServer
类和它的朋友们的讨论,接下来是对可以同时处理几个连接的各种方法的简要介绍。最后,我给大家看一下 Twisted 框架,这是一个用 Python 编写网络化应用的丰富而成熟的框架。
Note
如果你有一个严格的防火墙,它可能会在你开始运行自己的网络程序时发出警告,并阻止它们连接到网络。您应该配置您的防火墙,让您的 Python 完成它的工作,或者,如果防火墙有一个交互界面,当被询问时,简单地允许连接。但是,请注意,任何连接到网络的软件都有潜在的安全风险,即使(或者特别是如果)是您自己编写的软件。
一些网络模块
您可以在标准库中找到大量的网络模块,以及其他地方的更多模块。除了那些明显主要处理网络的模块之外,还有几个模块(例如那些处理各种形式的网络传输数据编码的模块)可以被看作是与网络相关的。在这里,我对模块的选择相当严格。
该插座模块
网络编程中的一个基本组件是套接字。套接字基本上是一个两端都有程序的“信息通道”。这些程序可能在不同的计算机上(通过网络连接),并且可以通过套接字相互发送信息。Python 中的大多数网络编程隐藏了socket
模块的基本工作方式,并且不直接与套接字交互。
套接字有两种:服务器套接字和客户端套接字。创建服务器套接字后,您告诉它等待连接。然后,它将监听某个网络地址(IP 地址和端口号的组合),直到客户端套接字连接。然后这两个人就可以交流了。
处理客户端套接字通常比处理服务器端要容易得多,因为无论客户端何时连接,服务器都必须准备好处理客户端,而且它必须处理多个连接,而客户端只是简单地连接、完成它的工作,然后断开连接。在本章的后面,我将通过SocketServer
类族和 Twisted 框架讨论服务器编程。
套接字是来自socket
模块的socket
类的实例。它用最多三个参数进行实例化:一个地址族(默认为socket.AF_INET
)、它是流(默认为socket.SOCK_STREAM
)还是数据报(socket.SOCK_DGRAM
)套接字,以及一个协议(默认为 0,应该没问题)。对于普通的套接字,您不需要提供任何参数。
服务器套接字使用它的bind
方法,然后调用listen
来监听给定的地址。然后,客户端套接字可以通过使用与在bind
中使用的地址相同的connect
方法连接到服务器。(例如,在服务器端,您可以使用socket.gethostname
功能获取当前机器的名称。)在这种情况下,地址只是一个形式为(host, port)
的元组,其中host
是主机名(如www.example.com
)port
是端口号(整数)。listen
方法接受一个参数,即其 backlog 的长度(在连接开始被禁止之前,允许排队等待接受的连接数)。
一旦服务器套接字正在侦听,它就可以开始接受客户端。这是使用accept
方法完成的。这个方法将阻塞(等待)直到客户端连接,然后它将返回一个形式为(client, address)
的元组,其中client
是一个客户端套接字,address
是一个地址,如前所述。服务器可以以它认为合适的方式处理客户端,然后开始等待新的连接,并再次调用accept
。这通常是在无限循环中完成的。
Note
这里讨论的服务器编程形式称为阻塞式或同步网络编程。在本章后面的“多重连接”一节中,您将看到非阻塞或异步网络编程的例子,以及使用线程同时处理几个客户机的例子。
对于传输数据,套接字有两种方法:send
和recv
(对于“接收”)。您可以用一个字符串参数调用send
来发送数据,用一个期望的(最大)字节数调用recv
来接收数据。如果你不确定用哪个数字,1024 是最好的选择。
清单 14-1 和 14-2 展示了一个非常简单的客户机/服务器对的例子。如果在同一台机器上运行它们(首先启动服务器),服务器应该打印出一条关于获得连接的消息,然后客户机应该打印出一条从服务器收到的消息。当服务器仍在运行时,您可以运行多个客户端。通过将客户机中对gethostname
的调用替换为运行服务器的机器的实际主机名,您可以让两个程序通过网络从一台机器连接到另一台机器。
Note
您使用的端口号通常受到限制。在 Linux 或 UNIX 系统中,您需要管理员权限才能使用 1024 以下的端口。这些编号较低的端口用于标准服务,例如 web 服务器的端口 80(如果有)。此外,例如,如果您使用 Ctrl+C 停止服务器,您可能需要等待一段时间才能再次使用相同的端口号(您可能会得到“地址已在使用中”错误)。
import socket
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
s.listen(5)
while True:
c, addr = s.accept()
print('Got connection from', addr)
c.send('Thank you for connecting')
c.close()
Listing 14-1.A Minimal Server
import socket
s = socket.socket()
host = socket.gethostname()
port = 1234
s.connect((host, port))
print(s.recv(1024))
Listing 14-2.A Minimal Client
你可以在 Python 库参考和 Gordon McMillan 的 Socket 编程 HOWTO ( http://docs.python.org/dev/howto/sockets.html
)中找到更多关于socket
模块的信息。
urllib 和 urllib2 模块
在可用的网络库中,可能最划算的是urllib
和urllib2
。它们使您能够通过网络访问文件,就像它们位于您的计算机上一样。通过一个简单的函数调用,几乎任何可以用统一资源定位器(URL)引用的东西都可以用作程序的输入。想象一下,如果你把这个和re
模块结合起来,你会得到什么样的可能性:你可以下载网页,提取信息,并为你的发现创建自动报告。
这两个模块做的工作或多或少是一样的,只是urllib2
有点“花哨”对于简单的下载,urllib
是完全可以的。如果你需要 HTTP 认证或 cookies,或者你想编写扩展来处理你自己的协议,那么urllib2
可能是你的正确选择。
打开远程文件
您可以像打开本地文件一样打开远程文件;不同的是,你只能使用读取模式,而不是open
(或file
),你使用来自urllib.request
模块的urlopen
。
>>> from urllib.request import urlopen
>>> webpage = urlopen('http://www.python.org')
如果您在线,变量webpage
现在应该包含一个类似文件的对象,链接到位于 http://www.python.org
的 Python 网页。
Note
如果你想尝试使用urllib
,但目前不在线,你可以使用以file:
开头的 URL 访问本地文件,比如file:c:\text\somefile.txt
。(记得躲开你的反斜杠。)
从urlopen
返回的类似文件的对象支持close
、read
、readline
和readlines
方法,以及迭代。
假设您想要提取刚刚打开的 Python 页面上“About”链接的(相对)URL。你可以用正则表达式做到这一点(关于正则表达式的更多信息,参见第十章中关于re
模块的部分)。
>>> import re
>>> text = webpage.read()
>>> m = re.search(b'<a href="([^"]+)" .*?>about</a>', text, re.IGNORECASE)
>>> m.group(1)
'/about/'
Note
当然,如果 web 页面在编写之后发生了变化,您可能需要修改正则表达式。
检索远程文件
urlopen
函数给了你一个可以读取的类似文件的对象。如果你想让urllib
帮你下载文件,在本地文件中保存一份拷贝,你可以用urlretrieve
来代替。它不是返回一个类似文件的对象,而是返回一个元组(filename, headers)
,其中filename
是本地文件的名称(这个名称是由urllib
自动创建的),而headers
包含一些关于远程文件的信息。(这里我就忽略headers
;如果你想了解更多,请查阅urllib
的标准库文档。)如果您想为下载的副本指定一个文件名,您可以将它作为第二个参数提供。
urlretrieve('http://www.python.org', 'C:\\python_webpage.html')
这将检索 Python 主页并将其存储在文件C:\python_webpage.html
中。如果不指定文件名,文件会被放在某个临时位置,供您打开(使用open
函数),但是当您使用完它时,您可能希望将其删除,这样它就不会占用您的硬盘空间。要清理这样的临时文件,您可以不带任何参数地调用函数urlcleanup
,它会为您处理好一切。
Some Utilities
除了通过 URL 读取和下载文件,urllib
还提供了一些操作 URL 本身的功能。(以下假设对 URL 和 CGI 有一定的了解。)以下功能可用:
quote(string[, safe])
:返回一个字符串,其中所有的特殊字符(在 URL 中有特殊意义的字符)都被 URL 友好的版本所替换(比如用%7E
代替∼
)。如果您有一个可能包含此类特殊字符的字符串,并且您想将它用作 URL,这将非常有用。安全字符串包含不应该这样编码的字符。默认为'/'
。quote_plus(string[, safe])
:类似于引号,但也用加号替换空格。unquote(string)
:与quote
相反。unquote_plus(string)
:与quote_plus
相反。urlencode(query[, doseq])
:将一个映射(如字典)或一个二元元组序列(形式为(key,value))转换为“URL 编码的”字符串,该字符串可用于 CGI 查询。(有关更多信息,请查看 Python 文档。)
其他模块
如上所述,除了本章明确讨论的模块,Python 库中和其他地方还有大量与网络相关的模块。表 14-1 列出了 Python 标准库中的一些网络相关模块。如表中所示,其中一些模块在本书的其他地方讨论过。
表 14-1。
Some Network-Related Modules in the Standard Library
| 组件 | 描述 | | --- | --- | | `asynchat` | `asyncore`的附加功能(参见第二十四章) | | `asyncore` | 异步套接字处理程序(参见第二十四章 | | `cgi` | 基本 CGI 支持(参见第十五章 | | `Cookie` | Cookie 对象操作,主要用于服务器 | | `cookielib` | 客户端 cookie 支持 | | `email` | 支持电子邮件(包括 MIME) | | `ftplib` | FTP 客户端模块 | | `gopherlib` | Gopher 客户端模块 | | `httplib` | HTTP 客户端模块 | | `imaplib` | IMAP4 客户端模块 | | `mailbox` | 阅读几种邮箱格式 | | `mailcap` | 通过 mailcap 文件访问 MIME 配置 | | `mhlib` | 访问 MH 邮箱 | | `nntplib` | NNTP 客户端模块(参见第二十三章 | | `poplib` | POP 客户端模块 | | `robotparser` | 支持解析 web 服务器机器人文件 | | `SimpleXMLRPCServer` | 一个简单的 XML-RPC 服务器(见第二十七章) | | `smtpd` | SMTP 服务器模块 | | `smtplib` | SMTP 客户端模块 | | `telnetlib` | Telnet 客户端模块 | | `urlparse` | 支持解释 URL | | `xmlrpclib` | XML-RPC 的客户端支持(参见第二十七章 |SocketServer 和朋友
正如您在前面关于socket
模块的章节中看到的,编写一个简单的套接字服务器并不困难。然而,如果你想超越基础,获得一些帮助会很好。SocketServer
模块是标准库中几个服务器框架的基础,包括BaseHTTPServer
、SimpleHTTPServer
、CGIHTTPServer
、SimpleXMLRPCServer
和DocXMLRPCServer
,所有这些都为基础服务器增加了各种特定的功能。
SocketServer
包含四个基本类:TCPServer
,用于 TCP 套接字流;UDPServer
,用于 UDP 数据报套接字;还有更隐晦的UnixStreamServer
和UnixDatagramServer
。你可能不需要最后三个。
要使用SocketServer
框架编写服务器,您需要将大部分代码放在请求处理程序中。每当服务器收到一个请求(来自客户机的连接)时,就会实例化一个请求处理程序,并在其上调用各种处理程序方法来处理请求。具体调用哪些方法取决于所使用的特定服务器和处理程序类,您可以对它们进行子类化,使服务器调用一组自定义的处理程序。基本的BaseRequestHandler
类将所有的动作放在处理程序的一个方法中,这个方法叫做handle
,由服务器调用。然后,这个方法可以访问属性self.request
中的客户端套接字。如果您正在处理一个流(如果您使用TCPServer
,您可能就是这样),您可以使用类StreamRequestHandler
,它设置了另外两个属性self.rfile
(用于读取)和self.wfile
(用于写入)。然后,您可以使用这些类似文件的对象与客户端进行通信。
在SocketServer
框架中的各种其他类实现了对 HTTP 服务器的基本支持,包括运行 CGI 脚本,以及对 XML-RPC 的支持(在第二十七章中讨论)。
清单 14-3 给出了清单 14-1 中最小服务器的SocketServer
版本。它可以与清单 14-2 中的客户端一起使用。请注意,StreamRequestHandler
负责在连接被处理后关闭连接。还要注意,将''
作为主机名意味着您指的是运行服务器的机器。
from socketserver import TCPServer, StreamRequestHandler
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('Got connection from', addr)
self.wfile.write('Thank you for connecting')
server = TCPServer(('', 1234), Handler)
server.serve_forever()
Listing 14-3.A SocketServer-Based Minimal Server
您可以在《Python 库参考》和 John Goerzen 的《Python 网络编程基础》(Apress,2004)中找到关于SocketServer
框架的更多信息。
多重连接
到目前为止讨论的服务器解决方案都是同步的:一次只有一个客户机可以连接并处理它的请求。如果一个请求需要一点时间,比如一个完整的聊天会话,重要的是可以同时处理多个连接。
您可以通过三种主要方式处理多个连接:分叉、线程化和异步 I/O。分叉和线程化可以非常简单地处理,通过使用任何SocketServer
服务器的混合类(参见清单 14-4 和 14-5 )。即使您想自己实现它们,这些方法也很容易使用。然而,它们也有缺点。分叉会占用资源,如果您有许多客户端,可能无法很好地伸缩(尽管对于合理数量的客户端,在现代的 UNIX 或 Linux 系统上,分叉是非常有效的,如果您有一个多 CPU 系统,甚至会更有效)。线程可能会导致同步问题。在这里,我不会详细讨论这些问题(也不会深入讨论多线程),但是在接下来的几节中,我将向您展示如何使用这些技术。
Forks? Threads? What’s All This, Then?
万一你不知道分叉或线程,这里有一点澄清。分叉是一个 UNIX 术语。当您派生一个进程(一个正在运行的程序)时,您基本上复制了它,两个结果进程都从当前执行点继续运行,每个进程都有自己的内存副本(变量等)。一个进程(原始进程)将是父进程,而另一个进程(副本)将是子进程。如果你是科幻迷,你可能会想到平行宇宙;分叉操作在时间线中创建了一个分叉,最终你会得到两个独立存在的宇宙(两个过程)。幸运的是,这些进程能够确定它们是原始进程还是子进程(通过查看 fork 函数的返回值),因此它们可以采取不同的行动。(如果他们不能,那还有什么意义呢?这两个进程会做同样的工作,你只会让你的计算机陷入困境。)
在分叉服务器中,每个客户端连接都有一个子连接被分叉。父进程继续监听新的连接,而子进程处理客户端。当客户端满意时,子进程简单地退出。因为分叉的进程是并行运行的,所以客户端不需要相互等待。
因为分叉可能有点资源密集(每个分叉的进程都需要自己的内存),所以存在一种替代方法:线程化。线程是轻量级进程或子进程,它们都存在于同一个(真正的)进程中,共享相同的内存。不过,这种资源消耗的减少也带来了负面影响。因为线程共享内存,所以您必须确保它们不会互相干扰变量,或者试图同时修改相同的内容,从而造成混乱。这些问题属于“同步”的范畴有了现代的操作系统(除了微软 Windows,不支持 forking),forking 其实挺快的,现代的硬件对资源消耗的处理也比以前好很多。如果您不想为同步问题而烦恼,那么分叉可能是一个不错的选择。
然而,最好的办法可能是完全避免这种并行性。在本章中,您将找到基于选择功能的其他解决方案。另一种避免线程和分叉的方法是切换到无栈 Python ( http://stackless.com
),这是一个 Python 版本,旨在能够快速、无痛地在不同上下文之间切换。它支持一种类似线程的并行形式,称为微线程,比真正的线程伸缩性好得多。比如 EVE Online ( http://www.eve-online.com
)中已经使用了无栈 Python 微线程来服务成千上万的用户。
异步 I/O 在底层实现起来有点困难。基本机制是select
模块的select
函数(在“带选择和轮询的异步 I/O”一节中有描述),这个函数很难处理。幸运的是,已经存在在更高层次上处理异步 I/O 的框架,为您提供了一个简单、抽象的接口来实现一个非常强大且可伸缩的机制。包含在标准库中的这种基本框架由第二十四章中讨论的asyncore
和asynchat
模块组成。Twisted(本章最后讨论)是一个非常强大的异步网络编程框架。
使用 SocketServer 进行分叉和线程处理
用SocketServer
框架创建分叉或线程服务器是如此简单,几乎不需要任何解释。清单 14-4 和 14-5 分别向您展示了如何从清单 14-3 分叉和线程化制作服务器。仅当handle
方法需要很长时间才能完成时,分叉或线程行为才有用。请注意,分叉在 Windows 中不起作用。
from socketserver import TCPServer, ForkingMixIn, StreamRequestHandler
class Server(ForkingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('Got connection from', addr)
self.wfile.write('Thank you for connecting')
server = Server(('', 1234), Handler)
server.serve_forever()
Listing 14-4.A Forking Server
from socketserver import TCPServer, ThreadingMixIn, StreamRequestHandler
class Server(ThreadingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('Got connection from', addr)
self.wfile.write('Thank you for connecting')
server = Server(('', 1234), Handler)
server.serve_forever()
Listing 14-5.A Threading Server
带选择和轮询的异步 I/O
当服务器与客户机通信时,它从客户机接收的数据可能是断断续续的。如果你使用分叉和线程,这不是一个问题。当一个 parallel 等待数据时,其他 parallel 可能会继续处理自己的客户端。然而,另一种方法是只与在特定时刻确实有话要说的客户打交道。你甚至不需要听完它们——你只需要听一点点(或者说,读一点点),然后把它放回和其他的一样。
这是框架asyncore
/ asynchat
(参见第章 24 )和 Twisted(参见下一节)所采用的方法。这种功能的基础是select
函数或poll
函数,两者都来自select
模块。两者中,poll
的可伸缩性更强,但它只在 UNIX 系统中可用(即在 Windows 中不可用)。
select
函数将三个序列作为它的强制参数,将一个以秒为单位的可选超时作为它的第四个参数。这些序列是文件描述符整数(或带有返回这种整数的fileno
方法的对象)。这些是我们正在等待的联系。这三个序列分别用于输入、输出和异常情况(错误等)。如果没有给出超时,select
会阻塞(即等待),直到其中一个文件描述符准备好执行操作。如果给定了超时,select
最多阻塞那么多秒,0 表示直接轮询(即没有阻塞)。select
返回三个序列(一个三元组,即长度为三的元组),每个序列代表对应参数的一个活动子集。例如,返回的第一个序列将是一个输入文件描述符序列,其中有要读取的内容。
例如,序列可以包含文件对象(不在 Windows 中)或套接字。清单 14-6 显示了一个服务器使用select
来服务几个连接。(注意,服务器套接字本身被提供给select
,以便它可以在有新的连接准备好被接受时发出信号。)服务器是一个简单的记录器,它(在本地)打印出从其客户机接收到的所有数据。您可以通过使用 telnet 连接到它来测试它(或者通过编写一个简单的基于套接字的客户机来提供一些数据)。尝试用多个 telnet 连接进行连接,看看它是否可以同时为多个客户端提供服务(尽管它的日志将是来自两个客户端的输入的混合)。
import socket, select
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
s.listen(5)
inputs = [s]
while True:
rs, ws, es = select.select(inputs, [], [])
for r in rs:
if r is s:
c, addr = s.accept()
print('Got connection from', addr)
inputs.append(c)
else:
try:
data = r.recv(1024)
disconnected = not data
except socket.error:
disconnected = True
if disconnected:
print(r.getpeername(), 'disconnected')
inputs.remove(r)
else:
print(data)
Listing 14-6.A Simple Server Using select
poll
方法比select
更容易使用。当您调用poll
时,您会得到一个投票对象。然后,您可以使用 poll 对象的register
方法向其注册文件描述符(或使用fileno
方法的对象)。您可以稍后使用unregister
方法再次移除这些对象。一旦您注册了一些对象(例如,套接字),您就可以调用poll
方法(带有一个可选的超时参数)并获得一个形式为(fd, event)
的成对列表(可能为空),其中fd
是文件描述符,event
告诉您发生了什么。这是一个位掩码,意味着它是一个整数,其中各个位对应于各种事件。各种事件是select
模块的常量,在表 14-2 中有解释。要检查给定的位是否置位(即,给定的事件是否发生),可以使用按位 and 运算符(&
),如下所示:
表 14-2。
Polling Event Constants in the select Module
| 事件名称 | 描述 | | --- | --- | | `POLLIN` | 从文件描述符中可以读取数据。 | | `POLLPRI` | 有紧急数据要从文件描述符中读取。 | | `POLLOUT` | 文件描述符已为数据做好准备,如果写入,将不会阻塞。 | | `POLLERR` | 一些错误条件与文件描述符相关联。 | | `POLLHUP` | 挂了。连接已经断开。 | | `POLLNVAL` | 无效请求。连接没有打开。 |if event & select.POLLIN: ...
清单 14-7 中的程序是对清单 14-6 中服务器的重写,现在使用poll
而不是select
。注意,我已经添加了一个从文件描述符(ints
)到套接字对象的映射(fdmap
)。
import socket, select
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
fdmap = {s.fileno(): s}
s.listen(5)
p = select.poll()
p.register(s)
while True:
events = p.poll()
for fd, event in events:
if fd in fdmap:
c, addr = s.accept()
print('Got connection from', addr)
p.register(c)
fdmap[c.fileno()] = c
elif event & select.POLLIN:
data = fdmap[fd].recv(1024)
if not data: # No data -- connection closed
print(fdmap[fd].getpeername(), 'disconnected')
p.unregister(fd)
del fdmap[fd]
else:
print(data)
Listing 14-7.A Simple Server Using poll
你可以在 Python 库参考( http://python.org/doc/lib/module-select.html
)中找到更多关于select
和poll
的信息。此外,阅读标准库模块asyncore
和asynchat
的源代码(可以在 Python 安装的asyncore.py
和asynchat.py
文件中找到)可能会有所启发。
扭曲的
Twisted,来自 Twisted Matrix Laboratories(http://twistedmatrix.com
),是 Python 的一个事件驱动的网络框架,最初是为网络游戏开发的,现在被各种网络软件使用。在 Twisted 中,你实现事件处理程序,就像你在 GUI 工具包中做的一样(见第十二章)。事实上,Twisted 与几种常见的 GUI 工具包(Tk、GTK、Qt 和 wxWidgets)配合得非常好。在这一节中,我将介绍一些基本概念,并向您展示如何使用 Twisted 进行一些相对简单的网络编程。一旦掌握了基本概念,您就可以查阅 Twisted 文档(在 Twisted 网站上可以找到,还有相当多的其他信息)来进行一些更严肃的网络编程。Twisted 是一个非常丰富的框架,支持 web 服务器和客户端、SSH2、SMTP、POP3、IMAP4、AIM、ICQ、IRC、MSN、Jabber、NNTP、DNS 等等。
Note
在撰写本文时,Twisted 的全部功能仅在 Python 2 中可用,尽管该框架越来越多的部分正在被移植到 Python 3。本节剩余部分中的代码示例使用 Python 2.7。
下载和安装 Twisted
安装 Twisted 相当容易。首先,去扭曲矩阵网站( http://twistedmatrix.com
),从那里,跟随其中一个下载链接。如果您使用的是 Windows,请下载适用于您的 Python 版本的 Windows 安装程序。如果您使用的是其他系统,请下载一个源文件。(如果你用的是 Portage、RPM、APT、Fink 或者 MacPorts 之类的包管理器,你大概可以让它直接下载安装 Twisted。)Windows installer 是一个不言自明的分步向导。编译和解包可能需要一些时间,但你所要做的就是等待。要安装源归档文件,首先要解压它(使用tar
,然后使用gunzip
或bunzip2
,这取决于您下载的归档文件的类型),然后运行 Distutils 脚本。
python setup.py install
然后,您应该能够使用 Twisted。
编写扭曲的服务器
本章前面写的基本套接字服务器非常清楚。其中一些有明确的事件循环,寻找新的连接和新的数据。基于SocketServer
的服务器有一个隐式的循环,其中服务器寻找连接并为每个连接创建一个处理程序,但是处理程序仍然必须明确地尝试读取数据。Twisted(像第asyncore
/ asynchat
框架,在第二十四章中讨论)使用了一种更加基于事件的方法。要编写一个基本的服务器,您需要实现事件处理程序来处理诸如新客户端连接、新数据到达、客户端断开连接(以及许多其他事件)等情况。专门化的类可以在基本事件的基础上构建更精细的事件,比如包装“数据到达”事件,收集数据直到发现一个新行,然后分派一个“数据行到达”事件。
Note
有一件事我在本节中没有涉及,但却是 Twisted 的一个特征,那就是延迟和延迟执行的概念。有关更多信息,请参见 Twisted 文档(例如,可以从 Twisted 文档的 HOWTO 页面获得名为“延迟是美好的”的教程)。
您的事件处理程序是在协议中定义的。您还需要一个工厂,当新连接到达时,它可以构造这样的协议对象。如果您只想创建自定义协议类的实例,可以使用 Twisted 附带的工厂,即模块twisted.internet.protocol
中的Factory
类。当你写你的协议时,使用和你的超类来自同一个模块的Protocol
。当您获得一个连接时,事件处理程序connectionMade
被调用。当你失去一个连接,connectionLost
就会被调用。通过处理器dataReceived
从客户端接收数据。当然,您不能使用事件处理策略将数据发送回客户端——为此,您可以使用具有write
方法的对象self.transport
。它还有一个client
属性,包含客户端地址(主机名和端口)。
清单 14-8 包含清单 14-6 和 14-7 中服务器的扭曲版本。我希望你同意扭曲的版本更简单,可读性更好。这涉及到一点点设置;您需要实例化Factory
并设置它的protocol
属性,以便它知道在与客户端通信时使用哪个协议(即您的自定义协议)。
然后,您开始在一个给定的端口监听,该工厂通过实例化协议对象来处理连接。您可以使用reactor
模块中的listenTCP
函数来完成此操作。最后,通过从同一个模块调用run
函数来启动服务器。
from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory
class SimpleLogger(Protocol):
def connectionMade(self):
print('Got connection from', self.transport.client)
def connectionLost(self, reason):
print(self.transport.client, 'disconnected')
def dataReceived(self, data):
print(data)
factory = Factory()
factory.protocol = SimpleLogger
reactor.listenTCP(1234, factory)
reactor.run()
Listing 14-8.A Simple Server Using Twisted
如果您使用 telnet 连接到该服务器进行测试,您可能会在每行输出中得到一个字符,这取决于缓冲等。您可以简单地使用sys.sout.write
而不是print
,但是在许多情况下,您可能希望一次得到一行,而不仅仅是任意的数据。为您编写一个处理这个问题的自定义协议会非常容易,但是事实上,已经有这样一个类可用了。模块twisted.protocols.basic
包含几个有用的预定义协议,其中有LineReceiver
。每当接收到一整行时,它实现dataReceived
并调用事件处理程序lineReceived
。
Tip
如果在接收数据时除了使用lineReceived
之外还需要做一些事情,这依赖于dataReceived
的LineReceiver
实现,那么可以使用由LineReceiver
定义的新事件处理程序rawDataReceived
。
切换协议只需要最少的工作。清单 14-9 显示了结果。如果您在运行这个服务器时查看结果输出,您会看到换行符被去掉了;换句话说,使用print
不会再给你双换行符。
from twisted.internet import reactor
from twisted.internet.protocol import Factory
from twisted.protocols.basic import LineReceiver
class SimpleLogger(LineReceiver):
def connectionMade(self):
print('Got connection from', self.transport.client)
def connectionLost(self, reason):
print(self.transport.client, 'disconnected')
def lineReceived(self, line):
print(line)
factory = Factory()
factory.protocol = SimpleLogger
reactor.listenTCP(1234, factory)
reactor.run()
Listing 14-9.An Improved Logging Server, Using the LineReceiver Protocol
正如前面提到的,Twisted 框架有比我在这里展示的更多的东西。如果你有兴趣了解更多,你应该查看在线文档,可以在 Twisted 网站( http://twistedmatrix.com
)获得。
快速总结
这一章已经让你体验了几种用 Python 进行网络编程的方法。您选择哪种方法将取决于您的特定需求和偏好。一旦你做出选择,你很可能需要学习更多关于具体方法的知识。以下是本章涉及的一些主题:
- 套接字和套接字模块:套接字是让程序(进程)可以通过网络进行通信的信息通道。
socket
模块提供了对客户机和服务器套接字的低级访问。服务器套接字在给定的地址监听客户端连接,而客户端只是直接连接。 - urllib 和 urllib2:这些模块允许您从各种服务器读取和下载数据,给定数据源的 URL。
urllib
模块是一个更简单的实现,而urllib2
是非常可扩展和强大的。两者都通过简单的函数工作,比如urlopen
。 - SocketServer 框架:这是一个由同步服务器基类组成的网络,可以在标准库中找到,它让您可以非常容易地编写服务器。甚至还支持带有 CGI 的简单 web (HTTP)服务器。如果你想同时处理几个连接,你需要使用一个分叉或者线程混合类。
- select 和 poll:这两个函数让您考虑一组连接,并找出哪些可以读写。这意味着您可以以循环的方式逐个为几个连接提供服务。这给人一种同时处理几个连接的错觉,尽管表面上代码有点复杂,但这是一个比线程或分叉更具可扩展性和效率的解决方案。
- Twisted:这个框架来自 Twisted Matrix Laboratories,非常丰富和复杂,支持大多数主要的网络协议。尽管它很大,使用的一些习惯用法可能看起来有点陌生,但基本用法非常简单和直观。Twisted 框架也是异步的,所以它非常高效和可伸缩。如果您有 Twisted,它可能是许多定制网络应用的最佳选择。
本章的新功能
| 功能 | 描述 | | --- | --- | | `urllib.urlopen(url[, data[, proxies]])` | 从 URL 打开类似文件的对象 | | `urllib.urlretrieve(url[, fname[, hook[, data]]])` | 从 URL 下载文件 | | `urllib.quote(string[, safe])` | 引用特殊的 URL 字符 | | `urllib.quote_plus(string[, safe])` | 与`quote`相同,但将空格引为`+` | | `urllib.unquote(string)` | `quote`的反面 | | `urllib.unquote_plus(string)` | `quote_plus`的反面 | | `urllib.urlencode(query[, doseq])` | 编码用于 CGI 查询的映射 | | `select.select(iseq, oseq, eseq[, timeout])` | 找到准备好读/写的套接字 | | `select.poll()` | 为轮询套接字创建一个轮询对象 | | `reactor.listenTCP(port, factory)` | 扭曲函数;监听连接 | | `reactor.run()` | 扭曲函数;主服务器循环 |什么现在?
你以为我们已经完成了网络的工作,是吗?没门儿。下一章讨论的是网络世界中一个非常专业且广为人知的实体:网络。
十五、Python 和 Web
这一章讲述了用 Python 进行 web 编程的一些方面。这是一个非常广阔的领域,但是我选择了三个主要的主题供你娱乐:屏幕抓取、CGI 和 mod_python。
此外,我还会为您提供一些指导,帮助您找到更高级的 web 应用和 web 服务开发的合适工具包。有关使用 CGI 的扩展示例,请参见第 25 和 26 章。有关使用特定 web 服务协议 XML-RPC 的示例,请参见第二十七章。
屏幕抓取
屏幕抓取是程序下载网页并从中提取信息的过程。这是一种有用的技术,只要在线页面上有您希望在程序中使用的信息,就可以使用这种技术。当然,如果所讨论的网页是动态的,也就是说,如果它随时间而变化,那么它就特别有用。否则,您可以只下载一次,然后手动提取信息。(当然,理想的情况是通过 web 服务获得信息,这将在本章后面讨论。)
从概念上讲,这种技术非常简单。你下载数据并分析它。例如,你可以简单地使用urllib
,获取网页的 HTML 源代码,然后使用正则表达式(见第十章)或其他技术来提取信息。举例来说,假设您想从 Python Job Board 的 http://python.org/jobs
中提取各种雇主名称和网站。您浏览源代码,发现名称和 URL 可以像这样的链接找到:
<a href="/jobs/1970/">Python Engineer</a>
清单 15-1 显示了一个使用urllib
和re
提取所需信息的示例程序。
from urllib.request import urlopen
import re
p = re.compile(’<a href="(/jobs/\\d+)/">(.*?)</a>’)
text = urlopen(’http://python.org/jobs’).read().decode()
for url, name in p.findall(text):
print(’{} ({})’.format(name, url))
Listing 15-1.A Simple Screen-Scraping Program
代码当然可以改进,但它做得很好。然而,这种方法至少有三个缺点。
- 正则表达式不完全可读。对于更复杂的 HTML 代码和更复杂的查询,表达式会变得更复杂,更难维护。
- 它不处理 HTML 的特性,比如 CDATA 部分和字符实体(比如
&
)。如果你遇到这样的野兽,程序将很有可能失败。 - 正则表达式依赖于 HTML 源代码中的细节,而不是一些更抽象的结构。这意味着网页结构的微小变化都会破坏程序。(当你读到这里的时候,它可能已经坏了。)
下面几节讨论了基于正则表达式的方法所带来的问题的两种可能的解决方案。第一种是使用一个名为 Tidy 的程序(作为一个 Python 库)和 XHTML 解析。第二种是用一个叫美汤的库,专门用来刮屏的。
Note
使用 Python 还有其他的屏幕抓取工具。举例来说,你可能会想看看贾平凹的scrape.py
(在 http://zesty.ca/python
找到的)。
Tidy 和 XHTML 解析
Python 标准库为解析 HTML 和 XML 等结构化格式提供了大量支持(参见 Python 库参考的“结构化标记处理工具”一节)。我将在第二十二章中更深入地讨论 XML 和 XML 解析。在这一节中,我只向您提供处理 XHTML 所需的工具,XHTML 是 HTML 5 规范描述的两种具体语法之一,它恰好是 XML 的一种形式。所描述的大部分内容应该同样适用于普通的 HTML。
如果每个 web 页面都包含正确有效的 XHTML,那么解析它的工作将会非常简单。问题是更老的 HTML 方言有点草率,有些人甚至不在乎那些草率方言的苛责。原因可能是,大多数 web 浏览器都相当宽容,会尽可能地呈现哪怕是最混乱、最无意义的 HTML。如果页面作者认为这是可以接受的,他们可能会感到满意。不过,这确实让屏幕抓取工作变得更加困难。
在标准库中解析 HTML 的一般方法是基于事件的;您可以编写事件处理程序,在解析器处理数据时调用它们。标准库模块html.parser
将允许您以这种方式解析非常松散的 HTML,但是如果您想要基于文档结构提取数据(例如第二个二级标题之后的第一项),您将需要做一些大量的猜测,例如是否有丢失的标签。如果你愿意,当然欢迎你这样做,但是还有另一种方法:整洁。
什么是整洁?
Tidy 是一个用于修复格式不良和松散的 HTML 的工具。它可以以一种相当智能的方式修复一系列常见错误,完成许多您可能不愿意自己做的工作。它也是非常可配置的,允许你打开或关闭各种修正。
这是一个充满错误的 HTML 文件的例子,其中一些只是老式的 HTML,还有一些是完全错误的(你能发现所有的问题吗?):
<h1>Pet Shop
<h2>Complaints</h3>
<p>There is <b>no <i>way</b> at all</i> we can accept returned
parrots.
<h1><i>Dead Pets</h1>
<p>Our pets may tend to rest at times, but rarely die within the
warranty period.
<i><h2>News</h2></i>
<p>We have just received <b>a really nice parrot.
<p>It’s really nice.</b>
<h3><hr>The Norwegian Blue</h3>
<h4>Plumage and <hr>pining behavior</h4>
<a href="#norwegian-blue">More information<a>
<p>Features:
<body>
<li>Beautiful plumage
以下是 Tidy 修复的版本:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<h1>Pet Shop</h1>
<h2>Complaints</h2>
<p>There is <b>no <i>way</i></b> <i>at all</i> we can accept
returned parrots.</p>
<h1><i>Dead Pets</i></h1>
<p><i>Our pets may tend to rest at times, but rarely die within the
warranty period.</i></p>
<h2><i>News</i></h2>
<p>We have just received <b>a really nice parrot.</b></p>
<p><b>It’s really nice.</b></p>
<hr>
<h3>The Norwegian Blue</h3>
<h4>Plumage and</h4>
<hr>
<h4>pining behavior</h4>
<a href="#norwegian-blue">More information</a>
<p>Features:</p>
<ul>
<li>Beautiful plumage</li>
</ul>
</body>
</html>
当然,Tidy 不能解决一个 HTML 文件的所有问题,但是它确实确保了它的格式良好(也就是说,所有元素都正确嵌套),这使得解析它变得容易得多。
变得整洁
Tidy 库有几个 Python 包装器,哪一个是最新的看起来有点不同。如果您正在使用 pip,您可以使用以下命令查看您的选项:
$ pip search tidy
PyTidyLib 是一个不错的选择,您可以按如下方式安装它:
$ pip install pytidylib
不过,您不必为这个库安装包装器。如果您运行的是某种 UNIX 或 Linux 机器,那么您很可能有 Tidy 的命令行版本。不管你用的是什么操作系统,你都可以从 Tidy 网站( http://html-tidy.org
)获得一个可执行的二进制文件。一旦你有了二进制版本,你就可以使用subprocess
模块(或者一些popen
函数)来运行 Tidy 程序。例如,假设您有一个名为messy.html
的混乱的 HTML 文件,并且您的执行路径中有 Tidy 的命令行版本,下面的程序将在其上运行 Tidy 并打印结果:
from subprocess import Popen, PIPE
text = open(’messy.html’).read()
tidy = Popen(’tidy’, stdin=PIPE, stdout=PIPE, stderr=PIPE)
tidy.stdin.write(text.encode())
tidy.stdin.close()
print(tidy.stdout.read().decode())
如果Popen
找不到tidy
,您可能想要提供可执行文件的完整路径。
实际上,您很可能会从中提取一些有用的信息,而不是打印结果,如下面几节所示。
但是为什么是 XHTML 呢?
XHTML 和旧形式 HTML 的主要区别(至少对于我们当前的目的来说)是,XHTML 对显式关闭所有元素非常严格。所以在 HTML 中,你可以通过开始另一个段落来结束一个段落(用一个<p>
标签),但是在 XHTML 中,你首先需要明确地结束一个段落(用一个</p>
标签)。这使得 XHTML 更容易解析,因为您可以直接判断何时进入或离开各种元素。XHTML 的另一个优势(在本章中我不会充分利用)是它是一种 XML 方言,所以你可以在它上面使用各种漂亮的 XML 工具,比如 XPath。(关于 XML 的更多信息,请参见第二十二章;有关 XPath 用法的更多信息,请参见例如 http://www.w3schools.com/xml/xml:xpath.asp
。)
解析从 Tidy 获得的良好 XHTML 的一个非常简单的方法是使用标准库模块html.
parser
中的HTMLParser
类。
使用 HTMLParser
使用HTMLParser
仅仅意味着对它进行子类化,并覆盖各种事件处理方法,如handle_starttag
和handle_data
。表 15-1 总结了相关的方法以及它们何时被解析器(自动)调用。
表 15-1。
The HTMLParser Callback Methods
| 回调方法 | 什么时候叫? | | --- | --- | | `handle_starttag(tag, attrs)` | 当发现一个开始标签时,`attrs`是一个由`(name, value)`对组成的序列。 | | `handle_startendtag(tag, attrs)` | 对于空标签;默认句柄分别开始和结束。 | | `handle_endtag(tag)` | 当找到结束标签时。 | | `handle_data(data)` | 对于文本数据。 | | `handle_charref(ref)` | 对于形式为`&#ref;`的字符引用。 | | `handle_entityref(name)` | 用于形式为`&name;`的实体引用。 | | `handle_comment(data)` | 征求意见;仅用注释内容调用。 | | `handle_decl(decl)` | 用于形式为`<!...>`的声明。 | | `handle_pi(data)` | 用于处理指令。 | | `unknown_decl(data)` | 读取未知声明时调用。 |出于屏幕抓取的目的,您通常不需要实现所有的解析器回调(事件处理程序),并且您可能不需要构造整个文档的某种抽象表示(比如文档树)来找到您想要的东西。如果你只是跟踪找到你要找的东西所需的最少信息,你就成功了。(参见第二十二章,在用 SAX 解析 XML 的上下文中了解更多关于这个主题的内容。)清单 15-2 显示了一个解决与清单 15-1 相同问题的程序,但是这次使用了HTMLParser
。
from urllib.request import urlopen
from html.parser import HTMLParser
def isjob(url):
try:
a, b, c, d = url.split(’/’)
except ValueError:
return False
return a == d == ’’ and b == ’jobs’ and c.isdigit()
class Scraper(HTMLParser):
in_link = False
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
url = attrs.get(’href’, ’’)
if tag == ’a’ and isjob(url):
self.url = url
self.in_link = True
self.chunks = []
def handle_data(self, data):
if self.in_link:
self.chunks.append(data)
def handle_endtag(self, tag):
if tag == ’a’ and self.in_link:
print(’{} ({})’.format(’’.join(self.chunks), self.url))
self.in_link = False
text = urlopen(’http://python.org/jobs’).read().decode()
parser = Scraper()
parser.feed(text)
parser.close()
Listing 15-2.A Screen-Scraping Program Using the HTMLParser Module
有几件事值得注意。首先,我在这里已经放弃了 Tidy 的使用,因为网页中的 HTML 表现得足够好了。如果你幸运的话,你可能会发现你也不需要使用 Tidy。还要注意,我使用了一个布尔状态变量(属性)来跟踪我是否在一个相关的链接中。我在事件处理程序中检查并更新这个属性。其次,handle_starttag
的attrs
参数是一个(key, value)
元组列表,所以我使用了dict
将它们转换成一个字典,我发现这样更容易管理。
handle_data
方法(和chunks
属性)可能需要一些解释。它使用了一种在基于事件的结构化标记(如 HTML 和 XML)解析中很常见的技术。我没有假设我会在对handle_data
的一次调用中获得我需要的所有文本,而是假设我可能会通过不止一次调用获得几大块文本。这可能有几个原因——缓冲、字符实体、我忽略的标记等等——我只需要确保我得到了所有的文本。然后,当我准备好展示我的结果时(在handle_endtag
方法中),我简单地将所有的块join
在一起。为了实际运行解析器,我用文本调用它的feed
方法,然后调用它的close
方法。
与使用正则表达式相比,此类解决方案在某些情况下对输入数据的变化更具鲁棒性。尽管如此,您可能会反对它过于冗长,可能并不比正则表达式更清晰或更容易理解。对于一个更复杂的提取任务,支持这种解析的论点似乎更有说服力,但人们仍然觉得一定有更好的方法。而且,如果你不介意安装另一个模块,有。。。
美味的汤
Beautiful Soup 是一个漂亮的小模块,用于解析和剖析你有时在 Web 上找到的那种 HTML 草率且格式不良的那种。引用美汤网站( http://crummy.com/software/BeautifulSoup
):
You didn’t write that terrible page. You just want to get some data from it. Beautiful soup is here to help.
下载安装美汤轻而易举。和大多数包一样,可以使用pip
。
$ pip install beautifulsoup4
你可能想做一个pip search
来看看是否有一个更新的版本。安装了 Beautiful Soup 之后,从 Python Job Board 中提取 Python 作业的运行示例变得非常非常简单,可读性很强,如清单 15-3 所示。我现在浏览文档的结构,而不是检查 URL 的内容。
from urllib.request import urlopen
from bs4 import BeautifulSoup
text = urlopen(’http://python.org/jobs’).read()
soup = BeautifulSoup(text, ’html.parser’)
jobs = set()
for job in soup.body.section(’h2’):
jobs.add(’{} ({})’.format(job.a.string, job.a[’href’]))
print(’\n’.join(sorted(jobs, key=str.lower)))
Listing 15-3.A Screen-Scraping Program Using Beautiful Soup
我简单地用我想要抓取的 HTML 文本实例化了BeautifulSoup
类,然后使用各种机制来提取部分结果解析树。例如,我使用soup.body
获取文档的主体,然后访问它的第一个section
。我用’h2’
作为参数调用结果对象,这相当于使用它的find_all
方法,这给了我一个该部分中所有h2
元素的集合。每一个都代表一项工作,我对它包含的第一个链接job.a
感兴趣。string
属性是它的文本内容,而a[’href’]
是href
属性。我相信你已经注意到了,我在清单 15-3 中添加了set
和sorted
(其中key
函数被设置为忽略大小写差异)。这和美汤无关;这只是为了让程序更有用,通过消除重复和按排序打印名字。
如果你想把你的废料用于一个 RSS 提要(在本章后面讨论),你可以使用另一个与 Beautiful Soup 相关的工具,叫做 Scrape ‘N’ Feed(在 http://crummy.com/
software/ScrapeNFeed
)。
使用 CGI 的动态网页
虽然本章的第一部分讨论了客户端技术,但现在我们换个角度来处理服务器端。本节讨论一种基本的 web 编程技术:公共网关接口(CGI)。CGI 是一种标准机制,通过这种机制,web 服务器可以将您的查询(通常通过 web 表单提供)传递给专用程序(例如,您的 Python 程序)并将结果显示为网页。这是一种创建 web 应用的简单方法,无需编写自己的专用应用服务器。有关 Python 中 CGI 编程的更多信息,请参见 Python 网站上的 Web 编程主题指南( http://wiki.python.org/moin/WebProgramming
)。
Python CGI 编程中的关键工具是cgi
模块。另一个在 CGI 脚本开发过程中非常有用的模块是cgitb
——稍后在“用 cgitb 调试”一节中会有更多的介绍
在使你的 CGI 脚本可以通过网络访问(和运行)之前,你需要把它们放在一个网络服务器可以访问它们的地方,添加一个磅命令行,并设置适当的文件权限。这三个步骤将在下面的章节中解释。
步骤 1:准备 Web 服务器
我假设你可以访问网络服务器——换句话说,你可以把东西放到网上。通常,就是把你的网页、图片等放在一个特定的目录中(在 UNIX 中,通常称为public_html
)。如果您不知道如何操作,您应该询问您的互联网服务提供商(ISP)或系统管理员。
Tip
如果您运行的是 macOS,那么 Apache web 服务器是操作系统安装的一部分。它可以通过“系统偏好设置”的“共享”偏好设置面板打开,方法是选中“Web 共享”选项。
如果您只是尝试一下,您可以使用http.server
模块直接从 Python 运行一个临时 web 服务器。与任何模块一样,可以通过提供带有-m
开关的 Python 可执行文件来导入和运行它。如果你添加--cgi
到模块中,结果服务器将支持 CGI。请注意,服务器将在您运行它的目录中提供文件,所以请确保您在那里没有任何秘密。
$ python -m http.server --cgi
Serving HTTP on 0.0.0.0 port 8000 ...
如果您现在将浏览器指向http://127.0.0.1:8000
或http://localhost:8000
,您应该会看到运行服务器的目录列表。您还应该看到服务器告诉您有关连接的信息。
你的 CGI 程序也必须放在一个可以通过网络访问的目录中。此外,它们必须以某种方式被识别为 CGI 脚本,这样 web 服务器就不只是将普通的源代码作为网页来提供。有两种典型的方法可以做到这一点:
- 将脚本放在名为
cgi-bin
的子目录中。 - 给你的脚本文件扩展名
.cgi
。
具体的工作方式因服务器而异,同样,如果您有疑问,请咨询您的 ISP 或系统管理员。(例如,如果您正在使用 Apache,您可能需要打开相关目录的ExecCGI
选项。)如果你正在使用来自http.server
模块的服务器,你应该使用一个cgi-bin
子目录。
步骤 2:添加磅爆炸线
当您将脚本放在正确的位置时(可能会给它指定一个特定的文件扩展名),您必须在脚本的开头添加一个井号。我在第一章中提到过,这是一种执行脚本的方式,不需要显式执行 Python 解释器。通常,这只是方便,但对于 CGI 脚本来说,这是至关重要的——没有它,web 服务器就不知道如何执行你的脚本。(据我所知,该脚本可以用其他编程语言编写,比如 Perl 或 Ruby。)一般来说,只需在脚本的开头添加以下行即可:
#!/usr/bin/env python
请注意,它必须是第一行。(前面没有空行。)如果这不起作用,您需要找出 Python 可执行文件的确切位置,并在磅爆炸行中使用完整路径,如下所示:
#!/usr/bin/python
如果你同时安装了 Python 2 和 3,你可能需要使用python3
来代替。(这也可能与前面显示的env
解决方案一起使用。)如果还是不行,那可能是你看不到的地方出了问题,也就是说,这条线以\r\n
结尾,而不是简单的\n
,你的网络服务器被弄糊涂了。确保您将文件保存为纯 UNIX 样式的文本文件。
在 Windows 中,使用 Python 二进制文件的完整路径,如下例所示:
#!C:\Python36\python.exe
步骤 3:设置文件权限
您需要做的最后一件事(至少如果您的 web 服务器运行在 UNIX 或 Linux 机器上)是设置适当的文件权限。您必须确保每个人都被允许读取和执行您的脚本文件(否则 web 服务器将无法运行它),但也要确保只有您被允许写入它(因此没有人可以更改您的脚本)。
Tip
有时,如果您在 Windows 中编辑一个脚本,并且它存储在 UNIX 磁盘服务器上(例如,您可能通过 Samba 或 FTP 访问它),在您对脚本进行更改后,文件权限可能会出错。因此,如果您的脚本无法运行,请确保权限仍然正确。
改变文件权限(或文件模式)的 UNIX 命令是chmod
。简单地运行下面的命令(如果你的脚本叫做somescript.cgi
),使用你的普通用户帐户,或者一个专门为这样的 web 任务设置的帐户。
chmod 755 somescript.cgi
完成所有这些准备工作后,您应该能够像打开网页一样打开脚本并执行它。
Note
您不应该在浏览器中将脚本作为本地文件打开。你必须用一个完整的 HTTP URL 打开它,这样你就可以通过网络(通过你的网络服务器)获取它。
你的 CGI 脚本通常不允许修改你电脑上的任何文件。如果您想允许它更改文件,您必须明确地授予它这样做的权限。你有两个选择。如果您有 root(系统管理员)权限,您可以为您的脚本创建一个特定的用户帐户,并更改需要修改的文件的所有权。如果您没有 root 访问权限,您可以设置该文件的文件权限,这样系统上的所有用户(包括 web 服务器用来运行 CGI 脚本的用户)都可以写入该文件。您可以使用以下命令设置文件权限:
chmod 666 editable_file.txt
Caution
使用文件模式 666 有潜在的安全风险。除非你知道自己在做什么,否则最好避开。
CGI 安全风险
一些安全问题与使用 CGI 程序有关。如果你允许你的 CGI 脚本写入你服务器上的文件,这种能力可能会被用来破坏数据,除非你仔细地编写你的程序。类似地,如果您评估用户提供的数据,就好像它是 Python 代码(例如,用exec
或eval
)或 shell 命令(例如,用os.system
或使用subprocess
模块),您就冒着执行任意命令的风险,这是一个巨大的风险。即使使用用户提供的字符串作为 SQL 查询的一部分也是有风险的,除非您非常小心地首先清理该字符串;所谓的 SQL 注入是攻击或闯入系统的一种常见方式。
一个简单的 CGI 脚本
最简单的 CGI 脚本类似于清单 15-4 。
#!/usr/bin/env python
print(’Content-type: text/plain’)
print() # Prints an empty line, to end the headers
print(’Hello, world!’)
Listing 15-4.A Simple CGI Script
如果你把它保存在一个名为simple1.cgi
的文件中,并通过你的网络服务器打开它,你应该会看到一个只包含“Hello,world!”以纯文本格式。为了能够通过 web 服务器打开此文件,您必须将它放在 web 服务器可以访问的地方。在典型的 UNIX 环境中,将它放在主目录中名为public_html
的目录下,您就可以用 URL http://localhost/∼username/simple1.cgi
打开它(用您的用户名代替username
)。有关详细信息,请咨询您的 ISP 或系统管理员。如果你使用的是cgi-bin
目录,你也可以称之为类似于simple1.py
的东西。
如您所见,程序写入标准输出的所有内容(例如,用print
)最终都会出现在结果网页中——至少是几乎所有内容。事实上,您首先打印的是 HTTP 头,它是关于页面的信息行。这里我唯一关心的头球是Content-type
。正如您所看到的,短语Content-type
后面是一个冒号、一个空格和类型名text/plain
。这表明该页面是纯文本的。为了表示 HTML,这一行应该如下所示:
print(’Content-type: text/html’)
打印完所有标题后,会打印一个空行,表示文档本身即将开始。如您所见,在这种情况下,文档只是字符串’Hello, world!’
。
使用 cgitb 调试
有时,编程错误会使您的程序因未捕获的异常而终止,并出现堆栈跟踪。当通过 CGI 运行程序时,这很可能会导致 web 服务器发出无用的错误消息,甚至可能只是一个黑色页面。如果您可以访问服务器日志(例如,如果您正在使用http.server
),您可能会从那里获得一些信息。为了帮助你调试 CGI 脚本,标准模块包含了一个有用的模块,叫做cgitb
(用于 CGI 回溯)。通过导入它并调用它的enable
函数,你可以得到一个非常有用的网页,上面有关于哪里出错的信息。清单 15-5 给出了一个如何使用cgitb
模块的例子。
#!/usr/bin/env python
import cgitb; cgitb.enable()
print(’Content-type: text/html\n’)
print(1/0)
print(’Hello, world!’)
Listing 15-5.A CGI Script That Invokes a Traceback (faulty.cgi)
在浏览器中(通过 web 服务器)访问该脚本的结果如图 15-1 所示。
图 15-1。
A CGI traceback from the cgitb module
请注意,在开发完程序后,您可能想要关闭cgitb
功能,因为回溯页面不是为程序的临时用户准备的。1
使用 cgi 模块
到目前为止,这些程序只产生了输出;他们没有使用任何形式的输入。输入作为键值对或字段从 HTML 表单(在下一节中描述)提供给 CGI 脚本。您可以使用来自cgi
模块的FieldStorage
类在 CGI 脚本中检索这些字段。当您创建您的FieldStorage
实例(您应该只创建一个)时,它从请求中获取输入变量(或字段),并通过类似字典的接口将它们呈现给您的程序。可以通过普通的键查找来访问FieldStorage
的值,但是由于一些技术上的问题(与文件上传有关,我们在这里不讨论),所以FieldStorage
的元素并不是您真正想要的值。例如,如果您知道请求包含一个名为name
的值,您不能简单地这样做:
form = cgi.FieldStorage()
name = form[’name’]
您需要这样做:
form = cgi.FieldStorage()
name = form[’name’].value
获取值的一个稍微简单的方法是getvalue
方法,它类似于字典方法get
,除了它返回项目的value
属性的值。这里有一个例子:
form = cgi.FieldStorage()
name = form.getvalue(’name’, ’Unknown’)
在前面的例子中,我提供了一个默认值(’Unknown’
)。如果不提供,默认为None
。如果字段未填写,则使用默认值。
清单 15-6 包含了一个使用cgi.FieldStorage
的简单例子。
#!/usr/bin/env python
import cgi
form = cgi.FieldStorage()
name = form.getvalue(’name’, ’world’)
print(’Content-type: text/plain\n’)
print(’Hello, {}!’.format(name))
Listing 15-6.A CGI Script That Retrieves a Single Value from a FieldStorage (simple2.cgi)
Invoking CGI Scripts Without Forms
CGI 脚本的输入通常来自已经提交的 web 表单,但是也可以直接用参数调用 CGI 程序。要做到这一点,可以在脚本的 URL 后面添加一个问号,然后添加由&符号分隔的键值对。例如,如果清单 15-6 中脚本的 URL 是 http://www.example.com/simple2.cgi
,您可以用 URL http://www.example.com/simple2.cgi?name=Gumby&age=42
用name=Gumby and age=42
调用它。如果您尝试这样做,您应该会收到消息“你好,Gumby!”而不是“你好,世界!”来自你的 CGI 脚本。(注意没有使用age
参数。)您可以使用urllib.parse
模块的urlencode
方法来创建这种 URL 查询:
>>> urlencode({’name’: ’Gumby’, ’age’: ’42’})
’age=42&name=Gumby’
你可以在自己的程序中使用这个策略,和urllib
一起,创建一个可以和 CGI 脚本交互的屏幕抓取程序。然而,如果您正在编写这样一个装置的两端(即服务器端和客户端),您很可能会更好地使用某种形式的 web 服务(如本章“Web 服务:正确抓取”一节中所述)。
简单的形式
现在您有了处理用户请求的工具;是时候创建一个用户可以提交的表单了。该表单可以是一个单独的页面,但我将把它放在同一个脚本中。
要了解更多关于编写 HTML 表单(或一般的 HTML)的知识,您可能需要一本关于 HTML 的好书(您当地的书店可能有几本)。你也可以在网上找到大量关于这个主题的信息。和往常一样,如果您发现某个页面看起来像是您想要做的事情的一个很好的示例,您可以在浏览器中检查它的源代码,方法是从其中一个菜单中选择“查看源代码”或类似的内容(取决于您使用的浏览器)。
Note
从 CGI 脚本中获取信息有两种主要方法:GET 方法和 POST 方法。就本章的目的而言,两者之间的区别并不重要。基本上,GET 是为了检索东西,并在 URL 中编码它的查询;POST 可以用于任何类型的查询,但是对查询的编码稍有不同。
让我们回到我们的剧本。在清单 15-7 中可以找到一个扩展版本。
#!/usr/bin/env python
import cgi
form = cgi.FieldStorage()
name = form.getvalue(’name’, ’world’)
print("""Content-type: text/html
<html>
<head>
<title>Greeting Page</title>
</head>
<body>
<h1>Hello, {}!</h1>
<form action=’simple3.cgi’>
Change name <input type=’text’ name=’name’ />
<input type=’submit’ />
</form>
</body>
</html>
""".format(name))
Listing 15-7.A Greeting Script with an HTML Form (simple3.cgi)
在这个脚本的开始,CGI 参数name
被检索,和以前一样,默认为’world’
。如果您只是在浏览器中打开脚本,而没有提交任何内容,则使用默认设置。
然后打印一个简单的 HTML 页面,包含name
作为标题的一部分。此外,该页面包含一个 HTML 表单,其action
属性被设置为脚本本身的名称(simple3.cgi
)。这意味着,如果表单被提交,您将返回到相同的脚本。表单中唯一的输入元素是一个名为name
的文本字段。因此,如果你提交一个新名字的字段,标题应该会改变,因为name
参数现在有了一个值。
图 15-2 显示了通过 web 服务器访问清单 15-7 中的脚本的结果。
图 15-2。
The result of executing the CGI script in Listing 15-7
使用 Web 框架
大多数人不会直接为任何严肃的 web 应用编写 CGI 脚本;相反,他们使用一个 web 框架,为你做了很多繁重的工作。有很多这样的框架可用,我将在后面提到其中的一些—但是现在,让我们坚持使用一个非常简单但是非常有用的框架,叫做 Flask ( http://flask.pocoo.org
)。使用pip
很容易安装。
$ pip install flask
假设你写了一个计算 2 的幂的令人兴奋的函数。
def powers(n=10):
return ’, ’.join(str(2**i) for i in range(n))
现在你想把这个杰作公之于众!要用 Flask 做到这一点,首先用适当的名称实例化Flask
类,并告诉它哪个 URL 路径对应于您的函数。
from flask import Flask
app = Flask(__name__)
@app.route(’/’)
def powers(n=10):
return ’, ’.join(str(2**i) for i in range(n))
如果您的脚本名为powers.py
,您可以让 Flask 如下运行它(假设是 UNIX 风格的 shell):
$ export FLASK_APP=powers.py
$ flask run
* Serving Flask app "powers"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
最后两行是 Flask 的输出。如果在浏览器中输入 URL,应该会看到从powers
返回的字符串。您还可以为您的函数提供一个更具体的路径。例如,如果您使用route(’/powers’)
而不是route(’/’)
,该功能将在http://127.0.0.1:5000/powers
可用。然后,您可以设置多个函数,每个函数都有自己的 URL。
你甚至可以为你的函数提供参数。您使用尖括号来指定参数,因此您可能会使用’/powers/<n>’
,例如。您在斜杠后指定的任何内容都将作为名为n
的关键字参数提供。它将是一个字符串,在我们的例子中,我们需要一个整数。我们可以通过使用route(’/powers/<int:n>’)
来添加这个转换。然后,重启 Flask 后,如果访问 URL http://127.0.0.1:5000/powers/3
,应该会得到输出1, 2, 4
。
Flask 还有很多其他特性,它的文档可读性很强。如果您想尝试简单的服务器端 web 应用开发,我推荐您看一看。
其他 Web 应用框架
有许多其他的 web 框架可供使用,有大有小。有些是相当模糊的,而另一些有专门的定期会议。表 15-2 中列出了一些流行的;要获得更全面的列表,您应该查阅 Python 网页( https://wiki.python.org/moin/WebFrameworks
)。
表 15-2。
Python Web Application Frameworks
| 名字 | 网站 | | --- | --- | | Django | [`https://djangoproject.com`](https://djangoproject.com) | | 涡轮齿轮 | [`http://turbogears.org`](http://turbogears.org) | | web2py | [`http://web2py.com`](http://web2py.com) | | 神交 | [`https://pypi.python.org/pypi/grok`](https://pypi.python.org/pypi/grok) | | Zope2 | [`https://pypi.python.org/pypi/Zope2`](https://pypi.python.org/pypi/Zope2) | | 金字塔 | [`https://trypyramid.com`](https://trypyramid.com) |Web 服务:抓取正确
Web 服务有点像计算机友好的网页。它们基于使程序能够通过网络交换信息的标准和协议,通常一个程序(客户机或服务请求者)请求一些信息或服务,另一个程序(服务器或服务提供者)提供这些信息或服务。是的,这是显而易见的东西,它似乎也非常类似于在第十四章中讨论的网络编程,但是有区别。
Web 服务通常在相当高的抽象层次上工作。他们使用 HTTP(“Web 的协议”)作为底层协议。除此之外,它们使用更多面向内容的协议,比如某种 XML 格式来编码请求和响应。这意味着 web 服务器可以是 web 服务的平台。正如这一部分的标题所示,这是网络抓取的另一个层次。您可以将 web 服务看作是为计算机化的客户端设计的动态网页,而不是供人使用的。
web 服务的标准在捕捉各种复杂性方面走得很远,但是你也可以用绝对的简单来完成很多事情。在这一节中,我只对这个主题做了一个简单的介绍,并提供了一些在哪里可以找到您可能需要的工具和信息的提示。
Note
由于有许多实现 web 服务的方法,包括大量的协议,并且每个 web 服务系统可能提供几个服务,所以有时有必要以一种客户机可以自动解释的方式来描述一个服务,也就是说一个元服务。这种描述的标准是 Web 服务描述语言(WSDL)。WSDL 是一种 XML 格式,它描述了诸如哪些方法可以通过服务使用,以及它们的参数和返回值。除了实际的服务协议(如 SOAP)之外,许多(如果不是大多数)web 服务工具包还将包括对 WSDL 的支持。
RSS 和朋友
RSS 代表 Rich Site Summary、RDF Site Summary 或 Really Simple Syndication(取决于版本号),其最简单的形式是用 XML 列出新闻条目的格式。让 RSS 文档(或提要)比简单的静态文档更像一种服务的是,它们应该定期(或不定期)更新。它们甚至可以被动态地计算,表示例如对博客等的最新添加。一种更新的格式是 Atom。有关 RSS 及其相关资源描述框架(RDF)的信息,请参见 http://www.w3.org/RDF
。原子的规格见 http://tools.ietf.org/html/rfc4287
。
有很多 RSS 阅读器,它们通常也可以处理其他格式,比如 Atom。因为 RSS 格式非常容易处理,所以开发人员不断为它开发新的应用。例如,一些浏览器(比如 Mozilla Firefox)会让你为一个 RSS 订阅源添加书签,然后给你一个动态书签子菜单,把单个新闻条目作为菜单项。RSS 也是播客的主干;播客本质上是一个列出声音文件的 RSS 提要。
问题是,如果您想编写一个处理来自几个站点的提要的客户端程序,您必须准备好解析几种不同的格式,甚至可能需要解析提要的各个条目中的 HTML 片段。尽管您可以使用BeautifulSoup
(或者它的一个面向 XML 的版本)来解决这个问题,但是使用 Mark Pilgrim 的通用提要解析器( https://pypi.python.org/pypi/feedparser
)可能是一个更好的主意,它可以处理几种提要格式(包括 RSS 和 Atom,以及一些扩展),并支持某种程度的内容清理。Pilgrim 还写过一篇有用的文章,“不惜一切代价解析 RSS”(http://xml.com/pub/a/2003/01/22/dive-into-xml.html
),以防你想自己处理一些清理工作。
使用 XML-RPC 的远程过程调用
在 RSS 简单的下载和解析机制之外,还有远程过程调用。远程过程调用是基本网络交互的抽象。你的客户程序要求服务器程序执行一些计算并返回结果,但是这都被伪装成一个简单的过程(或者函数或者方法)调用。在客户端代码中,看起来像是调用了一个普通的方法,但是调用它的对象实际上完全驻留在不同的机器上。这种过程调用最简单的机制可能是 XML-RPC,它通过 HTTP 和 XML 实现网络通信。因为该协议与语言无关,所以用一种语言编写的客户机程序很容易调用用另一种语言编写的服务器程序上的函数。
Tip
试着在网上搜索一下,为 Python 找到许多其他的 RPC 选项。
Python 标准库包括对客户端和服务器端 XML-RPC 编程的支持。有关使用 XML-RPC 的示例,请参见第二十七章和第二十八章。
RPC and REST
尽管这两种机制非常不同,但是远程过程调用可以与网络编程中所谓的代表性状态传输风格(通常称为 REST)相比较。基于 REST(或 RESTful)的程序也允许客户端以编程方式访问服务器,但是服务器程序被认为没有任何隐藏状态。返回的数据由给定的 URL 唯一确定(或者,在 HTTP POST 的情况下,由客户端提供的附加数据)。
更多关于 REST 的信息可以在网上找到。例如,你可以从维基百科上的文章开始,在 http://en.wikipedia.org/wiki/Representational_State_Transfer
。在 RESTful 编程中经常使用的一个简单而优雅的协议是 JavaScript Object Notation,或 JSON ( http://www.json.org
),它允许您以纯文本格式表示复杂的对象。您可以在json
标准库模块中找到对 JSON 的支持。
肥皂
SOAP 2 也是一种交换消息的协议,以 XML 和 HTTP 为底层技术。像 XML-RPC 一样,SOAP 支持远程过程调用,但是 SOAP 规范比 XML-RPC 的规范复杂得多。SOAP 是异步的,支持关于路由的元请求,并且有一个复杂的类型系统(与 XML-RPC 简单的固定类型集相反)。
Python 没有单一的标准 SOAP 工具包。你可能要考虑扭曲( http://twistedmatrix.com
)、ZSI ( http://pywebsvcs.sf.net
)或者索比( http://soapy.sf.net
)。有关 SOAP 格式的更多信息,请参见 http://www.w3.org/TR/soap
。
快速总结
以下是本章所涵盖主题的摘要:
- 屏幕抓取:这是自动下载网页并从中提取信息的做法。Tidy 程序及其库版本是在使用 HTML 解析器之前修复不良 HTML 的有用工具。另一个选择是使用漂亮的汤,它对杂乱的输入非常宽容。
- CGI:公共网关接口是一种创建动态网页的方法,它通过让一个 web 服务器运行并与你的程序通信来显示结果。
cgi
和cgitb
模块对于编写 CGI 脚本很有用。CGI 脚本通常从 HTML 表单中调用。 - Flask:一个简单的 web 框架,可以让你将代码发布为 web 应用,而不用太担心 web 部分的事情。
- Web 应用框架:对于用 Python 开发大型复杂的 web 应用,web 应用框架几乎是必不可少的。对于更简单的项目,Flask 是一个不错的选择。对于较大的项目,您可能想考虑像 Django 或 TurboGears 这样的东西。
- Web 服务:Web 服务对于程序就像(动态)网页对于人一样。您可能会将它们视为在更高的抽象层次上进行网络编程的一种方式。常见的 web 服务标准有 RSS(及其相关的 RDF 和 Atom)、XML-RPC 和 SOAP。
本章的新功能
| 功能 | 描述 | | --- | --- | | `cgitb.enable()` | 在 CGI 脚本中启用回溯 |现在怎么办?
我相信你已经通过运行程序测试了你写的程序。在下一章,你将学习如何真正地测试它们——彻底地、有条不紊地,甚至可能是着迷地(如果你幸运的话)。
Footnotes 1
另一种方法是关闭显示器,将错误记录到文件中。有关详细信息,请参阅 Python 库参考。
2
虽然这个名称曾经代表简单对象访问协议,但现在已经不是了。现在只是肥皂。