第10章 Web编程:CGI和WSGI

WSGI主要有利于Web框架和Web服务器的作者,不是Web应用的作者。WSGI不是一个应用程序API,而是框架与服务器之间的粘合API。

——Phillip J.Eby, 2004年8月

本章内容:

简介;

帮助Web服务器处理客户端数据;

构建CGI应用程序;

在CGI中使用Unicode;

高级CGI;

WSGI简介;

真实世界的Web开发;

相关模块。

10.1 简介

本章是Web编程方面的入门章节,将对Python网络编程做快速而广泛的概述,从Web浏览到创建用户反馈表单,从识别URL到生成动态Web页面。本章首先介绍通用网关接口(Common Gateway Interface CGI),接着讨论 Web 服务器网关接口(Web Server Gateway Interface, WSGI)。

10.2 帮助Web服务器处理客户端数据

本节将介绍CGI,包括 CGI的含义、出现原因,以及与Web服务器的工作方式,接着介绍如何使用Python创建CGI应用。

10.2.1 CGI简介

Web 最初目的是在全球范围内对文档进行在线存储和归档(大多用于教学和科研)。这些文件通常用静态文本表示,一般是HTML。

HTML 是一个文本排版工具,而不像是一种语言,可用于指明字体的类型、大小、样式。HTML 的主要特性是其超文本的兼容性,如突出显示标明一些文本,或用图形元素作为链接,指向其他本地文档或位于网上其他地方的文档。这样就可以通过鼠标单击或者其他用户选择机制来访问相关文档。这些静态HTML文档位于Web服务器上,在需要的时候会被发送到客户端。

随着因特网和Web服务的发展,除了浏览之外,还需要处理用户的输入。如在线零售商需要处理单个订单,网上银行和搜索引擎需要为每个用户建立独立账号。因此出现了表单,它们成为Web站点从用户获得特定信息的唯一形式(在Java applet出现之前)。反过来,在客户提交了特定数据后,就要求立即生成HTML页面。

现在Web服务器仅有一点做得很不错,即了解用户需要哪个文件,接着将这个文件(即HTML文件)发送给客户端。Web服务器不能处理表单中传递过来的用户相关的数据。这不是Web服务器的职责,Web服务器将这些请求发送给外部应用,将这些外部应用动态生成的HTML页面发送回客户端。

处理过程的第一步是Web服务器从客户端接到了请求(即GET或者POST),并调用相应的应用程序。它然后等待HTML页面,与此同时,客户端也在等待。一旦应用程序处理完成,它会将生成的动态HTML页面返回服务器端,然后服务器端再将这个最终结果返回给用户。对于表单处理过程,服务器与外部应用程序交互,收到并将生成的HTML页面通过CGI返回客户端。图 10-1 描述了 CGI的工作原理,其中逐步展示了用户从提交表单到返回最终结果Web页面的整个执行过程和数据流向。

Greeting in English, Spanish, - 图1 图10-1 CGI工作方式概览。CGI在Web服务器和应用之间充当了交互作用,这样才能够处理用户表单,生成并返回最终的动态HTML页

客户端输入给 Web 服务器端的表单可能包括处理过程和一些存储在后台数据库中的表单。需要记住的是,含有需要用户输入项(如文本框、单选按钮等)、Submit 按钮、图片的Web页面,都会涉及某种CGI活动。

创建 HTML的 CGI应用程序通常是用高级编程语言来实现的,可以接受、处理用户数据,向服务器端返回HTML页面。在接触CGI之前,需要告诫的是,一般生产环境的Web应用都不再使用CGI了。

由于CGI有明显的局限性,以及限制Web服务器同时处理客户端的数量,因此CGI被抛弃了。一些关键的Web服务使用C/C++这样的编译语言进行扩展。如今Web服务器典型的部件有Apache和集成的数据库访问部件(MySQL或者PostgreSQL)、Java(Tomcat)、PHP和各种动态语言(如 Python 或 Ruby)模块,以及 SSL/security。然而,如果在小型的私人Web网站或者小组织的Web网站上工作,就没有必要使用这些用于关键任务的强大而复杂的Web服务器。在开发小型Web网站或为了测试时,可以使用CGI。

另外,现在出现了很多Web应用程序开发框架和内容管理系统,这些工具淘汰了CGI。然而,这些新工具虽然进行了浓缩和抽象,但仍旧遵循着CGI最初提供的模式,如获取用户输入的信息,根据输入执行相关代码,并提供一个有效的HTML作为最终输出传递给客户端。因此,为了开发出高效的Web服务有必要学习CGI,了解其中的基础。

下一节将会介绍使用cgi模块在Python中建立一个CGI应用程序。

10.2.2 CGI应用程序

CGI 应用程序和典型的应用程序有些不同,主要的区别在于输入、输出以及用户和程序交互方面。当一个CGI脚本启动后,需要获得用户提供的表单数据,但这些数据必须要从 Web 客户端才可以获得,而不是从服务器或者硬盘上获得。这就是大家熟知的请求(request)。

与标准输出不同,这些输出将会发送回连接的 Web 客户端,而不是发送到屏幕、GUI窗口或者硬盘上。这些返回的数据必须是具有一系列有效头文件的 HTML 标签数据。如果Web客户端是浏览器,由于浏览器只能识别有效的HTTP数据(也就是MIME头和HTML),所以会发生错误(具体一点,就是内部服务器错误)。

最后,读者可能猜到了,用户与脚本之间没有任何交互。所有的交互都将发生在Web客户端(基于用户的行为)、Web服务器端和CGI应用程序间。

10.2.3 cgi模块

cgi模块中有个主要类:FieldStorage类,其完成了所有的工作。Python CGI脚本启动时会实例化这个类,通过Web 服务器从 Web客户端读出相关的用户信息。在实例化完成后,其中会包含一个类似字典的对象,它具有一系列的键值对。键就是通过表单传入的表单条目的名字,而值则包含相应的数据。

这些值可以是以下三种对象之一。一是 FieldStorage 对象(实例)。二是另一个名为MiniFieldStorage 类的类似实例,用在没有文件上传或 mulitple-part 格式数据的情况下。MiniFieldStorage实例只包含名程和数据的键值对。最后,它们还可以是这些对象的列表。当表单中的某个字段有多个输入值时就会产生这种对象。

对于简单的Web表单,可以发现其中所有的MiniFieldStorage实例。下边所有的例子都仅针对这种普通情况。

10.2.4 cgitb模块

前面已经提到,返回 Web 服务器的合法响应(将会转发给用户/浏览器)必须含有合法的HTTP头和HTML标记过的数据。是否考虑过在CGI应用崩溃时如何返回数据呢?想一想如果是一个Python脚本发生错误呢?对,会出现回溯消息。那么回溯的文本消息是否会被认为是合法的HTML头或HTML?不会。

Web服务器在收到无法理解的响应时,会抛弃这个响应,返回“500错误”。500是一个HTTP 响应编码,它表示发生了一个内部服务器错误。一般是服务器所执行的应用程序发生了错误。此时在浏览器中给出的提示消息没什么用,要么是空白,要么显示“内部服务器错误”或类似消息。

当Python程序在命令行或集成开发环境(IDE)中运行时,发生的错误会生成回溯消息,指出错误发生的位置,在浏览器中不会显示回溯消息。若想在浏览器中看到的是Web应用程序的回溯信息,而不是“内部服务器错误”,可以使用cgitb模块。

为了启用转储回溯消息,所要做的就是将下面的代码插入CGI应用中并进行调用。

import cgitb

cgitb.enable()

在本章的前半部分会有很多地方能用到这个模块。但在刚开始的简单例子中不会用到这两行代码。这里先介绍用笨方法查看并调试“内部服务器错误”消息。当理解了为什么服务器没有正确处理请求后,再添加这两行代码。

10.3 构建CGI应用程序

