cgi — 通用网关接口支持 — Python 文档

来自菜鸟教程
Python/docs/3.8/library/cgi
跳转至:导航、​搜索

cgi — 通用网关接口支持

源代码: :source:`Lib/cgi.py`



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

该模块定义了许多由 Python 编写的 CGI 脚本使用的实用程序。

介绍

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,它允许客户端软件显示带有标题、内嵌图像等的格式良好的文本。 这是打印一段简单 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 文档 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 属性中读取数据,然后将其作为 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 类添加了对上下文管理协议的支持。


高级接口

上一节解释了如何使用 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)

功能

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

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

在环境中或从文件中解析查询(文件默认为 sys.stdin)。 keep_blank_valuesstrict_parsingseparator 参数不变地传递给 urllib.parse.parse_qs()

3.8.8 版更改: 增加了 分隔符 参数。

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

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

返回一个类似于 urllib.parse.parse_qs() 的字典:键是字段名称,每个值是该字段的值列表。 对于非文件字段,该值是一个字符串列表。

这很容易使用,但如果您希望上传兆字节,则效果不佳 - 在这种情况下,请改用 FieldStorage 类,这更加灵活。

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

3.8.8 版更改: 增加了 分隔符 参数。

cgi.parse_header(string)
将 MIME 标头(例如 Content-Type)解析为主值和参数字典。
cgi.test()
健壮的测试 CGI 脚本,可用作主程序。 编写最少的 HTTP 标头并以 HTML 形式格式化提供给脚本的所有信息。
cgi.print_environ()
在 HTML 中格式化 shell 环境。
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”,“name”值为“Joe Blow”),[ X194X] 脚本已正确安装。 如果您对自己的脚本执行相同的过程,您现在应该可以调试它。

下一步可能是从脚本中调用 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 解释正在进行,所以回溯将是可读的。


常见问题及解决方法

  • 大多数 HTTP 服务器缓冲 CGI 脚本的输出,直到脚本完成。 这意味着在脚本运行时不可能在客户端的显示器上显示进度报告。
  • 检查上面的安装说明。
  • 检查 HTTP 服务器的日志文件。 (单独窗口中的 tail -f logfile 可能有用!)
  • 始终首先检查脚本是否存在语法错误,方法是执行类似 python script.py 的操作。
  • 如果您的脚本没有任何语法错误,请尝试在脚本顶部添加 import cgitb; cgitb.enable()
  • 调用外部程序时,请确保可以找到它们。 通常,这意味着使用绝对路径名 — PATH 在 CGI 脚本中通常不会设置为非常有用的值。
  • 读取或写入外部文件时,请确保它们可以由运行 CGI 脚本的用户 ID 读取或写入:这通常是运行 Web 服务器的用户 ID,或某些明确指定的 Web 服务器的用户 ID [ X260X] 功能。
  • 不要试图给 CGI 脚本一个 set-uid 模式。 这不适用于大多数系统,并且也是一种安全责任。

脚注

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