20.2. cgi-通用网关接口支持

源代码: Lib/cgi.py


通用网关接口(CGI)脚本的支持模块。

该模块定义了许多 Util,可用于以 Python 编写的 CGI 脚本。

20.2.1. Introduction

HTTP 服务器调用 CGI 脚本,通常用于处理pass HTML <FORM><ISINDEX>元素提交的用户 Importing。

通常,CGI 脚本位于服务器的特殊cgi-bin目录中。 HTTP 服务器将有关请求的各种信息(例如 Client 端的主机名,请求的 URL,查询字符串以及许多其他东西)放置在脚本的 Shell 环境中,执行脚本,并将脚本的输出发送回给 Client。

脚本的 Importing 也连接到 Client 端,有时以这种方式读取表单数据。在其他时候,表单数据是pass URL 的“查询字符串”部分传递的。该模块旨在处理不同的情况,并为 Python 脚本提供更简单的界面。它还提供了许多 Util,可帮助调试脚本,并且最新添加的Function是支持从表单上传文件(如果您的浏览器支持的话)。

CGI 脚本的输出应由两部分组成,并用空白行分隔。第一部分包含许多 Headers,告诉 Client 端要遵循的数据类型。生成最小 Headers 部分的 Python 代码如下所示:

print "Content-Type: text/html"     # HTML is following
print                               # blank line, end of headers

第二部分通常是 HTML,它允许 Client 端软件显示带有标题,嵌入式图像等格式良好的文本。这是 Python 代码,可打印简单的 HTML:

print "<TITLE>CGI script output</TITLE>"
print "<H1>This is my first CGI script</H1>"
print "Hello, world!"

20.2.2. 使用 cgi 模块

首先编写import cgi。不要使用from cgi import *-模块定义了各种名称,以供自己使用或实现名称空间中不需要的向后兼容性。

在编写新脚本时,请考虑添加以下几行:

import cgitb
cgitb.enable()

这将激活一个特殊的异常处理程序,如果发生任何错误,该异常处理程序将在 Web 浏览器中显示详细的报告。如果您不希望向脚本用户显示程序的胆量,则可以使用以下代码将报告保存到文件中:

import cgitb
cgitb.enable(display=0, logdir="/path/to/logdir")

在脚本开发过程中使用此Function非常有帮助。 cgitb生成的报告提供的信息可为您节省大量的时间来查找错误。测试脚本并确信其可以正常运行后,您随时可以在以后删除cgitb行。

要获取提交的表单数据,最好使用FieldStorage类。提供此模块中定义的其他类主要是为了向后兼容。实例化一次,不带参数。这将从标准 Importing 或环境中读取表单内容(取决于根据 CGI 标准设置的各种环境变量的值)。由于它可能消耗标准 Importing,因此只能实例化一次。

FieldStorage实例可以像 Python 字典一样被索引。它允许使用in运算符进行成员资格测试,还支持标准词典方法keys()和内置函数len()。包含空字符串的表单字段将被忽略,并且不会出现在字典中;要保留这些值,请在创建FieldStorage实例时为可选的* keep_blank_values *关键字参数提供一个真值。

例如,以下代码(假定* Content-Type *Headers 和空白行已被打印)检查字段nameaddr是否都设置为非空字符串:

form = cgi.FieldStorage()
if "name" not in form or "addr" not in form:
    print "<H1>Error</H1>"
    print "Please fill in the name and addr fields."
    return
print "<p>name:", form["name"].value
print "<p>addr:", form["addr"].value
...further form processing here...

在这里,passform[key]访问的字段本身就是FieldStorage(或MiniFieldStorage,具体取决于形式编码)的实例。实例的value属性产生该字段的字符串值。 getvalue()方法直接返回此字符串值。它也接受可选的第二个参数作为默认值,如果请求的键不存在,则返回默认值。

如果提交的表单数据包含多个具有相同名称的字段,则form[key]检索的对象不是FieldStorageMiniFieldStorage实例,而是此类实例的列表。同样,在这种情况下,form.getvalue(key)将返回字符串列表。如果您期望这种可能性(当您的 HTML 表单包含多个具有相同名称的字段时),请使用getlist()方法,该方法始终返回值列表(这样就无需对单个项目的大小写进行特殊处理)。例如,此代码连接任意数量的用户名字段,并用逗号分隔:

value = form.getlist("username")
usernames = ",".join(value)

如果一个字段代表一个上载的文件,则passvalue属性或getvalue()方法访问该值将以字符串形式读取内存中的整个文件。这可能不是您想要的。您可以pass测试filename属性或file属性来测试上传的文件。然后,您可以从file属性中随意读取数据:

fileitem = form["userfile"]
if fileitem.file:
    # It's an uploaded file; count lines
    linecount = 0
    while 1:
        line = fileitem.file.readline()
        if not line: break
        linecount = linecount + 1

如果在获取上载文件的内容时遇到错误(例如,当用户pass单击“上一步”或“取消”来break表单提交时),则该字段的对象的done属性将设置为值-1 。

文件上传草案标准具有从一个字段上传多个文件的可能性(使用递归* multipart/ 编码).发生这种情况时,该项将是类似于字典的FieldStorage项.这可以pass测试其type属性来确定,该属性应该是 multipart/form-data (或者可能是另一个与 multipart/* 匹配的 MIME 类型)。在这种情况下,可以像顶级表单对象一样递归地对其进行迭代。

当表单以“旧”格式提交(作为查询字符串或* application/x-www-form-urlencoded *类型的单个数据部分)时,这些项目实际上将是MiniFieldStorage类的实例。在这种情况下,listfilefilename属性始终为None

pass POST 提交的还具有查询字符串的表单将包含FieldStorageMiniFieldStorage项。

20.2.3. 高级界面

2.2 版中的新Function。

上一节说明了如何使用FieldStorage类读取 CGI 表单数据。本节描述了一个更高级别的接口,该接口已添加到该类中,以使人们能够以一种更易读和直观的方式进行操作。该界面不会使前几节中描述的技术过时-例如,它们对于有效地处理文件上传仍然很有用。

该接口包含两种简单的方法。使用这些方法,您可以以一种通用的方式处理表单数据,而不必担心在一个名称下是否仅发布了一个或多个值。

在上一节中,您学习了期望用户在一个名称下发布多个值的任何时候编写以下代码:

item = form.getvalue("item")
if isinstance(item, list):
    # The user is requesting more than one item.
else:
    # The user is requesting only one item.

例如,当一个表单包含一组多个具有相同名称的复选框时,这种情况很常见:

<input type="checkbox" name="item" value="1" />
<input type="checkbox" name="item" value="2" />

但是,在大多数情况下,表单中只有一个具有特定名称的表单控件,然后您期望并且仅需要一个与此名称相关联的值。因此,您编写了一个包含以下代码的脚本:

user = form.getvalue("user").upper()

代码的问题在于,您永远不要期望 Client 端会向脚本提供有效的 Importing。例如,如果好奇的用户将另一个user=foo对附加到查询字符串,则脚本将崩溃,因为在这种情况下getvalue("user")方法调用将返回列表而不是字符串。在列表上调用upper()方法无效(因为列表没有此名称的方法),并导致AttributeError异常。

因此,读取表单数据值的适当方法是始终使用代码检查所获得的值是单个值还是值列表。这很烦人,导致脚本的可读性降低。

一种更方便的方法是使用此更高级别的界面提供的方法getfirst()getlist()

  • FieldStorage. getfirst(* name * [,* default *])

    • 此方法始终仅返回与表单字段* name 关联的一个值。如果使用该名称发布了更多值,则该方法仅返回第一个值。请注意,各个浏览器接收值的 Sequences 可能会有所不同,因此不能指望。 [1]如果不存在这样的表单字段或值,则该方法返回由可选参数 default *指定的值。如果未指定,则此参数默认为None
  • FieldStorage. getlist(* name *)

    • 此方法始终返回与表单字段* name 关联的值的列表。如果 name *的表单字段或值不存在,则该方法返回一个空列表。如果只有一个这样的值,它将返回一个包含一个项目的列表。

使用这些方法,您可以编写漂亮的紧凑代码:

import cgi
form = cgi.FieldStorage()
user = form.getfirst("user", "").upper()    # This way it's safe.
for item in form.getlist("item"):
    do_something(item)

20.2.4. 老班

从 2.6 版开始不推荐使用:cgi模块的较早版本中提供的这些类仍受向后兼容性的支持。新应用程序应使用FieldStorage类。

SvFormContentDict将单值表单内容存储为字典;它假定每个字段名称仅以一次形式出现。

FormContentDict将多个值表单内容存储为字典(表单项是值列表)。如果您的表单包含多个具有相同名称的字段,则很有用。

存在其他类(FormContentInterpFormContentDict)仅是为了与 true 的旧应用程序向后兼容。

20.2.5. Functions

如果您需要更多控制权,或者在其他情况下要使用此模块中实现的某些算法,这些Function将很有用。

  • cgi. parse(* fp * [,* environ * [,* keep_blank_values * [,* strict_parsing *]]])

    • 在环境或文件中解析查询(文件默认为sys.stdin,环境默认为os.environ)。 * keep_blank_values strict_parsing *参数不变地传递给urlparse.parse_qs()
  • cgi. parse_qs(* qs * [,* keep_blank_values * [,* strict_parsing * [,* max_num_fields *]]])

    • 此模块不建议使用此Function。请改用urlparse.parse_qs()。此处仅出于向后兼容的目的对其进行维护。
  • cgi. parse_qsl(* qs * [,* keep_blank_values * [,* strict_parsing * [,* max_num_fields *]]])

    • 此模块不建议使用此Function。请改用urlparse.parse_qsl()。此处仅出于向后兼容的目的对其进行维护。
  • cgi. parse_multipart(* fp pdict *)

    • 解析* multipart/form-data 类型的 Importing(用于文件上传)。对于 Importing 文件,参数为 fp ;对于在 Content-Type Headers 中包含其他参数的字典,参数为 pdict *。

返回一个字典,就像urlparse.parse_qs()键是字段名称一样,每个值都是该字段的值列表。这很容易使用,但如果您希望上传兆字节,则不好用-在这种情况下,请使用FieldStorage类,它更加灵活。

请注意,这不会解析嵌套的 MultipartComponent,为此请使用FieldStorage

  • cgi. parse_header(* string *)

    • 将 MIMEHeaders(例如* Content-Type *)解析为一个主值和一个参数字典。
  • cgi. test ( )

    • 健壮的测试 CGI 脚本,可用作主程序。编写最小的 HTTPHeaders,并以 HTML 格式格式化提供给脚本的所有信息。
  • cgi. print_environ ( )

    • 用 HTML 格式化 Shell 环境。
  • cgi. print_form(* form *)

    • 用 HTML 格式化表格。
  • cgi. print_directory ( )

    • 用 HTML 格式化当前目录。
  • cgi. print_environ_usage ( )

    • 在 HTML 中打印有用的(由 CGI 使用)环境变量的列表。
  • cgi. escape(* s * [,* quote *])

    • 将字符串* s 中的字符'&''<''>'转换为 HTML 安全序列。如果您需要在 HTML 中显示可能包含此类字符的文本,请使用此选项。如果可选标志 quote *为 true,则还会翻译引号字符(")。这有助于将 HTML 属性值包含在双引号中,例如<a href="...">。请注意,单引号永远不会翻译。

如果要引用的值可能包含单引号或双引号字符,或同时包含两个引号,请考虑改为使用xml.sax.saxutils模块中的quoteattr()函数。

20.2.6. 关心安全

有一条重要的规则:如果调用外部程序(passos.system()os.popen()函数或具有类似Function的其他程序),请确保不要将从 Client 端收到的任意字符串传递给 Shell。这是一个众所周知的安全漏洞,Web 上任何地方的聪明黑客都可以利用可感染的 CGI 脚本来调用任意 shell 命令。甚至 URL 或字段名称的一部分也不能被信任,因为请求不必来自您的表单!

为了安全起见,如果必须将从表单获得的字符串传递给 Shell 命令,则应确保该字符串仅包含字母数字字符,破折号,下划线和句点。

20.2.7. 在 Unix 系统上安装 CGI 脚本

阅读 HTTP 服务器的文档,并与您的本地系统 Management 员联系,以查找应该在其中安装 CGI 脚本的目录。通常在服务器树中的目录cgi-bin中。

确保您的脚本可以被“其他”读取并执行; Unix 文件模式应为0755八进制(使用chmod 0755 filename)。确保脚本的第一行从第 1 列开始包含#!,后跟 Python 解释器的路径名,例如:

#!/usr/local/bin/python

确保 Python 解释器存在并且可由“其他”执行。

确保您的脚本需要读取或写入的所有文件分别由“其他”可读或可写–它们的模式应为0644(可读)和0666(可写)。这是因为出于安全原因,HTTP 服务器以用户“ nobody”的身份执行脚本,而没有任何特殊特权。它只能读取(写入,执行)每个人都可以读取(写入,执行)的文件。执行时的当前目录也有所不同(通常是服务器的 cgi-bin 目录),并且环境变量集也与您登录时所获得的不同。特别是,不要指望 shell 的搜索路径将可执行文件( PATH)或 Python 模块搜索路径( PYTHONPATH)设置为任何有趣的值。

如果您需要从不在 Python 默认模块搜索路径上的目录中加载模块,则可以在导入其他模块之前在脚本中更改路径。例如:

import sys
sys.path.insert(0, "/usr/home/joe/lib/python")
sys.path.insert(0, "/usr/local/lib/python")

(这样,将首先搜索最后插入的目录!)

非 Unix 系统的说明会有所不同;检查 HTTP 服务器的文档(通常会有关于 CGI 脚本的部分)。

20.2.8. 测试您的 CGI 脚本

不幸的是,当您从命令行try运行 CGI 脚本时,通常不会运行它,而从服务器运行时,在命令行中完美运行的脚本可能会神秘地失败。为什么仍然应该从命令行测试脚本是有一个原因的:如果脚本包含语法错误,Python 解释器将根本不会执行该脚本,并且 HTTP 服务器很可能会将隐式错误发送给 Client 端。

假设您的脚本没有语法错误,但是它不起作用,您别无选择,只能阅读下一节。

20.2.9. 调试 CGI 脚本

首先,检查琐碎的安装错误-仔细阅读上面有关安装 CGI 脚本的部分,可以节省大量时间。如果您想知道自己是否正确理解了安装过程,请try以 CGI 脚本的形式安装此模块文件的副本(cgi.py)。当作为脚本调用时,文件将以 HTML 格式转储其环境和表单内容。给它正确的模式,等等,并发送请求。如果安装在标准cgi-bin目录中,则可以pass在浏览器中 Importing 以下格式的 URL 来发送请求:

http://yourhostname/cgi-bin/cgi.py?name=Joe+Blow&addr=At+Home

如果这给出了类型 404 的错误,则服务器找不到脚本-也许您需要将其安装在其他目录中。如果出现另一个错误,则存在安装问题,您应先try解决此问题。如果您获得格式正确的环境和表单内容列表(在此示例中,字段应被列为“ addr”,其值为“ At Home”,而“ name”的值为“ Joe Blow”),则cgi.py脚本已正确安装。如果您对自己的脚本执行相同的过程,则现在应该可以对其进行调试。

下一步可能是从脚本中调用cgi模块的test()函数:用单个语句替换其主要代码

cgi.test()

这将产生与安装cgi.py文件本身相同的结果。

当普通的 Python 脚本引发未处理的异常(出于某种原因:模块名称中的错字,无法打开的文件等)时,Python 解释器会打印出一个不错的回溯并退出。尽管当 CGI 脚本引发异常时 Python 解释器仍会执行此操作,但是回溯很可能finally会出现在 HTTP 服务器的日志文件之一中,或者被完全丢弃。

幸运的是,一旦您设法使脚本执行一些代码,您就可以使用cgitb模块轻松地将 traceback 发送到 Web 浏览器。如果您还没有这样做,请添加以下行:

import cgitb
cgitb.enable()

到脚本顶部。然后try再次运行它;发生问题时,您应该看到一份详细的报告,该报告很可能使崩溃的原因显而易见。

如果您怀疑导入cgitb模块可能存在问题,则可以使用更可靠的方法(仅使用内置模块):

import sys
sys.stderr = sys.stdout
print "Content-Type: text/plain"
print
...your code here...

这依赖于 Python 解释器来打印回溯。输出的 Content Type 设置为纯文本,这将禁用所有 HTML 处理。如果您的脚本有效,则原始 HTML 将由您的 Client 端显示。如果引发异常,则很可能在打印了前两行之后,将显示回溯。因为没有 HTML 解释正在进行,所以回溯是可读的。

20.2.10. 常见问题与解决方案

  • 大多数 HTTP 服务器会缓冲 CGI 脚本的输出,直到脚本完成为止。这意味着在脚本运行时无法在 Client 端的显示器上显示进度报告。

  • 检查上面的安装说明。

  • 检查 HTTP 服务器的日志文件。 (在另一个窗口中的tail -f logfile可能会有用!)

  • 始终首先pass执行python script.py之类的操作来检查脚本中是否存在语法错误。

  • 如果您的脚本没有任何语法错误,请try在脚本顶部添加import cgitb; cgitb.enable()

  • 调用外部程序时,请确保可以找到它们。通常,这意味着使用绝对路径名- PATH通常不会在 CGI 脚本中设置为非常有用的值。

  • 在读取或写入外部文件时,请确保可以由运行 CGI 脚本的用户 ID 读取或写入它们:通常是运行 Web 服务器的用户 ID,或者是为 Web 服务器的suexec明确指定的用户 ID。Feature。

  • 不要try为 CGI 脚本设置 set-uid 模式。这不适用于大多数系统,并且也是安全责任。

Footnotes

  • [1]
    • 请注意,HTML 规范的某些最新版本确实规定了应按什么 Sequences 提供字段值,但是知道是否从符合要求的浏览器,甚至根本没有从浏览器接收到请求,既繁琐又容易出错。