本节将手把手地介绍如何设置Web服务器,接着逐步剖析如何在Python中创建CGI应用。即从一个简单的脚本开始,逐步添加内容。这里学习到的内容可以用来开发任何Web框架的应用。

10.3.1 构建Web服务器

为了用 Python 进行 CGI 开发,首先需要安装一个 Web 服务器,将其配置成可以处理Python CGI请求,然后让Web服务器访问CGI脚本。其中有些操作也许需要获得系统管理员的帮助。

生产环境中的服务器

如果需要一个真正的Web服务器,可以下载并安装Apache、ligHTTPD或thttpd。Apache中有许多插件或模块可以处理Python CGI,但在这里的例子中它们并不是必要的。如果想在生产环境中部署相关服务,或许需要安装这些软件。但即使这样也有点功能过剩。

开发人员服务器

出于学习目的或者想建立小型Web站点,使用Python自身带的Web服务器就已经足够了。第9章介绍了如何创建和配置基于Python的简单Web服务器。而本章的例子更加简单,仅仅使用了Python的CGI Web服务器。

如果想启动这个最基本的Web服务器,可以在Python 2.x中直接执行下边的Python语句。

$ python -m CGIHTTPServer [port]

在Python 3中这就不太容易了,因为所有这三个Web服务器和对应的处理程序都合并到一个模块(http.server)中,这个模块中含有一个基础服务器和三个请求处理程序类(BaseHTTPRequestHandler、SimpleHTTPRequestHandler和CGIHTTPRequestHandler)。

如果没有为服务器提供可选的端口号,则默认会使用 8000 端口。另外,-m 选项是Python 2.4中新增的。如果使用老版本的Python,或想用其他方式运行程序,可以使用下面的方法。

从命令行中执行脚本。

这种方法有点问题,因为必须知道 CGIHTTPServer.py 文件的实际存储位置。在Windows系统上,Python安装目录一般是C:\Python2X。

C:\>python C:\Python27\Lib\CGIHTTPServer.py

Serving HTTP on 0.0.0.0 port 8000 …

在POSIX系统上,需要稍微查找一下。

>>> import sys, CGIHTTPServer

>>> sys.modules['CGIHTTPServer']

<module 'CGIHTTPServer' from '/usr/local/lib/python2.7/

CGIHTTPServer.py'>

>>>^D

$ python /usr/local/lib/python2.7/CGIHTTPServer.py

Serving HTTP on 0.0.0.0 port 8000 …

使用-c选项。

使用-c选项可以运行由Python语句组成的字符串。因此,可以使用下面的方式导入CGIHTTPServer并执行其中的test()函数。

$ python -c "import CGIHTTPServer; CGIHTTPServer.test()"

Serving HTTP on 0.0.0.0 port 8000 …

在Python 3.x中,由于CGIHTTPServer合并进了http.server中,因此需要使用下面在Python 3.2中的等价调用方式。

$ python3.2 -c "from http.server import

CGIHTTPRequestHandler,test;test(CGIHTTPRequestHandler)"

创建快速脚本。

前面通过-c 选项执行导入并调用 test()的语句,将这些语句插入任意文件中,命名为cgihttpd.py文件(Python 2或3)。对于Python 3,由于没有CGIHTTPServer.py模块可供执行,因此启动服务器的唯一方式就是使用命令行,并提供非8000的端口号,如下所示。

$ python3.2 cgihttpd.py 8080

Serving HTTP on 0.0.0.0 port 8080 …

这4种方式都会从当前计算机中的当前目录下启动一个端口号为8000(或自行指定)的Web服务器。然后在启动服务器的目录下创建一个cgi-bin目录,放入Python CGI脚本。将一些HTML文件放到启动服务器的目录中,可能要在cgi-bin中放些Python CGI脚本,然后就可以在地址栏中输入这些地址来访问Web站点。

http://localhost:8000/friends.htm

http://localhost:8080/cgi-bin/friendsB.py

需要确保启动服务器的目录中有个cig-bin目录,同时确保其中有相应的.py文件。否则,服务器会将Python文件作为静态文本返回,而不是执行这些文件。

10.3.2 建立表单页

在示例10-1中写了一个简单的Web表单,即friends.html。从HTML代码中可以看到,该表单包括两个输入变量:person 和 howmany。这两个字段的值将会传到 CGI 脚本friendsA.py中。

读者会注意到在这个例子中,将 CGI脚本安装到主机默认的 cgi-bin 目录下(见其中的“ACTION”连接)(如果这个信息与读者的开发环境不一样,在测试Web页面和CGI之前请更新表单事件)。同时由于表单事件中缺少 METHOD 子标签,因此所有的请求都是默认的GET 类型。选择 GET 方法是因为这个表单中没有太多的字段,同时也希望请求的字段可以在“位置”(又称“地址”、“Go To”)栏中显示,以便看到发送到服务器端的URL。

示例10-1 静态表单页面(friends.htm)

这个HTML文件向用户展示了一个空文档,含有用户名和一系列可供用户选择的单选按钮。

Greeting in English, Spanish, - 图2

图10-2和图10-3显示了用friends.htm在Windows与Mac上的客户端渲染的界面。

Greeting in English, Spanish, - 图3 图10-2 Mac OS X中Chrome浏览器隐身模式下显示的Friends表单页面

Greeting in English, Spanish, - 图4 图10-3 Windows的Firefox 6上的Friends表单页面

10.3.3 生成结果页面

用户填写相关信息,单击 Submit 按钮会提交这些信息(在该文本框中输入完毕后按回车键也可以获得相同的效果)。提交之后,示例10-2中的friendsA.py脚本会随CGI一起执行。

示例10-2 CGI代码的结果界面(friendsA.py)

CGI脚本从表单中获取person和howmany字段,使用这些数据创建动态生成的结果页面。在Python 3版本(即friendsA3.py,这里没有列出)中,需要向第17行的print语句添加圆括号。这些代码都可在corepython.com上找到。

Greeting in English, Spanish, - 图5

这个脚本包含了读出并处理表单的输入,同时向用户返回最终HTML页面的功能。所有这些“实际”的工作仅是通过4行Python代码(第14~17行)来实现的。

表单的变量是FieldStorage的实例,包含person和howmany字段的值。将这些值分别存入Python的who和howmany变量中。变量reshtml包含需要返回的HTML文本的正文,还有一些根据表单中读入的数据动态填充的字段。

核心提示:分离HTTP头和HTML本身

有一点需要向CGI初学者指明的是,在向CGI脚本返回结果时,须先返回一个适当的HTTP头文件再返回HTML结果页面。另外,为了区分这些头文件和HTML结果页面,需要在两者之间插入一个空行(两个换行符),以 friendsA.py为例,即在第5行末尾插入一个显式的\n。本章后边的代码都进行了这样的处理。

图10-4是生成的页面(假设用户输入的名字为“Annalee Lenday”,单击“25 friends”单选按钮)。

Web站点的开发者或许会想,“如果这个人忘记了,我能自动将这个人的名字首字母大写,会不会更好些?”通过 Python 的 CGI可以很容易实现这个功能(下面很快就会实现)。

Greeting in English, Spanish, - 图6 图10-4 在提交姓名和朋友个数后,显示了Friends结果页面

注意GET请求是如何将表单中的变量和值加载在URL地址栏中的。读者是否观察到了friends.htm页面的标题有个“static”,而从friends.py脚本输出到屏幕上的则是“dynamic”?这样做是为了指明friends.htm文件是一个静态文本文件,而结果页面却是动态生成的。换句话说,结果页面的 HTML 不是以文本文件的形式存在硬盘上的,而是由 CGI脚本生成的,并且将其以本地文件的形式返回。

在下边的例子中,将会更新前面的CGI脚本,使其变得更灵活些,从而完全绕过静态文件。

10.3.4 生成表单和结果页面

