Skip to main content

21.2. cgi —公共网关接口支持

源代码: Lib/cgi.py


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

这个模块定义了一些由Python编写的CGI脚本使用的实用程序。

21.2.1. 介绍

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

通常,CGI脚本驻留在服务器的特殊 cgi-bin 目录中。 HTTP服务器在脚本的shell环境中放置有关请求的所有类型的信息(例如客户端的主机名,请求的URL,查询字符串和许多其他好处),执行脚本,并将脚本的输出发送回客户。

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

CGI脚本的输出应由两个部分组成,由空行分隔。第一部分包含许多标题,告诉客户端跟踪哪种数据。生成最小头部分的Python代码看起来像这样:

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

第二部分通常是HTML,它允许客户端软件显示格式正确的文本,包括头,行内图像等。这里是Python代码,打印一个简单的HTML:

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

21.2.2. 使用cgi模块

从编写 import cgi 开始。

当您编写新脚本时,请考虑添加这些行:

import cgitb
cgitb.enable()

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

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

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

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

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

例如,以下代码(假定 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 属性自动关闭之前从 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 (或者可能是与 multipart/* 匹配的另一MIME类型)。在这种情况下,它可以像顶层表单对象一样通过递归迭代。

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

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

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

在 3.5 版更改: 增加了对 FieldStorage 类的上下文管理协议的支持。

21.2.3. 高级接口

上一节介绍如何使用 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()

代码的问题是,你不应该期望客户端将为您的脚本提供有效的输入。例如,如果好奇的用户将另一个 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)

21.2.4. 功能

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

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_qs(qs, keep_blank_values=False, strict_parsing=False)

此模块中已弃用此函数。请改用 urllib.parse.parse_qs()。这里维护它只是为了向后兼容。

cgi.parse_qsl(qs, keep_blank_values=False, strict_parsing=False)

此模块中已弃用此函数。请改用 urllib.parse.parse_qsl()。这里维护它只是为了向后兼容。

cgi.parse_multipart(fp, pdict)

解析 multipart/form-data 类型的输入(用于文件上传)。参数是 fp 作为输入文件,pdict 是包含 Content-Type 头中其他参数的字典。

返回一个字典就像 urllib.parse.parse_qs() 键是字段名,每个值都是该字段的值列表。这是很容易使用,但不是很好,如果你期望兆字节上传—在这种情况下,使用 FieldStorage 类,而不是更灵活。

注意,这不会解析嵌套的多部分零件 - 使用 FieldStorage

cgi.parse_header(string)

将MIME头(如 Content-Type)解析为主值和参数字典。

cgi.test()

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

cgi.print_environ()

在HTML中格式化shell环境。

cgi.print_form(form)

在HTML中格式化表单。

cgi.print_directory()

使用HTML格式化当前目录。

cgi.print_environ_usage()

在HTML中打印有用(由CGI使用)环境变量的列表。

cgi.escape(s, quote=False)

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

3.2 版后已移除: 此函数不安全,因为默认情况下 quote 为false,因此已弃用。使用 html.escape()

21.2.5. 关心安全

有一个重要的规则:如果你调用一个外部程序(通过 os.system()os.popen() 函数或类似功能的其他函数),请确保你不将从客户端接收的任意字符串传递给shell。这是一个众所周知的安全漏洞,聪明的黑客在Web上的任何地方可以利用一个gullible CGI脚本来调用任意shell命令。即使部分URL或字段名称不可信任,因为请求不必来自您的表单!

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

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

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

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

#!/usr/local/bin/python

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

确保脚本需要读取或写入的任何文件分别由“其他”读取或写入 - 他们的模式应该是 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脚本的部分)。

21.2.7. 测试你的CGI脚本

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

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

21.2.8. 调试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”,值为“name”,值为“Joe Blow”),cgi.py 脚本安装正确。如果你对自己的脚本采用相同的过程,你现在应该能够调试它。

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

cgi.test()

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

当一个普通的Python脚本引发一个未处理的异常(无论什么原因:在模块名称中的打字错误,无法打开的文件等),Python解释器打印一个很好的追溯并退出。虽然Python解释器仍然会在CGI脚本引发异常时执行此操作,但很有可能跟踪将在HTTP服务器的日志文件中结束,或者完全丢弃。

幸运的是,一旦你设法让你的脚本执行 some 代码,你可以轻松地使用 cgitb 模块发送回溯到Web浏览器。如果你还没有这样做,只需添加行:

import cgitb
cgitb.enable()

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

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

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

这依赖于Python解释器来打印追踪。输出的内容类型设置为纯文本,这将禁用所有HTML处理。如果您的脚本工作,原始HTML将由您的客户端显示。如果引发异常,最有可能在打印前两行之后,将显示回溯。因为没有HTML解释,traceback将是可读的。

21.2.9. 常见问题和解决方案

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

  • 请检查上面的安装说明。

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

  • 总是先检查脚本语法错误,通过做一些像 python script.py

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

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

  • 当读取或写入外部文件时,确保它们可以由您的CGI脚本将在其下运行的用户标识读取或写入:这通常是Web服务器正在运行的用户标识,或一些明确指定的用户标识为Web服务器的 suexec 特征。

  • 不要试图给一个CGI脚本set-uid模式。这在大多数系统上不工作,也是安全责任。

脚注

[1]

请注意,HTML规范的一些最新版本确实应该提供字段值的顺序,但是知道是否从一个符合的浏览器,或者甚至从一个浏览器接收到一个请求是冗长和容易出错的。