cgi
cgi — Common Gateway Interface support
源代码:
Lib / cgi.py
通用网关接口(CGI)脚本的支持模块。
该模块定义了许多供Python编写的CGI脚本使用的实用程序。
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,它允许客户端软件以头部,内嵌图像等方式显示格式良好的文本。下面是打印简单HTML代码的Python代码:
print "<TITLE>CGI script output</TITLE>"
print "<H1>This is my first CGI script</H1>"
print "Hello, world!"
2.使用cgi模块
从写作开始import cgi
。不要使用from cgi import *
- 模块为自己使用或为了向后兼容性定义各种名称,而不需要在命名空间中使用。
当你写一个新的脚本时,考虑添加这些行:
import cgitb
cgitb.enable()
这将激活一个特殊的异常处理程序,如果发生任何错误,它将在Web浏览器中显示详细的报告。如果您不想将程序的内容显示给脚本的用户,您可以将这些报告保存到文件中,代码如下:
import cgitb
cgitb.enable(display=0, logdir="/path/to/logdir")
在脚本开发过程中使用此功能非常有用。生成的报告cgitb
提供的信息可以为您节省大量时间来追踪错误。您可以随时在cgitb
测试脚本时删除该行,并确信它能正常工作。
要获得提交的表单数据,最好使用FieldStorage
该类。本模块中定义的其他类主要用于向后兼容。实例化它只有一次,没有参数。它从标准输入或环境读取表单内容(取决于根据CGI标准设置的各种环境变量的值)。由于它可能会消耗标准输入,因此应仅实例化一次。
该FieldStorage
实例可以像Python字典一样编入索引。它允许in
运营商进行成员资格测试,并支持标准字典方法keys()
和内置函数len()
。包含空字符串的表单字段将被忽略,不会出现在字典中; 为了保留这些值,在创建实例时为可选的keep_blank_values
关键字参数提供一个真正的值FieldStorage
。
例如,下面的代码(其假定的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
实例的属性产生字段的字符串值。该getvalue()
方法直接返回该字符串值; 它也接受一个可选的第二个参数作为默认返回,如果请求的键不存在。
如果提交的表单数据包含多个具有相同名称的字段,则检索到的对象form[key]
不是一个FieldStorage
或MiniFieldStorage
实例,而是这些实例的列表。同样,在这种情况下,form.getvalue(key)
会返回一个字符串列表。如果您希望这种可能性(当您的HTML表单包含多个具有相同名称的字段时),请使用getlist()
总是返回值列表的方法(这样您就不需要特别处理单个项目的情况)。例如,此代码连接任意数量的用逗号分隔的用户名字段:
value = form.getlist("username")
usernames = ",".join(value)
如果一个字段表示上传的文件,则通过value
属性或getvalue()
方法访问该值将以字符串形式读取内存中的整个文件。这可能不是你想要的。您可以通过测试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
如果在获取上传文件的内容时遇到错误(例如,当用户通过单击“上一步”或“取消”按钮中断表单提交时)done
,该字段的对象属性将设置为值-1。
文件上传草案标准考虑了从一个字段上传多个文件的可能性(使用递归多部分/ *
编码)。发生这种情况时,该项目将成为类似字典的FieldStorage
项目。这可以通过测试它的type
属性来确定,该属性应该是multipart / form-data
(或者可能是与multipart / *
匹配的另一个MIME类型)。在这种情况下,它可以像顶级窗体对象一样递归迭代。
当以“旧”格式(作为查询字符串或作为application / x-www-form-urlencoded
类型的单个数据部分)提交表单时
,这些项目实际上就是该类的实例MiniFieldStorage
。在这种情况下,list
,file
,和filename
属性始终None
。
通过POST提交的也包含查询字符串的表单将包含两者FieldStorage
和MiniFieldStorage
项目。
3.更高层次的接口
2.2版本中的新功能。
上一节解释了如何使用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])
此方法始终只返回与表单字段名称
关联的一个值
。该方法只会返回第一个值
,以防在此名称
下发布更多值
。请注意,接收值
的顺序可能因浏览器而异,因此不应计算在内。[1]如果不存在这样的表单域或值
,则该方法返回由可选参数default
指定的值
。None
如果未指定,此参数默认为。
FieldStorage.getlist(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)
4.旧的类
自2.6版弃用:这些类存在于早期版本的cgi
模块中,仍然支持向后兼容。新的应用程序应该使用FieldStorage
该类。
SvFormContentDict
将单值表单内容存储为字典; 它假定每个字段名称只出现一次。
FormContentDict
将多个价值表单内容存储为字典(表单项是值列表)。如果您的表单包含具有相同名称的多个字段,则这很有用
其他类(FormContent
,InterpFormContentDict
)仅用于向后兼容真正的旧应用程序。
5.功能
如果你想要更多的控制,或者如果你想在其他情况下使用本模块中实现的一些算法,这些都很有用。
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]])
该功能在本模块中已弃用。urlparse.parse_qs()
改为使用。这里只是为了向后兼容而维护它。
cgi.parse_qsl(qs[, keep_blank_values[, strict_parsing]])
该功能在本模块中已弃用。urlparse.parse_qsl()
改为使用。这里只是为了向后兼容而维护它。
cgi.parse_multipart(fp, pdict)
解析multipart / form-data
类型的输入(用于文件上传)。参数是输入文件的fp
,包含Content-Type
头中其他参数的字典的pdict
。
就像urlparse.parse_qs()
键是字段名一样返回字典,每个值都是该字段的值列表。这很容易使用,但如果您预计会上传兆字节,则不会太好 - 在这种情况下,请使用FieldStorage
类更灵活的类。
请注意,这不会分析嵌套的多部分 - FieldStorage
用于此。
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使用)环境变量的列表。
cgi.escape(s[, quote])
转换角色'&','<'并'>'在串小号到HTML安全序列。如果您需要显示可能包含HTML中的此类字符的文本,请使用此选项。如果可选的标志引用为真,则引号mark(")也被翻译; 这有助于包含在由双引号分隔的HTML属性值中,如in <a href="...">。请注意,单引号永远不会翻译。
如果要引用的值可能包含单引号或双引号字符,或者同时考虑使用模块中的quoteattr()
函数xml.sax.saxutils
。
6.关心安全
有一条重要的规则:如果你调用一个外部程序(通过os.system()
或os.popen()
函数或其他具有类似功能的程序),请确保不要将从客户端收到的任意字符串传递给shell。这是一个众所周知的安全漏洞,Web上任何地方的聪明黑客都可以利用容易理解的CGI脚本来调用任意shell命令。即使部分URL或字段名称也不可信,因为请求不必来自您的表单!
为了安全起见,如果您必须将从窗体获得的字符串传递给shell命令,则应确保该字符串仅包含字母数字字符,破折号,下划线和句点。
7.在Unix系统上安装CGI脚本
阅读您的HTTP服务器的文档,并与您的本地系统管理员联系以查找应安装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脚本的部分)。
8.测试你的CGI脚本
不幸的是,当从命令行尝试CGI脚本时,通常不会运行CGI脚本,而从命令行完美运行的脚本在从服务器运行时可能会神秘失败。还有一个原因是为什么你仍然应该从命令行测试脚本:如果它包含语法错误,Python解释器根本不会执行它,并且HTTP服务器很可能会向客户端发送一个神秘错误。
假设你的脚本没有语法错误,但它不起作用,你别无选择,只能阅读下一节。
9.调试CGI脚本
首先,检查一下安装错误 - 仔细阅读上面关于安装CGI脚本的部分可以节省很多时间。如果您想知道您是否正确理解了安装过程,请尝试安装此模块文件(cgi.py
)的副本作为CGI脚本。当作为脚本调用时,该文件将以HTML形式转储其环境和表单的内容。给它正确的模式等,并发送一个请求。如果它安装在标准cgi-bin
目录中,应该可以通过在表单的浏览器中输入URL来发送请求:
http://yourhostname/cgi-bin/cgi.py?name=Joe+Blow&addr=At+Home
如果这给404类型的错误,服务器找不到脚本 - 也许你需要将它安装在不同的目录中。如果它提供了另一个错误,那么在尝试继续之前应该修复一个安装问题。如果您获得了格式良好的环境和表单内容列表(在本例中,这些字段应该被列为值为“在家”和“名称”值为“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解释,回溯将是可读的。
10.常见问题和解决方案
- 大多数HTTP服务器会缓存CGI脚本的输出,直到脚本完成。这意味着在脚本运行时不可能在客户端的显示器上显示进度报告。
- 检查上面的安装说明。
- 检查HTTP服务器的日志文件。(
tail -f logfile
在单独的窗口中可能会有用!)
- 首先通过做类似的事情来检查脚本是否有语法错误
python script.py
。
- 如果您的脚本没有任何语法错误,请尝试添加
import cgitb; cgitb.enable()
到脚本的顶部。
- 调用外部程序时,确保可以找到它们。通常,这意味着使用绝对路径名 -
PATH
通常不会在CGI脚本中设置为非常有用的值。
- 在读取或编写外部文件时,请确保可以在运行CGI脚本的用户标识下读取或写入它们:这通常是运行Web服务器的用户标识,或者是Web服务器
suexec
功能的一些明确指定的用户标识。
- 不要试图给一个CGI脚本一个set-uid模式。这在大多数系统上不起作用,并且也是安全责任。
注
1 | 请注意,HTML规范的某些最新版本确实应该提供字段值的顺序,但知道从合格的浏览器收到请求,还是从浏览器收到请求都很乏味且容易出错。 |
---|