这里抛弃了 friends.html 文件,并将其内容合并到 friendsB.py中。这个脚本现在将会同时生成表单页面和结果页面。但是如何控制生成哪个页面呢?如果发送了表单数据,那就意味着需要创建一个结果页面。如果没有获得任何信息,这就说明需要生成一个用户可以输入数据的表单页面。示例10-3中列出了新的friendsB.py脚本。

示例10-3 生成表单和结果页面(friendsB.py)

friends.htm和friendsA.py都合并进了friendsB.py中。最终的脚本可以用动态生成的HTML文件输出表单和结果页面,并且知道在何时输出哪个页面。若要将这些代码移植到Python 3版本的friendsB3.py中,需要在print语句中添加圆括号,并修改其中的表单事件。

Greeting in English, Spanish, - 图7

Greeting in English, Spanish, - 图8

逐行解释

第1~5行

除了通常的起始行和模块导入行之外,这里还把HTTP MIME头从后面的HTML正文部分中分离出来。因为需要在返回的两种页面(表单页面和结果页面)中都使用HTTP MIME头,而又不想复制文本。所以在需要的时候,将这个含有 HTML 头的字符串添加到相应的HTML正文中。

第7~28行

这段代码与CGI脚本里整合过的friends.htm表单页面有关。对表单页面的文本使用一个变量formhtml,还有一个用来创建单选按钮的字符串变量fradio。虽然可以从friends.htm复制这个单选按钮的HTML文本,但这里的目的是展示如何使用Python来生成更多的动态输出——见第22~26行的for循环。

showForm()函数负责生成表单页面用于用户输入。该函数为单选按钮创建一个文字集,并把这些HTML文本行合并到 formhtml主体中,然后给表单加上头信息,最后通过把整个字符串发送到标准输出的方式使客户端返回了整块数据。

这段代码中有两处有趣的地方值得注意。第一点是表单中第12行action处的“hidden”变量,这里的值为“edit”。只能通过这个字段才能决定显示哪个页面(表单页面或结果页面)。从第53~56行可以看到这个字段的作用。

还有,在生成所有按钮的循环过程中,把单选按钮0设置为默认按钮。这使得可以在一行代码里(第 18行)更新单选按钮的布局和/或它们的值,而无须再写多行文本。同时提高了灵活性,可以采用逻辑来判断哪个单选按钮被选中,参见后面的升级版friendsC.py。

现在读者或许会想:“既然可以检查person或howmany是否存在,那为什么还需要一个action变量呢?”问得好!在这种情况下当然可以只用person或howmany。

然而,action 变量很引人注目,从名称就能看出其作用,让代码很容易理解。person 和howmany变量都用来存储值,而action变量则用来作为一个标志。

创建action的另一个原因后面会再次使用到这个变量来决定生成哪个页面。具体来说,需要在出现person变量时显示一个表单(而不是结果页面)。如果在这里仅依赖person变量,代码会出问题。

第30~38行

显示结果页面的代码与friendsA.py中的几乎相同。

第40~55行

因为这个脚本可以产生不同的页面,所以创建了一个完整的rocess()函数来获得表单数据并决定采取何种动作。看起来process()的主体部分也和friendsA.py中主体部分的代码相似。然而,还是有两个主要区别。

由于不知道这个脚本是否能获得所需的字段(例如,第一次运行脚本时生成一个表单页面,这种情况下不会向服务器传递任何字段),因此需要用方括号将表单字段名称“括起来”,用if语句检查该字段是否存在。另外,上面提到的action字段可以用来决定生成哪个页面。第52~55行进行了这种检查。

图10-5显示了自动生成的表单,它与图10-2中的静态表单完全相同。但其后缀不是.html,而是.py。如果在name中填写“Cynthia Gilbert”,选择50个朋友,单击Submit按钮,会看到如图10-6所示的页面。

Greeting in English, Spanish, - 图9 图10-5 Windows上Chrome中自动生成的Friends表单页面

Greeting in English, Spanish, - 图10 图10-6 提交姓名和朋友个数之后的Friends结果页面

注意,URL中没有显示出静态的friends.htm,因为其需要同时处理表单和结果页面。

10.3.5 全面交互的Web站点

最后一个例子完成这个程序。在前面,用户在表单页面中输入个人信息,程序处理这些数据,并输出一个结果页面。现在将会在结果页面上添加一个链接,允许用户返回表单页面,但是返回的不是一个空白表单,而是含有用户输入信息的页面。这里还添加了一些错误处理代码,用来给出相关提示信息。示例10-4显示了新的friendsC.py。

示例10-4 具有完整用户交互和错误处理功能的程序(friendsC.py)

通过添加用于返回含有已输入信息的表单页面的连接,实现了完整的程序,给用户提供全面交互的Web应用体验。该应用程序现在也进行了一些简单的错误检查,在用户没有选择任何单选按钮时给予用户提示信息。

Greeting in English, Spanish, - 图11

Greeting in English, Spanish, - 图12

friendsC.py和friendsB.py没有太大区别。这里请读者找出不同点,但下面会简要列出其中的主要区别。

逐行解释

第7行

把URL从表单中移出来,因为现在除了输入表单之外,结果页面也要用到。

第9~18行、第68~70行、第74~81行

所有这些代码都用来显示错误提示信息。如果用户没有选择单选按钮来指明朋友数量,那么howmany字段就不会传送给服务器。这种情况下,showError()函数会向用户返回一个错误页面。

错误页面用到了 JavaScript 的“后退”按钮。因为按钮都是输入类型的,所以需要一个表单,但不需要有动作,因为只是简单地后退到浏览历史中的上一个页面。尽管这个脚本目前只支持(或者说只检测)一种类型的错误,但仍然使用了一个通用的error变量,因此如果以后继续开发这个脚本,就可以添加更多的错误检测。

第26~28行、第37~40行、第47行和第51~54行

这些代码的目的是创建一个有意义的链接,以便从结果页面返回表单页面。用户可以使用这个链接返回表单页面去更新或编辑填写的数据。新的表单页面首先会直接含有用户先前输入的信息(如果让用户重新输入这些信息会很令人沮丧!)。

为了实现这一点,需要把当前值嵌入到更新过的表单中。在第26行,给name添加了一个值。如果给出这个值,则会把它插入name字段中。显然,在初始表单页面上它是空值。在第37~38行,根据当前选定的朋友数目设置了单选按钮。最后,通过第48行和第52~54行更新了的doResults()函数,创建了这个包含已有信息的链接,让用户返回还有相关信息的表单页面。

第61行

最后,为了美观而添加一个简单的特性。在friendsA.py 和friendsB.py的界面中,可以看到返回结果和用户的输入一字不差。如果查看friendsA.py和friendsB.py中的相关代码,会发现其中是直接将名字显示出来。这意味着如果用户名字全部是小写,则最终显示的也是全小写。因此添加了str.title()函数,用来自动将用户名称的首字母转换成大写。字符串方法title()可以完成这个任务。虽然不一定需要这个功能,但至少让读者知道这个功能的存在。

图10-7~图10-10显示了用户和CGI表单及脚本的交互过程。

在图10-7中,调用friendsC.py生成表单页面。输入“fool bar”,但故意忘记选择单选按钮。单击Submit按钮后将会返回错误页面,如图10-8所示。

Greeting in English, Spanish, - 图13 图10-7 Friends的初始表单页面,没有选择朋友个数

Greeting in English, Spanish, - 图14 图10-8 无效的用户输入导致的错误页面

我们单击Back按钮,然后单击50单选按钮,并重新提交表格,其产生的页面如图10-9所示。该页面看起来很熟悉,但是在底面有了一个额外的链接,该链接可以将我们带回表单页面,新表单页面与原表单页面的唯一区别是由用户填写的所有数据现在成为默认设置,也就是说,数据值在表单中已经是可用的(希望你也注意到名字的首字母自动为大写),如图10-10所示。

