20.7 高级CGI

现在我们来看看CGI编程的高级方面。这包括cookie的使用(保存在客户端的缓存数据),同一个CGI字段的多重值和用multipart表单实现的文件上传。为了节省空间,我们将会在同一个程序中向你展示这三个特性。首先让我们看一下多次提交问题。

20.7.1 Mulitipart表单提交和文件的上传

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

20.7 高级CGI - 图1

但是对于multipart表单,你需要像这样明确给出编码:

20.7 高级CGI - 图2

在表单提交时你可以使用任一种编码,但在目前上传的文件仅能表现为multipart编码。Multipart编码是由网景在早期开发的,但是已经被微软(开始于IE4)和其他浏览器采用。

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

20.7 高级CGI - 图3

这个指令表现为一个空的文本字段,同时旁边有个按钮,可以让你浏览文件目录系统,找到要上传的文件。在使用multipart编码时,你客户端提交到服务器端的表单看起来会很像带有附件的email。同时还需要有一个单独的编码,因为它还没有聪明到”通过URL编码”的程度,尤其是对一个二进制文件。这些信息仍然会到达服务器,只是以一种不同的”封装”形式而已。

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

20.7.2 多值字段

除了上传文件,我们将会展示如何处理具有多值的字段。最常见的情况就是你有一系列的复选框允许用户有多个选择。每个复选框都会标上相同的字段名,但是为了区分它们,会有不同的值与特定的复选框关联。

正如你所知道的,在表单提交时,数据从用户端以键-值对形式发送到服务器端。当提交不止一个复选框时,就会有多个值对应同一个键。在这种情况下,cgi模块将会建立一个这类实例的列表,你可以遍历获得所有的值,而不是为你的数据指定一个MiniFielStorage实例。总的来说不是很痛苦。

20.7.3 cookie

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

由于HTTP是一个”无状态信息”的协议,如你在本章最开始看到的截图一样,是通过GET请求中的键值对来完成信息从一个页面到另一个页面的传递。实现这个功能的另外一种方法如我们以前看到的一样,是使用隐藏的表单�段,如在后期friends.py脚本中对action变量的处理。这些信息必须被嵌入新生成的页面中并返回给客户端,所以这些变量和值由服务器来管理。

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

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

有了cookie,我们不再需要为了跟踪用户而将数据从一页传到另一页了。虽然这在隐私问题上也引发了大量的争论,多数Web站点还是合理地使用了cookie。为了准备代码,在客户端获得请求文件前,Web服务器向客户端发送”SetCookie”头文件要求客户端存储cookie。

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

和multipart编码一样,cookie同样起源于网景,他们实现了cookie并制定出第一个规范并沿用至今,在下边的Web站点中你可以接触这些文档:http://www.netscape.com/newsref/std/cookie_spec.html。

一旦cookie标准化以后,这些文档最终都被废除了,你可以从评论请求文档(Request for Comment,RFC)中获得更多现在的信息。现今发布的最新的cookie的文件是RFC 2109。

20.7.4 使用高级CGI

现在我们来展示CGI应用程序advcgi.py,它的代码号功能和本章前部分讲到的friends3.py的差别不是很大。默认的第一页是用户填写的表单,它由4个主要部分组成:用户设置cookie字符串、姓名字段、编程语言复选框列表、文件提交框。在图20-14中可以看到示图。

图20-15是在另一个浏览器看到的表单效果图,在这个表单中,我们可以输入自己的信息,如图20-16中给的样式。注意查找文件的按钮在不同的浏览器中显示的文字是不同的,如“Browse…”、“Choose”、“…”等。

20.7 高级CGI - 图4

图 20-14 MacOS X系统IE5浏览器中上传及填写多值表单页

20.7 高级CGI - 图5

图 20-15 Linux系统Netscape4浏览器中的同一个高级CGI

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

由于这是服务器端第一次接到数据,这时,当我们向客户端返回结果页面时,我们使用”SetCookie:”头文件来捕获浏览器端的cookie。

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

你也会注意到在结果页面下方的那个链接,它使用相同的CGI脚本,可以帮我们返回表单页。

