# 21.2. `cgi` - 公共网关接口支持

源代码： Lib / cgi.py

通用网关接口（CGI）脚本的支持模块。

这个模块定义了一些由Python编写的CGI脚本使用的实用程序。

## 21.2.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!")

## 21.2.2. 使用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字典一样索引。It allows membership testing with the `in` operator, and also supports the standard dictionary method `keys()` and the built-in function `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`属性生成字段的字符串值。`getvalue()`方法直接返回此字符串值；它还接受可选的第二个参数作为默认值，如果请求的键不存在则返回。

如果提交的表单数据包含多个具有相同名称的字段，则`form[key]`检索的对象不是`FieldStorage`或`MiniFieldStorage`实例，但是这种实例的列表。类似地，在这种情况下，`form.getvalue(key)`将返回字符串列表。如果您期望这种可能性（当您的HTML表单包含多个具有相同名称的字段），请使用`getlist()`方法，它总是返回值列表（以便您不需要特殊 - case单项案例）。例如，此代码连接任意数量的用户名字段，用逗号分隔：

    value = form.getlist("username")
    usernames = ",".join(value)

如果字段表示上传的文件，通过`value`属性或`getvalue()`方法访问该值将以字节形式读取内存中的整个文件。这可能不是你想要的。您可以通过测试`filename`属性或`file`属性来测试上传的文件。你可以在 `FieldStorage` 实例的垃圾回收器部分自动关闭前从 `file` 属性读取数据( `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（或者另一个MIME类型匹配multipart / * 。在这种情况下，它可以像顶层表单对象一样通过递归迭代。

当表单以“旧”格式（作为查询字符串或作为application / x-www-form-urlencoded类型的单个数据部分）提交时，项目实际上将是class `MiniFieldStorage`。在这种情况下，`list`，`file`和`filename`属性始终为`None`。

通过POST提交并具有查询字符串的表单将同时包含`FieldStorage`和`MiniFieldStorage`项目。

在版本3.4中更改：在创建`FieldStorage`实例的垃圾回收时，`file`属性会自动关闭。

在3.5版本中已更改：向`FieldStorage`类添加了上下文管理协议支持。

## 21.2.3. 更高层次的接口

上一节介绍如何使用`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]如果没有这样的表单字段或值，则该方法返回由可选参数默认指定的值。如果未指定，此参数默认为`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)

## 21.2.4. 功能

如果你想要更多的控制，或者如果你想在其他情况下使用在这个模块中实现的一些算法，这些是有用的。

`cgi.``parse`(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False)

  在环境或文件中解析查询（文件默认为`sys.stdin`）。keep_blank_values和strict_parsing参数会不变地传递到`urllib.parse.parse_qs()`。

`cgi.``parse_qs`(qs, keep_blank_values=False, strict_parsing=False)

  此模块中已弃用此函数。请改用`urllib.parse.parse_qs()`。这里维护它只是为了向后兼容。

`cgi.``parse_qsl`(qs, keep_blank_values=False, strict_parsing=False)

  此模块中已弃用此函数。请改用`urllib.parse.parse_qsl()`。这里维护它只是为了向后兼容。

`cgi.``parse_multipart`(fp, pdict)

  解析类型multipart / form-data的输入（用于文件上传）。对于包含Content-Type头中的其他参数的字典，参数为输入文件的fp和pdict。

  返回一个字典就像`urllib.parse.parse_qs()`键是字段名，每个值都是该字段的值列表。这是很容易使用，但不是很好，如果你期望兆字节上传 - 在这种情况下，使用`FieldStorage`类，而不是更灵活。

  请注意，这不会解析嵌套的多部分零件 - 使用`FieldStorage`。

`cgi.``parse_header`(string)

  将MIME标头（例如Content-Type）解析为主值和参数字典。

`cgi.``test`()

  稳健的测试CGI脚本，可用作主程序。以HTML格式写入最少的HTTP标头并格式化提供给脚本的所有信息。

`cgi.``print_environ`()

  在HTML中格式化shell环境。

`cgi.``print_form`(form)

  在HTML中格式化表单。

`cgi.``print_directory`()

  使用HTML格式化当前目录。

`cgi.``print_environ_usage`()

  在HTML中打印有用（由CGI使用）环境变量的列表。

`cgi.``escape`(s, quote=False)

  将字符串s中的字符`'&'`，`'<'`和`'>'`如果您需要显示可能在HTML中包含此类字符的文本，请使用此选项。If the optional flag quote is true, the quotation mark character (`"`) is also translated; this helps for inclusion in an HTML attribute value delimited by double quotes, as in `<a href="...">`. 请注意，单引号从不翻译。

  自版本3.2后已弃用：此函数不安全，因为默认情况下quote为false，因此已弃用。请改用`html.escape()`。