Greeting in English, Spanish, - 图15 图10-9 含有正确输入信息的Friends结果页面

Greeting in English, Spanish, - 图16 图10-10 返回的Friends表单页面

这时用户可以更改任何一个字段并重新提交表单。

然而,作为开发者,毫无疑问会注意到表单和数据比之前复杂很多,生成的HTML页面也是这样,结果页面更是复杂。如果不想在Python代码中直接与HTML文本打交道,可以考虑一些Python包,如HTMLgen、xist或HSC。这些第三方工具专门用来从Python对象生成HTML。

最后,在示例10-5中,将列出Python 3的等价版本:friendsC3.py。

示例10-5 Python 3版本的frirendsC.py(friendsC3.py)

这个Python 3的等价版本,到底有哪些不同呢?

Greeting in English, Spanish, - 图17

Greeting in English, Spanish, - 图18

10.4 在CGI中使用Unicode

Core Python Programming或Core Python Language Fundamentals的第6章介绍了Unicode字符串的使用。其中一节给出了一个简单的例子脚本,即接受Unicode字符串,写入一个文件,并重新读出来。而这里将演示一个可以输出Unicode字串符的简单CGI脚本。这个例子中会向浏览器提供足够的信息,从而可以正确地渲染相关字符。唯一的要求是读者的计算机必须安装有对应的东亚字体来让浏览器显示相关字符。

为了使用Unicode,将编写一个CGI脚本生成一个多语言的Web页面。首先用Unicode字符串定义一些消息。这里假设读者的编辑器只能输入ASCII码。因此,非ASCII码的字符使用\u转义符输入。这样从文件或数据库中也能读取到这些消息。

Greeting in English, Spanish,

Chinese and Japanese.

UNICODE_HELLO = u"""

Hello!

\u00A1Hola!

\u4F60\u597D!

\u3053\u3093\u306B\u3061\u306F!

"""

CGI产生的第一个输出是Content-type,这是HTTP头。需要特别注意的是,这里表明了内容是以UTF-8编码进行传输的,这样浏览器才可以正确地解释内容。

print 'Content-type: text/html; charset=UTF-8\r'

print '\r'

然后输出真正的消息。先用字串符类的encode()方法先将字符串转换成UTF-8序列。

print UNICODE_HELLO.encode('UTF-8')

可以在示例10-6中查看相关代码,输出结果如图10-11中浏览器窗口所示。

示例10-6 简单的Unicode CGI示例(uniCGI.py)

这段代码会在Web浏览器中显示Unicode字符串。

Greeting in English, Spanish, - 图19

Greeting in English, Spanish, - 图20 图10-11 简单的Unicode CGI程序在Firefox上的输出结果

10.5 高级CGI

现在来看看CGI编程的高级方面。这包括cookie的使用(保存在客户端的缓存数据),同一个CGI字段的多个值,以及用multipart表单提交方式实现文件上传。为了节省篇幅,将会在同一个程序中展示这三个特性。首先来看multipart提交。

10.5.1 mulitipart表单提交和文件上传

目前,CGI 特别指出只允许两种表单编码:“application/x-www-form-urlencoded”和“multipart/form-dat”。由于前者是默认的,因此就没有必要像下边那样在 FORM 标签里声明编码方式。

<FORM enctype="application/x-www-form-urlencoded" …>

但是对于multipart表单,需要像下面这样明确给出编码。

<FORM enctype="multipart/form-data" …>

表单提交时可以使用任意一种编码,但在上传文件时只能使用multipart编码。multipart编码是由网景在早期开发的,现今主流浏览器都采用这个编码。

通过使用输入文件类型完成文件上传。

<INPUT type=file name=…>

这个指令显示一个空的文本框,同时旁边有个按钮,可以通过该按钮浏览文件目录结构,找到要上传的文件。在使用multipart编码时,客户端提交到服务器端的表单看起来会很像带有附件的(multipart)email 消息。同时需要对文件单独编码,因为程序没有聪明到使用“urlencode”来对文件自动编码的程度,尤其是对一个二进制文件。这些信息仍然会到达服务器,只是以一种不同的“封装”形式而已。

不论使用的是默认编码还是 multipart 编码,cgi 模块都会以同样的方式来处理它们,在表单提交时提供键和相应的值。还可以像以前那样通过FieldStorage实例来访问数据。

10.5.2 多值字段

除了上传文件之外,还会展示如何处理多值字段。最常见的情况就是有一系列的复选框允许用户有多个选择。每个复选框都会指向相同的字段名,但是为了区分这些复选框,会有不同的值与特定的复选框关联。

读者已经知道,在提交表单时,数据从用户端以键-值对形式发送到服务器端。当提交不止一个复选框时,就会有多个值对应同一个键。在这种情况下,cgi模块将会建立一个包含这类实例的列表,可以遍历该列表获得所有的值,而不是使用单个 MiniFieldStorage 实例持有数据。总的来说不是很麻烦。

最后,将在例子中使用cookie。如果读者对cookie还不太熟悉,可以把它们看成Web站点服务器要求保存在客户端(如浏览器)上的二进制数据。

由于HTTP是一个“无状态信息”的协议,因此就像在本章最开始看到的截图一样,信息通过GET请求中的键值对来从一个页面传递到另一个页面。还有另一种方法,前面已经见到过,即使用隐藏的表单字段,如较新的friends*.py脚本中的action变量。这些变量和值由服务器托管,因为这些信息必须嵌入到新生成的页面中并返回给客户端。

另一种可以保持多个页面浏览连续性的方法就是在客户端保存这些数据。这就是引进cookie的原因。服务器向客户端发送一个请求来保存cookie,而不必用在返回的Web页面中嵌入数据的方法来保持数据。cookie连接到最初服务器的主域上(这样一个服务器就不能设置或者覆盖其他Web站点中的cookie),并且有一定的存储期限(因此浏览器不会堆满cookie)。

这两个属性是通过有关数据条目的键-值对和cookie联系在一起的。cookie还有一些其他的属性,如域子路径、cookie安全传输请求。

有了cookie,就不必为了将数据从一页传到另一页而跟踪用户了。虽然这在隐私问题上也引发了大量的争论,但是多数Web站点认真负责地使用了cookie。为了准备代码,在客户端获得请求文件前,Web服务器向客户端发送“Set-Cookie”头文件要求客户端存储cookie。

一旦在客户端建立了cookie,HTTP_COOKIE环境变量会将那些cookie自动放到请求中发送给服务器。cookie是以分号分隔的键值对存在的,即以分号(;)分隔各个键值对,每个键值对中间都由等号(=)分开。为了访问这些数据,应用程序需要多次拆分这些字符串(也就是说,使用str.split()或者手动解析)。

和multipart编码一样,cookie同样起源于网景,网景制定出第一个cookie规范并沿用至今。在下边的Web站点中可以访问这些文档。

http://www.netscape.com/newsref/std/cookie_spec.html

在cookie标准化后,这个文档就被废除了,读者可以从评论请求文档(RFC)中获得现在的更多信息。现今最新的cookie文件是1997年发布的RFC 2109。2000年发布了RFC 2965用来替代RFC 2109。最新的是2011年4月发布的RFC 6265,替换了之前的两个 [1]

现在展示CGI应用程序advcgi.py,它的代码和功能与本章前面介绍的friendsC.py差别不是很大。默认的第一页是用户填写的表单,由4个主要部分组成:用户设置的cookie字符串、姓名字段、编程语言复选框列表、文件提交框。在图10-12中可以看到截图,其中包含一些示例输入信息。

这些数据以mutipart编码提交到服务器端,在服务器端以同样的方式用FieldStorage实例获取。唯一不同的就是对上传文件的检索。在这个应用程序中,选择的是逐行读取、遍历文件。如果不介意文件大小,也可以一次读入整个文件。