如果我们单击下方的那个链接,没有任何表单数据提交给我们的脚本,因此会显示一个表单页面。然而,如你在图20-17中看到的一样,所有的东西都可以显示出来,并非是一个空的表单!我们前边输入的信息都被显示出来了!在没有表单数据的情况下我们是怎样做到这一点的呢(将其隐藏或者作为URL中的请求参数)?实际上秘密是这些数据都被保存在客户端的cookie中了。

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

20.7 高级CGI - 图6

图 20-16 高级CGI提交演示,Win32系统Opera 8浏览器

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

20.7 高级CGI - 图7

图 20-17 由Web服务器生成和返回的结果页面,Win 32系统Opera 4浏览器

20.7 高级CGI - 图8

图 20-18 通过客户端cookie载入数据的表单页

我们相信你现在已经迫不及待的想看下这个程序了,详见例20.8。

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

逐行(逐块)解释

1 ~ 7行

普通的起始和模块导入行出现在这里。你可能不太熟悉的唯一模块是cStringIO,我们曾在第10章简单讲解过它并在例20.1中用过。cStringIO.StingIO()会在字符串上创建一个类似文件的对象,所以访问这个字符串与打开一个文件并使用文件句柄去访问数据很相似。

9 ~ 12行

在声明AdvCGI类之后,header和url(静态)变量被创建出来,在显示所有不同页面的方法中会用到这些变量。

14 ~ 80行

所有这个块中的代码都是用来创建、显示表单页面的。那些数据属性都是不言自明的。getCPPcookie()取得Web客户端发来的cookie信息,而showForm()校对所有这些信息并把表单页面返回给客户端。

82 ~ 91行

这个代码块负责错误页面。

93 ~ 144行

结果页面的生成使用了本块代码。setCPPcookie()方法要求客户端为我们的应用程序存储cookie,而doResults()方法聚集所有数据并把输出发回客户端。

例20.8 高级CGI应用(asvcgi.py)

这个脚本有一个处理所有事情的主函数,AdvCGI,它有方法显示表单、错误或结果页面,同时也可以从客户端(Web浏览器)读写cookie。

20.7 高级CGI - 图9

20.7 高级CGI - 图10

20.7 高级CGI - 图11

20.7 高级CGI - 图12

20.7 高级CGI - 图13

doResults()方法收集所有数据并把输出发回客户端。

146 ~ 196行

脚本一开始就实例化了一个AdvCGI页面对象,然后调用它的go()方法让一切运转起来,这和严格的基于过程编写的程序不同。go()方法中包含读取所有新到的数据并决定显示哪个页面的逻辑。

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

处理person字段(第154~159行)的方法和我们先前看到的一样,一个键-值对;然而,在收集语言信息时却需要一点技巧,原因是我们必须检查一个(Mini) FieldStorage对象或一个该对象的列表。我们将使用熟悉的type()内建函数来达到目的。最终,我们会有一个单独或多个语言名的列表,具体依赖于用户的选择情况。

使用cookie(第161~165行)来保管数据展示了如何利用它们来避免使用任何类型的CGI字段。你一定注意到了代码里包含这些数据的地方没有调用CGI处理,这意味着数据并非来自FieldStorage对象。这些数据是由Web客户端通过每一次请求和从cookie取得的值(包括用户的选择结果和用来填充后续表单的已有信息)传给我们的。

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

最后,我们看看文件上传处理(第178~187行)。不论一个文件是否已经上传,FieldStorage都会从file属性中获得一个文件句柄。在第180行,如果没有指明文件名,那么我们只须把它设成空字符串。如果访问过value属性,那么文件的整个内容都会被放到value里。还有一个更好的做法,你可以去访问文件指针——file属性——并且可以每次只读一行或者其他更慢一些的处理方法。

在我们的例子里,文件上传只是用户提交过程的一部分,所以我们可以简单地把文件指针传给doResults()函数,从文件中抽取数据。由于空间限制doResults()将只显示文件最前面1K的内容,这也表明显示一个4M的二进制文件是没必要(或未必有效/有用)的。