cgi
— 通用网关接口支持¶
源代码: Lib/cgi.py
自版本 3.11 起弃用,将在版本 3.13 中移除: The cgi
模块已弃用(有关详细信息和替代方案,请参阅 PEP 594)。
The FieldStorage
类通常可以用 urllib.parse.parse_qsl()
替换为 GET
和 HEAD
请求,以及 email.message
模块或 multipart 替换为 POST
和 PUT
。大多数 实用程序函数 都有替代方案。
通用网关接口 (CGI) 脚本的支持模块。
此模块定义了一些供用 Python 编写的 CGI 脚本使用的实用程序。
全局变量 maxlen
可以设置为一个整数,表示 POST 请求的最大大小。大于此大小的 POST 请求会导致在解析过程中引发 ValueError
。此变量的默认值为 0
,表示请求大小不受限制。
可用性: 不包括 Emscripten,不包括 WASI。
此模块在 WebAssembly 平台 wasm32-emscripten
和 wasm32-wasi
上不起作用或不可用。有关更多信息,请参阅 WebAssembly 平台。
简介¶
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 文档的 HEAD 部分的 META 标记中,或者由 Content-Type 标头提供。这将从标准输入或环境(取决于根据 CGI 标准设置的各种环境变量的值)读取表单内容。由于它可能会消耗标准输入,因此应仅实例化一次。
The FieldStorage
实例可以像 Python 字典一样进行索引。它允许使用 in
运算符进行成员资格测试,还支持标准字典方法 keys()
和内置函数 len()
。包含空字符串的表单字段将被忽略,不会出现在字典中;要保留此类值,在创建 FieldStorage
实例时,为可选的 keep_blank_values 关键字参数提供一个真值。
例如,以下代码(假设 Content-Type 标头和空行已经打印)检查字段 name
和 addr
是否都设置为非空字符串
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
属性会生成字段的字符串值。The getvalue()
方法直接返回此字符串值;它还接受一个可选的第二个参数作为默认值,如果请求的键不存在,则返回该默认值。
如果提交的表单数据包含多个具有相同名称的字段,则通过 form[key]
获取的对象不是 FieldStorage
或 MiniFieldStorage
实例,而是一个包含此类实例的列表。类似地,在这种情况下,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
类的实例。在这种情况下,list
、file
和 filename
属性始终为 None
。
通过 POST 提交的表单,如果也包含查询字符串,将包含 FieldStorage
和 MiniFieldStorage
项。
在 3.4 版中变更: 当创建 FieldStorage
实例被垃圾回收时,file
属性将被自动关闭。
在 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_values、strict_parsing 和 separator 参数将不变地传递给urllib.parse.parse_qs()
。从 3.11 版本开始弃用,将在 3.13 版本中移除: 此函数,与
cgi
模块中的其他部分一样,已弃用。它可以通过直接在所需查询字符串上调用urllib.parse.parse_qs()
来替换(除了multipart/form-data
输入,它可以按照parse_multipart()
所述进行处理)。
- cgi.parse_multipart(fp, pdict, encoding='utf-8', errors='replace', separator='&')¶
解析类型为 multipart/form-data 的输入(用于文件上传)。参数为 fp,表示输入文件;pdict,表示包含 Content-Type 标头中其他参数的字典;以及 encoding,表示请求编码。
返回一个与
urllib.parse.parse_qs()
相同的字典:键是字段名称,每个值是该字段的值列表。对于非文件字段,该值是一个字符串列表。这很容易使用,但如果你期望上传兆字节的数据,它就不太好用了——在这种情况下,请使用
FieldStorage
类,它更加灵活。在 3.7 版本中变更: 添加了 encoding 和 errors 参数。对于非文件字段,该值现在是一个字符串列表,而不是字节。
在 3.10 版本中变更: 添加了 separator 参数。
从 3.11 版本开始弃用,将在 3.13 版本中移除: 此函数,与
cgi
模块中的其他部分一样,已弃用。它可以使用email
包(例如email.message.EmailMessage
/email.message.Message
)中的功能来替换,该包实现了相同的 MIME RFC,或者使用 multipart PyPI 项目。
- cgi.parse_header(string)¶
将 MIME 标头(例如 Content-Type)解析为一个主值和一个参数字典。
从 3.11 版本开始弃用,将在 3.13 版本中移除: 此函数,与
cgi
模块中的其他部分一样,已弃用。它可以使用email
包中的功能来替换,该包实现了相同的 MIME RFC。例如,使用
email.message.EmailMessage
from email.message import EmailMessage msg = EmailMessage() msg['content-type'] = 'application/json; charset="utf8"' main, params = msg.get_content_type(), msg['content-type'].params
- 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”),则 cgi.py
脚本已正确安装。如果您对自己的脚本执行相同的过程,您现在应该能够调试它。
下一步可能是从您的脚本中调用 cgi
模块的 test()
函数:用以下单行语句替换其主代码
cgi.test()
这应该产生与安装 cgi.py
文件本身获得的结果相同的结果。
当普通 Python 脚本引发未处理的异常时(无论出于何种原因:模块名称中的拼写错误、无法打开的文件等),Python 解释器会打印一个漂亮的回溯并退出。虽然 Python 解释器在您的 CGI 脚本引发异常时仍然会这样做,但大多数情况下,回溯最终会出现在 HTTP 服务器的日志文件之一中,或者完全被丢弃。
幸运的是,一旦您设法让您的脚本执行一些代码,您就可以使用 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 脚本将以其运行的用户身份读取或写入它们:这通常是 Web 服务器运行的用户身份,或者为 Web 服务器的
suexec
功能显式指定的用户身份。不要尝试为 CGI 脚本赋予 set-uid 模式。这在大多数系统上不起作用,也是安全隐患。
脚注