由于这是服务器端第一次接到数据,因此正是此时,在向客户端返回结果页面时使用“Set-Cookie:”头文件来捕获浏览器端的cookie。

Greeting in English, Spanish, - 图21 图10-12 高级CGI cookie、文件上传及多值表单页面

在图10-13中,可以看到数据提交后的结果。用户输入的所有字段都可以在页面中显示出来。在最后对话框中指定的文件也上传到了服务器端,并显示出来。

读者也会注意到在结果页面下方的那个链接,这里使用了相同的 CGI 脚本来返回表单页面。

如果单击下方的那个链接,就不会向脚本提交任何表单数据,而是会显示一个表单页面。然而,如图10-14所示,之前填写的所有信息可以显示出来,而不是一个空表单!在没有表单数据的情况下是怎样做到这一点的呢(要么使用隐藏字段,要么作为URL中的查询参数)?原因是这些数据都保存在客户端的cookie中了。

用户的cookie将用户输入表单中的值都保存了起来,用户名、使用的语言、上传文件的信息都会存储在cookie中。

当脚本检测到表单没有数据时,它会返回一个表单页面,但是创建在表单页面前,它会从客户端的cookie中抓取数据(当用户在单击了那个链接的时候将会自动传入)并且将其填入相应的表单项中。因此当表单最终显示出来时,先前输入的信息便会魔术般地显示在用户面前。

Greeting in English, Spanish, - 图22 图10-13 高级CGI应用程序的结果页面

Greeting in English, Spanish, - 图23 图10-14 新表单页面,其中含有从cookie中载入的数据,但没有上传过的文件

相信读者现在已经迫不及待地想查看这个程序了,详见示例10-7。

示例10-7 高级CGI应用(advcgi.py)

这段代码中有个做了较多工作的主类AdvCGI。其中包括用于显示表单、错误消息或结果页面的方法,以及用来在客户端(即Web浏览器)之间读写cookie的方法。

Greeting in English, Spanish, - 图24

Greeting in English, Spanish, - 图25

Greeting in English, Spanish, - 图26

Greeting in English, Spanish, - 图27

advcgi.py和本章前部分提到的CGI脚本friendsC.py非常像,可以返回表单页面、结果页面、错误页面。除了新的脚本中所有的高级CGI特性外,还在脚本中增加了更多的面向对象特性,即用类和方法代替了一系列的函数。对类来说,页面的HTML文本是静态数据,意味着它们在实例中都是以常量出现的,虽然这里仅有一个实例。

逐行解释

第1~6行

这里是普通的起始行和模块导入行。唯一可能不太熟悉的模块是StringIO类。这是一个类似文件的数据结构,其核心元素是字符串,可以理解为内存中的文本流。

在Python 2中,可以在StringIO或等价的cStringIO模块中找到这些类。在Python 3中,这些类移到了 io 模块中。与之类似,Python 2 中的 urllib.quote()和 urllib.unquote()函数在Python3中移到了urllib.parse包中。

第8~28行

在声明AdvCGI类之后,创建了header和url(静态类)变量,在显示不同页面的方法中会用到这些变量。下面是HTML静态文本表单,其中含有编程语言设置和每种语言的HTML元素。

第33~55行

这个例子用到了cookie。下面还有setCPPCookies()方法,应用程序会调用这个方法来发送cookie(从Web服务器)到浏览器,并存储在浏览器中。

getCPPCookies()所做的刚好相反。当浏览器对应用进行连续调用时,这个方法将相同的cookie通过HTTP头发送回服务器。在应用执行时,应用可以通过HTTP_COOKIE环境变量访问到这些值。

这个方法解析cookie,特别是寻找以CPP开头的字符串(第37行)。在这个例子中,只查找名为“CPPuser”和“CPPinfo”的cookie。键“user”和“info”在第38行提取为标签。跳过了索引7处的等号。在第39~42行去除了索引8处的值并进行计算,计算结果保存到Python对象中。异常处理程序查看cookie负载,对于非法的Python对象,仅仅是保存相应的字符串值。如果这个cookie丢失,就会给它指定一个空字符串(第43~48行)。getCPPCookies()方法只会被showForm()调用。

在这个简单的例子中自行解析 cookie,但对于复杂的应用,一般使用 Cookie 模块(在Python 3中重命名为http.cookies)来完成这个任务。

与之类似,如果在编写Web客户端,需要管理浏览器存储的所有cooki(e 一个cookie jar),并与Web服务器通信,可能会需要用到cookielib模块(在Python 3中重命名为http.cookiejar)。

第57~76行

showForm()和doReuslts()都会调用checkUserCookie()方法,用来检查是否设置了用户提供的cookie值。表单和结果的HTML模板都会显示这个值。

showForm()方法唯一的目的是将表单显示给用户。这个方法需要getCPPCookies()从之前的请求中(如果有)获取cookie,并适当地调整表单的格式。

第78~87行

这块代码用来生成错误页面。

第89~101行

这只是个用于结果页面的HTML模板。doResults()会用到这些代码,用于填充所有需要的数据。

第102~135行

这一块代码会创建结果页面。setCPPCookies()方法请求客户端为应用程序存储 cookie, doResults()方法将所有数据放在一起发送回客户端。

go()方法会调用 doResults(),用于处理主要任务,以输出数据。在这个方法的第一部分(第109~119行),用于处理用户数据,即选择的编程语言集(至少需要选择一个,详见go()方法)、上传的文件以及用户提供的cookie值,后两者都是可选的。

doResults()的最后一步(第128~135行)将所有数据打包到单个“CPPinfo”cookie中,为后面做准备,并根据数据渲染结果模板。

第137~180行

这段代码首先实例化一个 AdvCGI 页面对象,接着调用其中的 go()方法开始工作。go()方法用于读取所有将要到达的数据并确定显示哪个页面。

如果没有给出名字或选定语言,则会显示错误页面。如果没有收到任何输入数据,将调用 showForm()方法来输出表单;否则,将调用 doResults()方法来显示结果页面。通过设置self.error变量可以创建错误页面,这样做有两个目的。一是可以将错误原因设置在字符串里,二是可以作为一个标记表明有错误发生。如果该变量不为空,用户会被导向到错误页面。

person字段是一个键值对,处理方法(第145~150行)和先前看到的一样。但在收集语言信息时(第153~160行)需要一点技巧,原因是必须检查一个(Mini)FieldStorage实例或一个包含该实例的列表。这里将使用熟悉的 isinstance()内置函数来达到目的。最终,会获得单个或多个语言名的列表,具体依赖于用户的选择情况。

如果使用cookie来保管数据,就可以避免使用任何类型的CGI字段。在本章之前的示例中,将这些值作为CGI变量传递。而现在只使用cookie。读者会注意到获取这些数据的代码没有调用CGI处理,这意味着数据并非来自FieldStorage对象。Web客户端的每次请求都会发送相应的数据,其中的值从cookie中获得(包括用户的选择结果和用来填充后续表单的已有信息)。

因为showResults()方法从用户那里取得了新的输入值,所以该方法负责设置cookie,例如,通过调用setCPPCookies()。而showForm()必须读出cookie中的值才能用表单页显示用户的当前选项。这通过对getCPPCookies()的调用来实现。

最后,来看看文件的上传处理(第162~171行)。不论一个文件实际上是否已经上传, FieldStorage都会从file属性中获得一个文件句柄。在第171行,如果没有指明文件名,就把它设置成空字符串。还有一个更好的做法,可以访问文件指针(即file属性),并且可以每次只读一行或者其他更慢一些的处理方法。

在这个例子里,文件上传只是用户提交过程的一部分,所以只是简单地把文件指针传给doResults()函数,从文件中抽取数据。由于受到空间限制,doResults()将只显示文件的前 4KB内容(第112行),完全没有必要显示一个4GB的完整二进制文件。

