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

源代码: Lib/cgi.py


公共网关接口(CGI)脚本支持模块。

这个模块定义了许多实用程序,供用Python编写的CGI脚本使用。

介绍

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

最常见的情况是,CGI脚本在服务器的 cgi-bin 目录。HTTP服务器将有关请求的各种信息(例如客户端的主机名、请求的URL、查询字符串和许多其他功能)放在脚本的shell环境中,执行脚本,并将脚本的输出发送回客户端。

脚本的输入也连接到客户机,有时表单数据以这种方式读取;有时表单数据通过URL的“查询字符串”部分传递。这个模块旨在处理不同的情况,并为Python脚本提供一个更简单的接口。它还提供了许多帮助调试脚本的实用程序,最新添加的是支持从表单上载文件(如果您的浏览器支持)。

CGI脚本的输出应该由两个部分组成,用空行分隔。第一部分包含许多头,告诉客户接下来是什么类型的数据。生成最小头段的python代码如下所示:

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

第二部分通常是HTML,它允许客户端软件显示格式良好的文本,包括标题、内嵌图像等。下面是打印简单HTML的python代码:

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

使用CGI模块

从写作开始 import cgi .

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

import cgitb
cgitb.enable()

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

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

在脚本开发期间使用此功能非常有帮助。报告由 cgitb 提供可以在跟踪错误时节省大量时间的信息。您可以随时删除 cgitb 当您测试了脚本并确信它正确工作时,请稍后再行。

要获取提交的表单数据,请使用 FieldStorage 类。如果表单包含非ASCII字符,请使用 encoding 关键字参数设置为文档定义的编码值。它通常包含在HTML文档头部分的元标记中,或者 Content-Type 标题)。这将从标准输入或环境中读取表单内容(取决于根据CGI标准设置的各种环境变量的值)。因为它可能使用标准输入,所以应该只实例化一次。

这个 FieldStorage 实例可以像python字典一样被索引。它允许使用 in 运算符,还支持标准字典方法 keys() 以及内置功能 len() . 包含空字符串的表单域将被忽略,并且不会出现在字典中;若要保留这些值,请为可选的 keep_blank_values 创建时的关键字参数 FieldStorage 实例。

例如,以下代码(假定 Content-Type 标题和空行已打印)检查字段 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...

这里是字段,通过 form[key] ,它们本身就是 FieldStorage (或) MiniFieldStorage ,取决于表单编码)。这个 value 实例的属性生成字段的字符串值。这个 getvalue() 方法直接返回该字符串值;如果请求的键不存在,它还接受一个可选的第二个参数作为默认值返回。

如果提交的表单数据包含多个同名字段,则由 form[key] 不是一个 FieldStorageMiniFieldStorage 实例,但此类实例的列表。同样,在这种情况下, form.getvalue(key) 将返回字符串列表。如果希望出现这种情况(当HTML表单包含多个同名字段时),请使用 getlist() 方法,它总是返回一个值列表(这样就不需要对单个项的大小写进行特殊区分)。例如,此代码连接任意数量的用户名字段,用逗号分隔:

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

如果字段表示上载的文件,则通过 value 属性或 getvalue() 方法以字节的形式读取内存中的整个文件。这可能不是你想要的。您可以通过测试 filename 属性或 file 属性。然后您可以从 file 属性,然后作为 FieldStorage 实例 read()readline() 方法将返回字节)::

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

FieldStorage 对象还支持在 with 语句,完成后将自动关闭它们。

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