## 21.2.5. 关心安全

有一个重要的规则：如果你调用一个外部程序（通过`os.system()`或`os.popen()`函数。None None 即使部分URL或字段名称不可信任，因为请求不必来自您的表单！

为了安全起见，如果必须将从表单获取的字符串传递给shell命令，则应确保字符串只包含字母数字字符，破折号，下划线和句点。

## 21.2.6. 在Unix系统上安装CGI脚本

阅读您的HTTP服务器的文档，并与您的本地系统管理员确定应该安装CGI脚本的目录；通常这在服务器树中的目录`cgi-bin`中。

确保您的脚本是可读的和可执行的“其他”； Unix文件模式应为`0o755`八进制（使用`chmod 0755 filename`）。确保脚本的第一行包含`#!`从第1列开始，然后是Python解释器的路径名，for实例：

    #!/usr/local/bin/python

确保Python解释器存在，并且可由“others”执行。

确保脚本需要读取或写入的任何文件分别由“其他”读取或写入 - 它们的模式应为`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脚本的部分）。

## 21.2.7. 测试你的CGI脚本

不幸的是，当您从命令行尝试它时，CGI脚本通常不会运行，从服务器运行时，从命令行完美运行的脚本可能会神秘失败。有一个原因，你仍然应该从命令行测试你的脚本：如果它包含语法错误，Python解释器将不会执行它，HTTP服务器将很可能发送一个神秘的错误给客户端。

假设你的脚本没有语法错误，但它不工作，你别无选择，只能阅读下一节。

## 21.2.8. 调试CGI脚本

首先，检查小的安装错误 - 仔细阅读上面的部分安装您的CGI脚本可以节省你很多时间。如果您不知道是否已正确理解安装过程，请尝试将此模块文件（`cgi.py`）的副本作为CGI脚本安装。None 给它正确的模式等，并发送一个请求。如果它安装在标准的`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解释器打印出一个很好的traceback并退出。虽然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解释器来打印traceback。输出的内容类型设置为纯文本，这将禁用所有HTML处理。如果您的脚本工作，原始HTML将由您的客户端显示。如果引发异常，最有可能在打印前两行之后，将显示回溯。因为没有HTML解释，traceback将是可读的。

## 21.2.9. 常见问题和解决方案

- 大多数HTTP服务器缓冲CGI脚本的输出，直到脚本完成。这意味着在脚本运行时无法在客户端的显示器上显示进度报告。
- None
- 检查HTTP服务器的日志文件。（在单独的窗口中可以使用`尾 -f 日志文件`
- 始终通过执行类似`python script.py`的脚本来检查语法错误。
- 如果脚本没有任何语法错误，请尝试将`import cgitb； cgitb.enable() 脚本的顶部。`
- 当调用外部程序时，请确保找到它们。通常，这意味着使用绝对路径名 - `PATH`通常不会在CGI脚本中设置为非常有用的值。
- When reading or writing external files, make sure they can be read or written by the userid under which your CGI script will be running: this is typically the userid under which the web server is running, or some explicitly specified userid for a web server’s `suexec` feature.
- 不要试图给一个CGI脚本set-uid模式。None

脚注

[1] | 请注意，HTML规范的一些最新版本确实应该提供字段值的顺序，但是知道是否从一个符合的浏览器，或者甚至从一个浏览器接收到一个请求是冗长和容易出错的。