如果读者读过本书之前的版本,会发现这里对以往的例子进行了大规模的重构。以前的例子很老,没有反映出当前Python中的做法。这个改进版的advcgi.py不能在2.5之前版本的 Python 中运行。但仍可以从本书的网站中看到之前版本的代码,以及对应的Python 3版本。

10.6 WSGI简介

本节介绍WSGI的所有内容,首先介绍使用动机和背景。本节第二部分将介绍如何编写Web应用,而无须在意这个应用是怎么执行的。

10.6.1 动机(替代CGI)

现在读者已经对CGI有深入的了解,并知道为什么需要CGI。因为服务器无法创建动态内容,它们不知道用户特定的应用信息和数据,如验证信息、银行账户、在线支付等。Web服务器必须与外部的进程通信才能处理这些自定义工作。

本章前2/3部分讨论了CGI如何解决这个问题,同时也介绍了其工作原理。但是这种方式无法扩展,CGI进程(类似Python解释器)针对每个请求进行创建,用完就抛弃。如果应用程序接收数千个请求,创建大量的语言解释器进程很快就会导致服务器停机。有两种方法可以解决这个问题,一是服务器集成,二是外部进程。下面分别介绍这两种方法。

10.6.2 服务器集成

服务器集成,也称为服务器API。这其中包括如Netscape服务器应用编程接口(NSAPI)和微软的因特网服务器编程接口(ISAPI)这样的商业解决方案。当前(从20世纪90年代中期开始)应用最广泛的服务器解决方案是Apache HTTP Web服务器,这是一个开源的解决方案,通常称为Apache,拥有一个服务器API。另外,使用术语模块来描述服务器上插入的编译后的组件,这些组件可以扩展服务器的功能和用途。

所有这三个针对CGI性能的解决方案都将网关集成进服务器。换句话说,不是将服务器切分成多个语言解释器来分别处理请求,而是生成函数调用,运行应用程序代码,在运行过程中进行响应。服务器根据对应的API通过一组预先创建的进程或线程来处理工作。大部分可以根据所支持应用的需求进行相应的调整。例如,服务器一般还会提供压缩数据、安全、代理、虚拟主机等功能。

当然,任何方案都有缺点,对于服务器API,这种方式会带来许多问题,如含有bug的代码会影响到服务器实现执行效率,不同语言的实现无法完全兼容,需要API开发者使用与Web服务器实现相同的编程语言,应用程序需要整合到商业解决方案中(如果没有使用开源服务器API),应用程序必须是线程安全的,等等。

10.6.3 外部进程

另一个解决方案是外部进程。这是让CGI应用在服务器外部运行。当有请求进入时,服务器将这个请求传递到外部进程中。这种方式的可扩展性比纯CGI要好,因为外部进程存在的时间很长,而不是处理完单个请求后就终止。使用外部进程最广为人知的解决方案是FastCGI。有了外部进程,就可以利用服务器API的好处,同时避免了其缺点。比如,在服务器外部运行就可以自由选择实现语言,应用程序的缺陷不会影响到Web服务器,不需要必须与闭源的商业软件结合起来。

很自然,FastCGI有Python实现,除此之外还有Apache的其他Python模块(如PyApache、mod_snkae、mod_python 等),其中有些已经不再维护了。所有这些模块加上纯 CGI 解决方案,组成了各种Web服务器API网关解决方案,以调用 Python Web应用程序。

由于使用了不同的调用机制,所以开发者有了新的负担。不仅要开发应用本身,还要决定与Web服务器的集成。实际上,在编写应用时,就需要完全知道最后会使用哪个机制,并以相应的方式执行。

对于Web框架开发者,问题就更加突出了,由于需要给予用户最大的灵活性。如果不想强迫他们开发多版本的应用,就必须为所有服务器解决方案提供接口,以此来让更多的用户采用你的框架。这个困境看起来绝不是Python的风格,就导致了Web服务器网类接口(Web Server Gateway Interface,WSGI)标准的建立。

10.6.4 WSGI简介

WSGI 不是服务器,也不是用于与程序交互的 API,更不是真实的代码,而只是定义的一个接口。WSGI规范作为PEP 333于2003年创建,用于处理日益增多的不同Web框架、Web服务器,及其他调用方式(如纯CGI、服务器API、外部进程)。

其目标是在Web服务器和Web框架层之间提供一个通用的API标准,减少之间的互操作性并形成统一的调用方式。WSGI 刚出现就得到了广泛应用。基本上所有基于 Python 的Web服务器都兼容WSGI。将WSGI作为标准对应用开发者、框架作者和社区都有帮助。

根据WSGI定义,其应用是可调用的对象,其参数固定为以下两个:一个是含有服务器环境变量的字典,另一个是可调用对象,该对象使用HTTP状态码和会返回给客户端的HTTP头来初始化响应。这个可调用对象必须返回一个可迭代对象用于组成响应负载。

在下面这个WSGI应用的“Hello World”示例中,这些内容分别命名为environ变量和start_response()。

def simple_wsgi_app(environ, start_response):

status = '200 OK'

headers = [('Content-type', 'text/plain')]

start_response(status, headers)

return ['Hello world!']

environ 变量包含一些熟悉的环境变量,如 HTTP_HOST、HTTP_USER_AGENT、SERVER_PROTOCOL等。而start_response()这个可调用对象必须在应用执行,生成最终会发送回客户端的响应。响应必须含有HTTP返回码(200、300等),以及HTTP响应头。

在这个第1版的WSGI标准中,start_response()还应该返回一个write()函数,以便支持遗留服务器,此时生成的是数据流。建议使用WSGI时只返回可迭代对象,让Web服务器负责管理数据并返回给客户端(而不是让应用程序处理这些不精通的事情)。由于这些原因,大多数应用并不使用或保存start_response()的返回值,只是简单将其抛弃。

在前面的例子中,可以看到其中设置了200状态码,以及Content-Type头。这些信息都传递给 start_response(),来正式启动响应。返回的内容必须是可迭代的,如列表、生成器等,它们生成实际的响应负载。在这个例子中,只返回含有单个字符串的列表,但其实可以返回更多数据。除了返回列表之外,还可以返回其他可迭代对象,如生成器或其他可调用实例。

关于start_response()最后一件事是其第三个参数,这是个可选参数,这个参数含有异常信息,通常大家知道其缩写exc_info。如果应用将HTTP头设置为“200 OK”(但还没有发送),并且在执行过程中遇到问题,则可以将 HTTP 头改成其他内容,如“403 Forbidden”或“500 Internal Server Error”。

为了做到这一点,可以假设应用使用一对正常的参数开始执行。当发生错误时,会再次调用start_response(),但会将新的状态码与HTTP头和exc_info一起传入,替换原有的内容。

如果第二次调用时 start_response()没有提供 exc_info,则会发生错误。而且必须在发送HTTP头之前第二次调用start_response()。如果发送完HTTP头才调用,则必须抛出一个异常,抛出类似exc_info[0]、exc_info[1]或exc_info[2]等内容。

关于 start_response()可调用对象的更多内容,请参考 http://www.python.org/dev/peps/pep-0333/#the-start-response-callable中的PEP 333。

10.6.5 WSGI服务器

在服务器端,必须调用应用(前面已经介绍了),传入环境变量和start_response()这个可调用对象,接着等待应用执行完毕。在执行完成后,必须获得返回的可迭代对象,将这些数据返回给客户端。在下面这段代码中,给出了一个具有简单功能的例子,这个例子演示了WSGI服务器看起来会是什么样子的。

import StringIO

import sys

def run_wsgi_app(app, environ):

body = StringIO.StringIO()

def start_response(status, headers):

body.write('Status: %s\r\n' % status)

for header in headers:

body.write('%s: %s\r\n' % header)

return body.write

iterable = app(environ, start_response)

try:

if not body.getvalue():

raise RuntimeError("start_response() not called by app!")