文件上传草案标准允许从一个字段上传多个文件(使用递归 multipart/* 编码)。当发生这种情况时,该项将是类似于 FieldStorage 项目。这可以通过测试 type 属性,应该是 multipart/form-data (或者可能是另一个匹配的mime类型) multipart/* )在这种情况下,它可以像顶级表单对象一样递归地迭代。

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

通过POST提交的具有查询字符串的表单将同时包含 FieldStorageMiniFieldStorage 项目。

在 3.4 版更改: 这个 file 属性在创建的垃圾收集时自动关闭 FieldStorage 实例。

在 3.5 版更改: 已将对上下文管理协议的支持添加到 FieldStorage 类。

高级接口

上一节介绍如何使用 FieldStorage 类。本节描述了一个更高级别的接口,它被添加到这个类中,以允许以更可读和直观的方式进行操作。接口不会使前面章节中描述的技术过时——例如,它们对于有效地处理文件上载仍然很有用。

接口由两个简单的方法组成。使用这些方法,您可以以通用的方式处理表单数据,而无需担心在一个名称下是否只发布了一个或多个值。

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

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()

代码的问题在于,您不应该期望客户机为您的脚本提供有效的输入。例如,如果一个好奇的用户附加了另一个 user=foo 与查询字符串配对,则脚本将崩溃,因为在这种情况下 getvalue("user") 方法调用返回列表而不是字符串。调用 upper() 列表上的方法无效(因为列表没有此名称的方法),并导致 AttributeError 例外。

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

更方便的方法是使用这些方法 getfirst()getlist() 由更高级别的接口提供。

FieldStorage.getfirst(name, default=None)

此方法始终只返回一个与窗体域关联的值 name . 如果在这样的名称下发布了更多的值,该方法只返回第一个值。请注意,接收值的顺序可能因浏览器而异,不应计算在内。 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)

功能

如果您想要更多的控制,或者如果您想在其他情况下使用这个模块中实现的一些算法,这些方法是有用的。

cgi.parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False)

在环境中或从文件(文件默认为 sys.stdin )这个 keep_blank_valuesstrict_parsing 参数传递到 urllib.parse.parse_qs() 不变。

cgi.parse_multipart(fp, pdict, encoding='utf-8', errors='replace')

分析类型的输入 multipart/form-data (用于文件上载)。参数是 fp 对于输入文件, PDICT 对于包含其他参数的字典, Content-Type 标题,以及 encoding ,请求编码。

返回字典,就像 urllib.parse.parse_qs() :keys是字段名,每个值都是该字段的值列表。对于非文件字段,该值是字符串列表。

这很容易使用,但如果您希望上传兆字节,则不是很好——在这种情况下,请使用 FieldStorage 而不是更灵活的类。

在 3.7 版更改: 增加了 encodingerrors 参数。对于非文件字段,该值现在是字符串列表,而不是字节。

cgi.parse_header(string)

分析一个mime头(例如 Content-Type )输入一个主值和一个参数字典。

cgi.test()

强大的测试CGI脚本,可用作主程序。以HTML格式写入最小HTTP头并格式化提供给脚本的所有信息。

cgi.print_environ()

以HTML格式设置外壳环境。

cgi.print_form(form)

以HTML格式设置表单。

cgi.print_directory()

以HTML格式设置当前目录。

cgi.print_environ_usage()

以HTML格式打印有用(由CGI使用)环境变量的列表。

关心安全

有一个重要的规则:如果调用外部程序(通过 os.system()os.popen() 功能。或者其他具有类似功能的字符串),确保不会将从客户机接收到的任意字符串传递给shell。这是一个众所周知的安全漏洞,聪明的黑客可以利用一个容易上当受骗的CGI脚本来调用任意的shell命令。即使是部分URL或域名也不能被信任,因为请求不必来自您的表单!

为了安全起见,如果必须将从窗体获取的字符串传递给shell命令,则应确保该字符串只包含字母数字字符、短划线、下划线和句点。

在UNIX系统上安装CGI脚本

阅读HTTP服务器的文档,并与本地系统管理员联系,以找到应该安装CGI脚本的目录;通常这是在一个目录中 cgi-bin 在服务器树中。

确保您的脚本是可读的,并且可由“其他人”执行;Unix文件模式应该是 0o755 八进制(使用) chmod 0755 filename )确保脚本的第一行包含 #! 从第1列开始,后跟python解释器的路径名,例如:

#!/usr/local/bin/python

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

确保脚本需要读取或写入的任何文件都是可读或可写的,分别由“其他人”--它们的模式应该是 0o644 为了可读和 0o666 可写的。这是因为,出于安全原因,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脚本)。

测试你的CGI脚本

不幸的是,当您从命令行尝试时,CGI脚本通常不会运行,而从服务器运行时,从命令行完美运行的脚本可能会神秘失败。您仍然应该从命令行测试脚本有一个原因:如果脚本包含语法错误,那么Python解释器根本不会执行它,HTTP服务器很可能会向客户机发送一个神秘的错误。

假设您的脚本没有语法错误,但它不起作用,那么您除了阅读下一节之外别无选择。

调试CGI脚本

首先,检查是否有微小的安装错误---仔细阅读上面关于安装CGI脚本的部分可以节省很多时间。如果您怀疑是否正确理解了安装过程,请尝试安装此模块文件的副本。 (cgi.py )作为CGI脚本。当作为脚本调用时,文件将以HTML格式转储其环境和表单内容。给它正确的模式等,并发送一个请求。如果安装在标准中 cgi-bin 目录,应该可以通过在表单的浏览器中输入URL向其发送请求:

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

如果出现404类型的错误,服务器就找不到该脚本——也许您需要将其安装到其他目录中。如果它给出了另一个错误,那么在进一步尝试之前,您应该先解决安装问题。如果您得到了环境和表单内容的格式良好的列表(在本例中,字段应列为“addr”,值为“at home”,值为“joe blow”),则 cgi.py 脚本已正确安装。如果您对自己的脚本执行相同的过程,那么现在应该可以调试它了。

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

cgi.test()

这将产生与安装 cgi.py 文件本身。

当一个普通的python脚本引发一个未经处理的异常(不管是什么原因:模块名的错别字、无法打开的文件等),python解释器会打印一个好的回溯并退出。当您的CGI脚本引发异常时,python解释器仍然会这样做,但最有可能的情况是,跟踪最终会出现在HTTP服务器的一个日志文件中,或者被完全丢弃。

幸运的是,一旦您成功地让脚本执行 some 代码,您可以使用 cgitb 模块。如果您还没有这样做,只需添加行:

import cgitb
cgitb.enable()

在脚本的顶部。然后再运行一次;当出现问题时,您应该看到一个详细的报告,它可能会使崩溃的原因变得明显。

如果您怀疑导入 cgitb 模块,您可以使用更健壮的方法(只使用内置模块)::

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

这依赖于python解释器来打印回溯。输出的内容类型设置为纯文本,这将禁用所有HTML处理。如果脚本工作正常,客户机将显示原始HTML。如果它引发了一个异常,很可能在打印前两行之后,会显示一个回溯。因为没有进行HTML解释,所以回溯将是可读的。

常见问题及解决办法

  • 大多数HTTP服务器都会缓冲来自CGI脚本的输出,直到脚本完成。这意味着在脚本运行时,不可能在客户端的显示器上显示进度报告。

  • 检查上述安装说明。

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

  • 总是先检查脚本的语法错误,方法如下 python script.py .

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

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

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

  • 不要试图给CGI脚本设置uid模式。这在大多数系统上都不起作用,而且也是一项安全责任。

脚注

1

请注意,HTML规范的一些最新版本确实说明了字段值应按什么顺序提供,但知道是从符合要求的浏览器接收到请求,还是从浏览器接收到请求,这是一件乏味且容易出错的事情。