>>> from env_helper import info; info()
页面更新时间: 2023-06-24 11:29:39
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-9-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

3.2. 使用 CGI 创建动态网页

本章的第一部分讨论了客户端技术,下面将注意力转向服务器端。本节讨论基本的Web编程 技术:通用网关接口(CGI)。CGI是一种标准机制,Web服务器可通过它将(通常是通过Web表 达提供的)查询交给专用程序(如你编写的Python程序),并以网页的方式显示查询结果。这是 一种创建Web应用的简单方式,让你无需编写专用的应用程序服务器。有关Python CGI编程的详 细信息,请参阅Python网站的Web编程主题指南(http://wiki.python.org/moin/WebProgramming )。

Python CGI编程的关键工具是模块cgi,另一个对开发CGI脚本很有帮助的模块是cgitb 。 要让CGI脚本能够通过Web进行访问(和运行),必须将其放在Web服务器能够访问的地方、添加!#行并设置合适的文件权限。接下来依次介绍这三个步骤。

3.2.1. 第一步:准备 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 ...

如果现在将浏览器指向http://127.0.0.1:8000 或 http://localhost:8000 ,将看到运行这个服务器所 在目录的内容。另外,你还将看到服务器提供的有关连接的信息。

CGI程序也必须放在可通过Web访问的目录中。另外,必须将其标识为CGI脚本,以免Web服务器以网页的方式提供其源代码。为此,有两种常见的方式:

  • 将脚本放在子目录cgi-bin中;

  • 将脚本文件的扩展名指定为.cgi。

具体的工作原理随服务器而异。如果你心存疑虑,请咨询ISP或系统管理员。(例如,如果你使用的是Apache,可能需要对目标目录启用ExecCGI选项。)如果你使用的是模块http.server中的 服务器,应使用子目录cgi-bin。

3.2.2. 第二步:添加!#行

将脚本放到正确的位置(还可能给它指定特定的文件扩展名)后,必须在其开头添加一个 !# 行。 通过添加 !# 行,无需显式地执行Python解释器就能执行脚本。 通常,这只是提供了便利,但对CGI脚本来说却至关重要,因为如果没有 !# 行,Web服务器将不知道如何执行脚 本。 (Web服务器只知道脚本可能是使用Perl、Ruby等其他编程语言编写的。)

一般而言,只需在 脚本开头添加如下行即可:

#!/usr/bin/env python

请注意,它必须是第一行(之前没有空行)。如果这样做不管用,就得确定Python可执行文 件的准确位置,并在 !# 行中使用完整的路径,如下所示:

#!/usr/bin/python

如果同时安装了Python 2和Python 3,可能需要将python替换为python3(前面的env解决方案 亦如此)。如果这样做也不管用,可能存在你看不到的错误,具体地说是!#行以:raw-latex:rn而不是:raw-latex:`n结 `尾,把Web服务器搞糊涂了。请务必将脚本保存为UNIX风格的纯文本文件。

在Windows中,可使用Python可执行文件的完整路径,如下所示:

#!C:\Python36\python.exe

3.2.3. 第三步:设置文件权限

需要做的最后一件事情是设置合适的文件权限(至少当Web服务器运行在UNIX或Linux系统 中时如此)。必须确保谁都可以读取和执行你的脚本文件(否则Web服务器将无法运行它),同时 确保只有你才能写入(这样其他任何人都不能修改你的脚本)。

提示 如果你在Windows中编辑脚本,而它存储在UNIX磁盘服务器中(你可使用Samba或FTP 来访问它),则当你修改脚本后,其文件权限可能发生变化。因此,如果你的脚本无法运 行,请确定其文件权限依然是正确的。

在UNIX中,修改文件权限(或文件模式)的命令为chmod。要修改文件权限,只需通过普通 用户账户或专为完成Web任务而建立的账户执行下面的命令(这里假设脚本名为somescript.cgi。

chmod 755 somescript.cgi

做好所有这些准备工作后,就应该能够像打开网页一样打开脚本以执行它。

注意 在浏览器中,不应像打开本地文件那样打开脚本,而必须使用完整的HTTP URL来打开它, 这样才能通过Web(Web服务器)取回它。

通常,CGI脚本不能修改计算机上的任何文件。要让它能够修改文件,必须显式地赋予它权 限。为此,有两种选择:如果有root(系统管理员)权限,可为脚本专门创建一个用户账户,并 调整需要修改的文件的所有者;如果没有root权限,可设置该文件的文件权限,让系统中的所有 用户(包括Web服务器用来运行CGI脚本的账户)都能写入这个文件。要设置这样的文件权限, 可使用如下命令:

chmod 666 editable_file.txt

警告 使用文件模式666存在潜在的安全风险。除非你知道这样做的后果,否则最好不要这样做。

3.2.4. CGI 安全风险

使用CGI程序存在一些安全风险。如果你允许CGI脚本对服务器中的文件执行写入操作,那 么这可能被人利用来破坏数据——除非编写脚本时非常小心。同样,如果直接将用户提供的数据 作为Python代码(如使用exec或eval)或shell命令(如使用os.system或模块subprocess)执行, 就可能执行恶意的命令,进而面临极大的风险。即便在SQL查询中使用用户提供的字符串也很危 险,除非你预先仔细审查这些字符串。SQL注入是一种常见的攻击系统的方式。

3.2.5. 简单的 CGI 脚本

最简单的CGI脚本类似于代码清单15-4。

代码清单15-4 简单的CGI脚本

>>> #!/usr/bin/env python
>>>
>>> print('Content-type: text/plain')
>>> print()# 打印一个空行,以结束首部
>>> print('Hello, world!')
Content-type: text/plain

Hello, world!

如果将这些代码保存为文件simple1.cgi并通过Web服务器打开它,将看到一个网页,其中只 包含纯文本Hello, world!。要通过Web服务器打开这个文件,必须将其放在Web服务器能够访问的 地方。在典型的UNIX环境中,可将其放在主目录下的目录public_html中,这样就可使用URL http://localhost/~username/simple1.cgi (将username替换为你的用户名)来打开它。有关这方面的 详情,请咨询ISP或系统管理员。如果你使用了目录cgi-bin,也可将这个文件命名为simple1.py。

如你所见,这个程序写入到标准输出(如使用print)的内容都出现在网页中——至少大部 分内容都如此。事实上,首先打印的是HTTP首部,这些行包含有关网页的信息。这里关心的唯 一首部是 Content-type 。 如你所见, Content-type 后面跟着一个冒号、 一个空格和类型名 text/plain。这指出这个网页是纯文本的。要指出网页是HTML的,应将这行修改成下面这样:

print('Content-type: text/html')

打印所有的首部后,打印了一个空行,以指出接下来为文档本身。如你所见,这里的文档只 包含字符串’Hello, world!’。

3.2.6. 使用 cgitb 进行调试

有时候,编程错误可能导致程序终止,并因未捕获的异常而显示栈跟踪。通过CGI运行程序 时,如果出现这种情况,可能导致Web服务器显示毫无帮助的错误消息甚至黑色网页。如果你能 够访问服务器日志(例如,如果你使用的是http.server),可能能够在这里找到蛛丝马迹。然而, 为帮助调试CGI脚本,标准库提供了一个很有用的模块,名为cgitb(用于CGI栈跟踪)。通过导 入这个模块并调用其中的函数enable,可显示一个很有用的网页,其中包含有关什么地方出了问 题的信息。代码清单15-5演示了如何使用模块cgitb。

代码清单15-5 显示栈跟踪的CGI脚本(faulty.cgi)

#!/usr/bin/env python

import cgitb; cgitb.enable()
print('Content-type: text/html\n')
print(1/0)
print('Hello, world!')

在浏览器中通过Web服务器访问这个脚本时,结果如图所示。

模块cgitb显示的CGI栈跟踪

图 3.4 模块cgitb显示的CGI栈跟踪

3.2.7. 使用模块 cgi

到目前为止,所有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。在字段 没有值时,将使用默认值。

代码清单15-6是一个使用cgi.FieldStorage的简单示例。

代码清单15-6 从FieldStorage中获取单个值的CGI脚本(simple2.cgi)

>>> #!/usr/bin/env python
>>>
>>> import cgi
>>> form = cgi.FieldStorage()
>>>
>>> name = form.getvalue('name', 'world')
>>> print('Content-type: text/plain\n')
>>> print('Hello, {}!'.format(name))
Content-type: text/plain

Hello, world!
/tmp/ipykernel_64720/2317628978.py:3: DeprecationWarning: 'cgi' is deprecated and slated for removal in Python 3.13
  import cgi

在不使用表单的情况下调用CGI脚本

CGI脚本的输入通常来自提交的表单,但调用CGI脚本时也可直接指定参数。为此可在指 向脚本的URL后面加上问号,再加上用&分隔的键值对。例如,如果指向代码清单15-6所示 脚本的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服务,这将在15.4节介绍。

3.2.8. 简单的表单

有了处理用户请求的工具,该来创建用户可提交的表单了。这个表单可以是独立的页面,但 这里将它放在脚本中。

要深入地了解如何编写HTML表单(或HTML),可参考介绍HTML的优秀著作(当地书店可 能就有不少)。另外,在网上也能找到很多有关这个主题的信息。与往常一样,发现值得模仿的 优秀网页后,可在浏览器中查看其源代码,方法是从菜单中选择“查看源代码”之类的选项(具 体是哪个选项取决于你使用的浏览器)。

注意 从CGI脚本中获取信息的主要方式有两种:方法GET和方法POST。就本章而言,两者的 差别并不重要。大致上,GET用于获取信息并在URL中进行查询编码,而POST可用于任 何类型的查询,但对查询进行编码的方式稍有不同。

回到我们的脚本,代码清单15-7是扩展后的版本。

代码清单15-7 包含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服务器访问脚本程序的结果。

执行代码所示CGI脚本的结果

图 3.5 执行代码所示CGI脚本的结果