body.write('\r\n%s\r\n' % '\r\n'.join(line for line in iterable))

finally:

if hasattr(iterable, 'close') and callable(iterable.close):

iterable.close()

sys.stdout.write(body.getvalue())

sys.stdout.flush()

底层的服务器/网关会获得开发者提供的应用程序,将其与 envrion 字典放在一起, envrion字典含有os.environ()中的内容,以及WSGI相关的wsig.*环境变量(参见PEP,但不包括wsgi.input、wsig.errors、wsgi.version等元素),以及任何框架或中间件环境变量(下面会引入更多中间件)。使用这些内容来调用run_wsgi_app(),该函数将响应传送给客户端。

事实上,应用开发者不会在意这些细节。如创建一个提供WSGI规范的服务器,并为应用程序提供一致的执行框架。从前面的例子中可以看到,WSGI 在应用端和服务器端有明显的界线。任何应用都可以传递到上面描述的服务器(或任何其他WSGI服务器)中。同样,在任何应用中,无须关心哪种服务器会调用这个应用。只须在意当前的环境,以及将数据返回给客户端之前需要执行的start_response()可调用对象。

10.6.6 参考服务器

前面提到过,不应该强迫应用开发者编写服务器,所以不应该创建并编写类似run_wsgi_app()这样的代码,而是应该能够选择任何WSGI服务器,如果都不行,Python在标准库中提供了简单的参考服务器:wsgiref.simple_server.WSGIServer。

可以用这个类直接构建一个服务器。然而,wsgiref 包提供了一个方便的函数,即make_server(),通过这个函数可以部署一个用于简单访问的参考服务器。下面的示例应用(simple_wsgi_app())完成了这个任务。

!/usr/bin/env python

from wsgiref.simple_server import make_server

httpd = make_server('', 8000, simple_wsgi_app)

print "Started app serving on port 8000…"

httpd.serve_forever()

这里获得前面创建的应用,将 simple_wsgi_app()封装到服务器中并在 8000 端口运行,启动服务器循环。如果在浏览器(或其他可以访问[host, port]对的工具)中访问http://localhost:8000,可以看到以纯文本形式的“Hello World!”输出。

对于懒人来说,无须编写应用或服务器。wsgiref 模块还有一个示例应用,即wsgiref.simple_server.demo_app()。这个demo_app()与simple_wsgi_app()几乎相同,只是其还会显示环境变量。下面是在参考服务器中运行这个示例应用的代码。

!/usr/bin/env python

from wsgiref.simple_server import make_server, demo_app

httpd = make_server('', 8000, demo_app)

print "Started app serving on port 8000…"

httpd.serve_forever()

启动一个CGI服务器,接着浏览应用,应该可以看到“Hello World!”输出以及环境变量转储。

这只是兼容WSGI服务器的参考模型。它不具有完整功能或也不打算在生产环境中使用,但服务器创建者可以以此为蓝本,创建自己的兼容 WSGI 的服务器。应用开发者可以将demo_app()当作参考,来实现兼容WSGI的应用。

10.6.7 WSGI应用示例

前面已经提到,WSGI现在已经是标准了,几乎所有Python Web框架都支持它,虽然有些从表面上看不出来。例如,Google App Engine处理程序类,在正常导入后,可能看到如下代码。

class MainHandler(webapp.RequestHandler):

def get(self):

self.response.out.write('Hello world!')

application = webapp.WSGIApplication([

('/', MainHandler)], debug=True)

run_wsgi_app(application)

不是所有框架都是这种模式,但可以清楚地看到WSGI的引用。为了进一步比较,可以深入底层,在App Engine的Python SDK中,以及在webapp子包的util.py模块中,可以看到run_bare_wsgi_app()函数。其中的代码与simple_wsgi_app()非常像。

10.6.8 中间件及封装WSGI应用

在某些情况下,除了运行应用本身之外,还想在应用执行之前(处理请求)或之后(发送响应)添加一些处理程序。这就是熟知的中间件,它用于在 Web服务器和 Web应用之间添加额外的功能。中间件要么对来自用户的数据进行预处理,然后发送给应用;要么在应用将响应负载返回给用户之前,对结果数据进行一些最终的调整。这种方式类似洋葱结构,应用程序在内部,而额外的处理层在周围。

预处理可以包括动作,如拦截、修改、添加、移除请求参数,修改环境变量(包括用户提交的表单(CGI)变量),使用 URL 路径分派应用的功能,转发或重定向请求,通过入站客户端 IP 地址对网络流量进行负载平衡,委托其功能(如使用User-Agent 头向移动用户发送简化过的UI/应用),以及其他功能。

而后期处理主要包括调整应用程序的输出。下面这个示例类似第2章创建的时间戳服务器,其中对于应用返回的每一行结果,都会添加一个时间戳。在实际中,这会更加复杂,但大致方式都相同,如添加将应用的输出转为大写或小写的功能。这里使用ts_simple_wsgi_app()封装了simple_wsgi_app(),将前者作为应用注册到服务器中。

!/usr/bin/env python

from time import ctime

from wsgiref.simple_server import make_server

def ts_simple_wsgi_app(environ, start_response):

return ('[%s] %s' % (ctime(), x) for x in \

simple_wsgi_app(environ, start_response))

httpd = make_server('', 8000, ts_simple_wsgi_app)

print "Started app serving on port 8000…"

httpd.serve_forever()

如果需要进行更多的处理工作,可以使用类封装,而不是前面的函数封装。此外,由于添加了封装的类和方法,因此还可以将environ和start_response()整合到一个元组变量中,使用这个元组作为参数来减少代码量(见下面示例中的stuff)。

class Ts_ci_wrapp(object):

def init(self, app):

self.orig_app = app

def call(self, *stuff):

return ('[%s] %s' % (ctime(), x) for x in

self.orig_app(*stuff))

httpd = make_server('', 8000, Ts_ci_wrapp(simple_wsgi_app))

print "Started app serving on port 8000…"

httpd.serve_forever()

这个类命名为Tsciwrapp,这是“timestamp callable instance wrapped application”的简称,该类会在创建服务器时实例化。其初始化函数将原先的应用作为输入,并缓存它以备后用。当服务器执行应用程序时,和之前一样,它依然传入environ字典和start_response()可调用对象。由于做了一些改动,程序会调用示例本身(因为定义了__call()方法)。environ 和start_response()都会通过stuff传递给原先的应用。

尽管在这里使用了可调用实例,而前面使用的是函数,但也可以使用其他可调用对象。还要记住,后面几个例子都没有以任何方式修改simple_wsgi_app()。WSGI的主旨是在Web应用和Web服务器之间做了明显的分割。这样有利于分块开发,让团队更方便地划分任务,让Web应用能以一致且灵活的方式在任何兼容WSGI的后端中运行。同时无论用户使用(Web)服务器软件运行什么应用程序,Web服务器开发者都无须处理任何自定义或特定的hook。

10.6.9 在Python 3中使用WSGI

PEP 333为Python 3定义了WSGI标准。PEP 3333是PEP 333的增强版,为Python 3带来了WSGI标准。具体来说,就是所有网络流量以字节形式传输。在Python 2中,字符串原生是用字节表示的,而在Python 3中,字符串Unicode用来表示文本数据,而原先的ASCII字符串重命名为bytes类型。

具体来说,PEP 3333将原生字符串(即str类型,不管是Python 2还是Python 3的str)用于所有 HTTP 头和对应的元数据中。而将“byte”字符串用在 HTTP 负载(如请求/响应、GET/POST/PUS输入数据、HTML输出等)中。关于PEP 3333的更多信息,请参考其定义,可以在www.python.org/dev/peps/pep-3333/中找到它。

还有其他独立于PEP 3333的相关提议值得一读。其中一个是PEP 444,这是首次尝试定义“WSGI 2”标准。社区将PEP 3333看作“WSGI 1.0.1”,即原来PEP 333规范的增强版,而将PEP 444看作下一代WSGI。

