通用网关接口(CGI)是Web开发的基础技术标准, 它定义了Web服务器(如Apache/Nginx)如何将客户端请求(通常来自Web表单)转发给外部程序(如Python脚本)处理的规范机制。 通过CGI协议,服务器会将HTTP请求参数注入环境变量, Python脚本则通过标准输入输出与服务器交互,最终动态生成网页返回客户端。
这种轻量级方案使开发者无需部署专用应用服务器即可快速构建Web应用,
Python标准库中的cgi
模块(含FieldStorage等工具类)为此提供了完整支持,
虽然现代Web开发已转向WSGI/ASGI等更高效的协议,但理解CGI仍有助于掌握Web底层交互原理,
详细技术文档可参考Python官方Web编程指南(http://wiki.python.org/moin/WebProgramming)。
Python CGI编程的关键工具是模块cgi
,另一个对开发CGI脚本很有帮助的模块是cgitb
。
要让CGI脚本能够通过Web进行访问(和运行),
必须将其放在Web服务器能够访问的地方、添加!#行
并设置合适的文件权限。
接下来依次介绍这三个步骤。
这里假设能够访问Web服务器。换而言之,能够将内容发布到Web。
通常,要将内容发布到Web,只需将网页、图像等放入特定的目录(在UNIX中通常为public_html
)即可。
如果不知道如何将内容发布到Web,请咨询Internet服务提供商(ISP)或系统管理员。
提示:如果使用的是macOS系统,应随操作系统一起安装了Apache Web服务器。 要开启此服务器,可在系统首选项中的共享首选项面板中选择复选框“Web共享”。
如果只是想尝试使用CGI,可在Python中使用模块http.server
直接运行一个临时Web服务器。
与其他模块一样,可通过向Python可执行文件提供开关-m
来导入并运行这个模块。
如果同时 指定了--cgi
,启动的服务器将支持CGI。请注意,
这个服务器将提供运行它时所在目录中的文件, 因此务必确保这个目录中没有机密内容。
$ python -m http.server --cgi
Serving HTTP on 0.0.0.0 port 8000 ...
CGI程序也必须放在可通过Web访问的目录中。另外,必须将其标识为CGI脚本, 以免Web服务器以网页的方式提供其源代码。为此,有两种常见的方式:
- 将脚本放在子目录
cgi-bin
中; - 将脚本文件的扩展名指定为
.cgi
。
具体的工作原理随服务器而异。如果心存疑虑,请咨询ISP或系统管理员。
(例如,如果使用的是Apache,可能需要对目标目录启用ExecCGI选项。)
如果使用的是模块http.server
中的服务器,应使用子目录cgi-bin
。
将脚本放到正确的位置(还可能给它指定特定的文件扩展名)后,必须在其开头添加一个 !#
行。
通过添加 !#
行,无需显式地执行Python解释器就能执行脚本。
通常,这只是提供了便利,但对CGI脚本来说却至关重要,因为如果没有 !#
行,
Web服务器将不知道如何执行脚本。
(Web服务器只知道脚本可能是使用Perl、Ruby等其他编程语言编写的。)
一般而言,只需在脚本开头添加如下行即可:
#!/usr/bin/env python
请注意,它必须是第一行(之前没有空行)。若这样做无效,
就得确定Python可执行文件的准确位置,并在 !#
行中使用完整的路径,如下所示:
#!/usr/bin/python
如果同时安装了Python 2和Python 3,可能需要将python替换为python3(前面的env解决方案 亦如此)。
若这样做无效,可能存在看不到的错误,具体地说是!#
行以\r\n
而不是\n
结尾,
把Web服务器搞糊涂了。请务必将脚本保存为UNIX风格的纯文本文件。
在Windows中,可使用Python可执行文件的完整路径,如下所示:
#!C:\Python36\python.exe
需要做的最后一件事情是设置合适的文件权限(至少当Web服务器运行在UNIX或Linux系统中时如此)。 必须确保谁都可以读取和执行脚本文件(否则Web服务器将无法运行它), 同时确保只有自己才能写入(这样其他任何人都不能修改自己的脚本)。
提示:如果在Windows中编辑脚本,而它存储在UNIX磁盘服务器中(可使用Samba或FTP 来访问它), 则当修改脚本后,其文件权限可能发生变化。因此,如果脚本无法运行, 请确定其文件权限依然是正确的。
在UNIX中,修改文件权限(或文件模式)的命令为chmod
。要修改文件权限,
只需通过普通用户账户或专为完成Web任务而建立的账户执行下面的命令(这里假设脚本名为somescript.cgi
)。
chmod 755 somescript.cgi
做好所有这些准备工作后,就应该能够像打开网页一样打开脚本来执行。
注意:在浏览器中,不应像打开本地文件那样打开脚本, 而必须使用完整的HTTP URL来打开它, 这样才能通过Web服务器取回它。
通常,CGI脚本不能修改计算机上的任何文件。要让它能够修改文件, 必须显式地赋予它权 限。为此,有两种选择:如果有root(系统管理员)权限, 可为脚本专门创建一个用户账户,并 调整需要修改的文件的所有者; 如果没有root权限,可设置该文件的文件权限, 让系统中的所有用户(包括Web服务器用来运行CGI脚本的账户)都能写入这个文件。 要设置这样的文件权限, 可使用如下命令:
chmod 666 editable_file.txt
警告:使用文件模式666存在潜在的安全风险。除非知道这样做的后果,否则最好不要这样做。
使用CGI程序存在一些安全风险。如果允许CGI脚本对服务器中的文件执行写入操作,
那么这可能被人利用来破坏数据——除非编写脚本时非常小心。同样,
如果直接将用户提供的数据 作为Python代码(如使用exec或eval)或shell命令(如使用os.system
或模块subprocess
)执行,
就可能执行恶意的命令,进而面临极大的风险。
即便在SQL查询中使用用户提供的字符串也很危险,
除非预先仔细审查这些字符串。SQL注入是一种常见的攻击系统的方式。
最简单的CGI脚本类似如下代码:
#!/usr/bin/env python
print('Content-type: text/plain')
Content-type: text/plain
打印一个空行,以结束首部。
print()
print('Hello, world!')
Hello, world!
如果将这些代码保存为文件simple1.cgi
并通过Web服务器打开它,
将看到一个网页,其中只包含纯文本Hello, world!
。
要通过Web服务器打开这个文件,必须将其放在Web服务器能够访问的地方。
在典型的UNIX环境中,可将其放在主目录下的目录public_html
中,
有关这方面的详情,请咨询ISP或系统管理员。如果使用了目录cgi-bin
,也可将这个文件命名为simple1.py
。
这个程序写入到标准输出(如使用print)的内容都出现在网页中,至少大部分内容都如此。
事实上,首先打印的是HTTP首部,这些行包含有关网页的信息。这里关心的唯 一首部是 Content-type
。
Content-type
后面跟着一个冒号、 一个空格和类型名 text/plain
。
这指出这个网页是纯文本的。要指出网页是HTML的,应将这行修改成下面这样:
print('Content-type: text/html')
Content-type: text/html
打印所有的首部后,打印了一个空行,以指出接下来为文档本身。 这里的文档只包含字符串'Hello, world!'。
有时候,编程错误可能导致程序终止,并因未捕获的异常而显示栈跟踪。
通过CGI运行程序时,如果出现这种情况,可能导致Web服务器显示毫无帮助的错误消息甚至黑色网页。
如果能够访问服务器日志(例如,如果使用的是http.server
),
可能能够在这里找到蛛丝马迹。然而,为帮助调试CGI脚本,
标准库提供了一个很有用的模块,名为cgitb
(用于CGI栈跟踪)。
通过导入这个模块并调用其中的函数enable
,可显示一个很有用的网页,
其中包含有关什么地方出了问题的信息。下面代码演示了如何使用模块cgitb
。
显示栈跟踪的CGI脚本(faulty.cgi
)
#!/usr/bin/env python
import cgitb; cgitb.enable()
print('Content-type: text/html\n')
print(1/0)
print('Hello, world!')
在浏览器中通过Web服务器访问这个脚本时,结果如图所示。
到目前为止,所有CGI脚本都只生成输出,而没有使用任何形式的输入。
输入是通过HTML 表单(将在下一节介绍)以键-值对(字段)的方式提供给CGI脚本的。
在CGI脚本中,可使用模块cgi
中的FieldStorage类来获取这些字段。
当创建FieldStorage
实例(应只创建一个)时,它将从请求中取回输入变量(字段),
并通过一个类似于字典的接口将它们提供给脚本。要访问 FieldStorage
中的值,
可通过普通的键查找,但出于一些技术原因(与文件上传相关,这里不讨论),
FieldStorage
的元素并不是需要的值。例如,即便知道请求包含一个名为name
的值,
也不能像下面这样做:
form = cgi.FieldStorage()
name = form['name']
而必须这样做:
form = cgi.FieldStorage()
name = form['name'].value
一种更简单的获取值的方式是使用方法getvalue
。它类似于字典的方法get
,但返回项目的
value
属性的值,如下所示:
form = cgi.FieldStorage()
name = form.getvalue('name', 'Unknown')
在这个示例中,提供了一个默认值('Unknown')。如果没有提供,默认值将为None。在字段 没有值时,将使用默认值。
下面是一个使用cgi.FieldStorage
的简单示例:
从FieldStorage
中获取单个值的CGI脚本(simple2.cgi
)
#!/usr/bin/env python
import cgi
form = cgi.FieldStorage()
/tmp/ipykernel_10109/3838189618.py:1: DeprecationWarning: 'cgi' is deprecated and slated for removal in Python 3.13 import cgi
name = form.getvalue('name', 'world')
print('Content-type: text/plain\n')
Content-type: text/plain
print('Hello, {}!'.format(name))
Hello, world!
在不使用表单的情况下调用CGI脚本。
CGI脚本的输入通常来自提交的表单,但调用CGI脚本时也可直接指定参数。
为此可在指向脚本的URL后面加上问号,再加上用&分隔的键值对。例如,
脚本的URL为 http://www.example.com/simple2.cgi ,
可这样使用参数name=Gumby
和age=42
来调用这个脚本:http://www.example.com/simple2.cgi?name=Gumby&age=42 。
如果这样做,这个 CGI脚本将显示消息Hello, Gumby!而不是Hello, World!(请注意,没有使用参数age
)。
要创建这样的URL查询,可使用模块urllib.parse
中的方法urlencode
:
urlencode({'name': 'Gumby', 'age': '42'})
'age=42&name=Gumby'
可结合使用这种策略和urllib
来创建能够与CGI脚本交互的屏幕抓取程序。然而,
与其在服务器端和客户端都采取这种做法,还不如使用Web服务。
有了处理用户请求的工具,该来创建用户可提交的表单了。 这个表单可以是独立的页面,但这里将它放在脚本中。 要深入地了解如何编写HTML表单(或HTML),可参考介绍HTML的优秀著作(当地书店可能就有不少)。 另外,在网上也能找到很多有关这个主题的信息。与往常一样,发现值得模仿的优秀网页后, 可在浏览器中查看其源代码,方法是从菜单中选择“查看源代码”之类的选项(具体是哪个选项取决于使用的浏览器)。
注意:从CGI脚本中获取信息的主要方式有两种:方法GET和方法POST。 就本章而言,两者的差别并不重要。大致上,GET用于获取信息并在URL中进行查询编码, 而POST可用于任何类型的查询,但对查询进行编码的方式稍有不同。
回到脚本,以下代码是扩展后的版本,包含HTML表单的问候脚本(simple3.cgi
)。
#!/usr/bin/env python
import cgi
form = cgi.FieldStorage()
name = form.getvalue('name', 'world')
print("""Content-type: text/html
<html>
<head>
<title>Greeting Page</title>
</head>
<body>
<h1>Hello, {}!</h1>
<form action='simple3.cgi'>
Change name <input type='text' name='name' />
<input type='submit' />
</form>
</body>
</html>
""".format(name))
Content-type: text/html <html> <head> <title>Greeting Page</title> </head> <body> <h1>Hello, world!</h1> <form action='simple3.cgi'> Change name <input type='text' name='name' /> <input type='submit' /> </form> </body> </html>
在这个脚本开头,与以前一样获取CGI参数name
,并将默认值设置为'world'。
如果在浏览器中打开这个脚本时没有提交任何值,将使用默认值。
接下来,打印了一个简单的HTML页面,其中的标题包含参数name
的值。
另外,这个页面还包含一个HTML表单,该表单的属性action被设置为脚本的名称(simple3.cgi
)。
这意味着提交表单后,将再次运行这个脚本。这个表单只包含一个输入元素,名为name
的文本框。
因此,如果在文本框中输入新名字并提交表单,标题将发生变化,因为现在参数name
包含值。
下图显示了通过Web服务器访问脚本程序的结果。