10.7 现实世界中的Web开发

CGI是过去用来开发Web的方式,其引入的概念至今仍应用于Web开发当中。因此,这就是为什么在这里花时间学习CGI。而对WSGI的介绍让读者更接近真实的开发流程。

今天,Python Web开发新手的选择余地很多。除了著名的Django、Pyramid和Google App Engine这些Web框架之外,用户还可以在其他众多的框架中选择。实际上,框架甚至都不是必需的,直接使用Python,不用任何其他额外的工具或框架特性,就能开发出兼容WSGI的Web服务器。但最好继续使用框架,这样可以方便地用到框架提供的其他Web功能。

现代的Web执行环境一般由多线程或多进程模型、认证/安全cookie、基本的用户验证、会话管理组成。普通应用程序的开发者都会了解这其中大部分内容。验证表示的是用户通过用户名和密码进行登录,cookie用来维护用户信息,会话管理有时候也是如此。为了使应用具有可扩展性,Web服务器应当能够处理多个用户的请求。因此,需要用到多线程或多进程。但会话在这里还没有完全涉及。

如果读者阅读本章中运行在服务器上的所有应用程序代码,那么就需要说明一下,脚本从头到尾执行一次,服务器循环永远执行,Web应用(Java中称为servlet)针对每个请求执行。代码中不会保存状态信息,前面已经提到过,HTTP 是无状态的。换句话说,数据是不会保存到局部或全局变量中,或以其他方式保存起来。这就相当于把一个请求当成一个事务。每次来了一个事务,就进行处理,处理完就结束,在代码库中不保存任何信息。

此时就需要用到会话管理,会话管理一段时间内在一个或多个请求之间保存用户的状态。一般来说,这是通过某种形式的持久存储完成的,如缓存、平面文件 [2]甚至是数据库。开发者需要自己处理这些内容,特别是编写底层代码时,本章中已经见到过这些代码。毫无疑问,已经做了很多无用功,因此许多著名的大型Web框架,包括Django,都有自己的会话管理软件(下一章将介绍Django的相关内容。)

10.8 相关模块

表10-1列出了对Web开发有用的模块。还可以参考第3章和第13章介绍的模块,这些对Web应用都很有用。

表10-1 Web编程相关模块 Greeting in English, Spanish, - 图28 ① Python 1.6中新增。

② Python 2.0中新增。

③ Python 2.2中新增。

④ Python 2.4中新增。

⑤ Python 2.5中新增。

⑥ Python 3.0中新增。

10.9 练习

CGI和Web应用

10-1 urllib模块和文件。更新 friendsC.py脚本,让其可以访问磁盘里的文件,以姓名和朋友数量各为一列存储相关信息,并且可以在每次运行脚本的时候持续添加姓名。

选做题:增加一些代码把这种文件的内容转储到Web浏览器里(以HTML格式)。

另一个选做题:增加一个链接,用于清空文件中的所有名字。

10-2 错误检查。friendsC.py 脚本在没有选择任意一个单选按钮指定朋友的数目时会报告一个错误。更新CGI脚本,在如果没有输入名字(如空字符或空白)时也会报告一个错误。

选做题:目前为止探讨的仅是服务器端的错误检查。了解 JavaScript 编程,并通过创建 JavaScript 代码来同时检查错误,以确保这些错误在到达服务器前终止,这样便实现了客户端错误检查。

10-3 简单CGI。为Web站点创建“Comments”或“Feedback”页面。由表单获得用户反馈,在脚本中处理数据,最后返回一个“thank you”页面。

10-4 简单CGI。创建一个Web客户簿。接受用户输入的名字、e-mail地址、日志项,并将其保存到文件中(格式自选)。类似上一练习,返回一个“thanks for filling out a guestbooks entry”页面。同时再给用户提供一个查看客户簿的链接。

10-5 Web浏览器cookie和Web站点注册。为Web站点添加用户验证服务。使用加密的方式管理用户姓名和密码。读者也许在Core Python Programming或Core Python Language Fundamentals中用纯文本文件的方式完成了练习,这里可以使用已完成练习的部分代码。

选做题:熟悉Web浏览器cookie,并在最后登录成功后将会话保持4个小时。

选做题:允许通过OpenID进行联合验证,允许用户通过Google、Yahoo!、AOL、WordPress,甚至是其他专有验证系统,如“Facebook Connect”或“通过Twitter登录”来登录系统。还可以用Google Identity Toolki(t 从http://code.google.com/apis/identitytoolkit下载)。

10-6 错误消息。当一 个CGI脚本崩溃时发生了什么?如何用cgitb模块检测错误消息?

10-7 CGI、文件更新及Zip文件。创建一个CGI应用,它不仅能将文件保存到服务器磁盘中,而且能智能解压Zip文件(或其他存档文件)到同名子文件夹中。

10-8 Web数据库应用程序。思考Web数据库应用程序所需的数据库构架。对于多用户的应用程序,需要授予每个用户访问数据库全部内容的权限,但每次只能有一个用户拥有写入权限。家人及亲属的“地址簿”就是这样一个例子。每个成员成功登录后,显示出来的页面应该有几个选项:添加条目,查看、更新、移除或删除自己的条目,以及查看整个条目(整个数据库)。

设计UserEntry类,为该类的每个实例创建一个数据库项。读者可以使用前面练习中的任何代码来实现这个注册框架。最后,使用任意存储机制的数据库,如MySQL这样的关系数据库,或更简单的Python持久存储模块,如anydbm或shelve。

10-9 电子商务引擎。建立一个通用的电子商务/在线购物Web服务站点,并可以改进以支持多用户。添加验证系统,以及表示用户和购物车的类(如果读者有Core Python Programming或Core Python Language Fundamentals,可以使用“面向对象编程”一章中为练习4和练习11编写的代码。同时还需要管理商品,无论是可见的货物还是虚拟的服务。还需要添加PayPal或Google提供的付款系统。在学习后续几章后,将这个临时的CGI解决方案移植到Django、Pyramid或Google App Engine中。

10-10 Python 3。检查friendsC.py和friendsC3.py之间的区别。描述每个改动。

10-11 Python 3、Unicode/Text与Data/Bytes。将Unicode示例(即uniCGI.py)移植到Python 3中。

WSGI

10-12 背景知识。什么是WSGI?为什么要创建WSGI?

10-13 背景知识。有哪些技术可以弥补CGI可扩展性方面的问题?

10-14 背景知识。举例说明哪些著名的框架是兼容WSGI的,再了解一下哪些框架不支持WSGI。

10-15 背景知识。WSGI和CGI有什么区别?

10-16 WSGI应用。WSGI应用可以是哪些类型的Python对象?

10-17 WSGI应用。WSGI应用程序需要哪两个参数?详细了解第二个参数。

10-18 WSGI应用。WSGI应用可能会返回哪些类型的数据?

10-19 WSGI应用。练习 10-1~练习10-11的解决方案只可用于以CGI方式处理表单数据的服务器。选择其中一个练习,将其导入 WSGI 中,这样就可以在任何兼容WSGI的服务器上运行,也许只需一点点修改。

10-20 WSGI服务器。10.6.5节介绍的WSGI服务器提供了一个简单的run_wsgi_app()服务器函数它,它用来执行WSGI应用程序。

a)这个 run_bare_wsgi_app()函数目前不支持可选的第三个参数 exc_info。学习PEP 333和3333,添加对exc_info的支持。

b)将这个函数移植到Python 3中。

10-21 案例研究。用下列Python Web框架实现Web应用:Werkzeug、WebOb、Django、Google App Engine,并与用WSGI方式实现的Web应用进行对比。

10-22 标准。PEP 3333针对Python 3,包含了对PEP 333的说明和加强。PEP 444是另外一个提议。描述PEP 444的内容,以及与已有PEP的关系。