Python 创建WSGI应用程序,首先使用一个简单的URL模式匹配表达式来定义应用程序中的唯一路由。在一个更大或更复杂的应用程序中,模式可能不止一种:
import re
path_pat= re.compile(r"^/anscombe/(?P<dataset>.*?)/?$")
通过这种模式,可以在路径顶层定义一个关于WSGI的完整脚本。在本例中,该脚本是anscombe
。我们把下一级路径作为通过安斯库姆四重奏选取的数据集,数据集的值落于I
、II
、III
或IV
。
使用命名参数作为选取条件。在许多情况下,可以使用如下语法来描述RESTful API:
/anscombe/{dataset}/
这种理想化的模式转化为了适当的正则表达式,并在路径中保留了数据集选择器的名称。
一些URL路径示例如下,它们演示了这种模式的工作原理:
>>> m1 = path_pat.match( "/anscombe/I" )
>>> m1.groupdict()
{'dataset': 'I'}
>>> m2 = path_pat.match( "/anscombe/II/" )
>>> m2.groupdict()
{'dataset': 'II'}
>>> m3 = path_pat.match( "/anscombe/" )
>>> m3.groupdict()
{'dataset': ''}
这些示例都显示了从URL路径中解析得到的详细信息。如果指定了某个序列的名称,则它会出现在路径中;如果没有指定序列名称,则模式返回的是一个空字符串。
完整的WSGI应用程序如下:
import traceback
import urllib.parse
def anscombe_app(
environ: Dict, start_response: SR_Func
) -> Iterable[bytes]:
log = environ['wsgi.errors']
try:
match = path_pat.match(environ['PATH_INFO'])
set_id = match.group('dataset').upper()
query = urllib.parse.parse_qs(environ['QUERY_STRING'])
print(environ['PATH_INFO'], environ['QUERY_STRING'],
match.groupdict(), file=log)
dataset = anscombe_filter(set_id, raw_data())
content_bytes, mime = serialize(
query['form'][0], set_id, dataset)
headers = [
('Content-Type', mime),
('Content-Length', str(len(content_bytes))),
]
start_response("200 OK", headers)
return [content_bytes]
except Exception as e: # pylint: disable=broad-except
traceback.print_exc(file=log)
tb = traceback.format_exc()
content = error_page.substitute(
title="Error", message=repr(e), traceback=tb)
content_bytes = content.encode("utf-8")
headers = [
('Content-Type', "text/html"),
('Content-Length', str(len(content_bytes))),
]
start_response("404 NOT FOUND", headers)
return [content_bytes]
该应用程序会从请求中提取两条信息:环境字典中的PATH_INFO
键和QUERY_STRING
键。PATH_INFO
请求会定义要提取的数据集。QUERY_STRING
请求将指定输出的格式。
请注意,查询字符串可能非常复杂。我们使用了urllib.parse
模块来准确定位查询字符串中所有名称和值的配对,而不是简单地假定它是一个类似于?form=json
的字符串。在通过查询字符串提取得到的字典中,可以在query['form'][0]
中找到以form
为键的值。这应该是定义过的格式之一,否则会引发异常,并显示一个错误页面。
定位到路径和查询字符串后,用粗体强调了应用程序的处理过程。这两条语句依赖三个函数来对结果进行收集、过滤和序列化。
- 函数
raw_data()
从文件中读取原始数据,结果是一个包含一组Pair
对象的字典。 - 函数
anscombe_filter()
接收一个选择字符串和源数据字典,并返回Pair
对象的单个列表。 serialize()
函数随后将该配对列表序列化为字节码。序列化器会生成字节码,这样就可以用合适的报头打包并返回了。
我们选择了生成一个HTTP的Content-Length
报头作为结果的一部分。这个报头并不是必需的,但有助于下载大文件。由于我们决定发送该报头,因此必须用序列化的数据创建字节对象,以便统计这些字节。
如果选择忽略Content-Length
报头,那么可以大规模地改变这个应用程序的结构。可以把每个序列化器都更改为生成器函数,并且它们在创建时会生成字节对象。对于大型数据集来说,这种优化可能有益,然而对于关注下载进度的用户可能不太友好,因为浏览器无法显示下载的完成情况。
一种常见的优化是将事务分解为两部分。第一部分用于计算结果并将文件放入Downloads目录。响应是一个带有Location
报头的302 FOUND
,用于标识需下载的文件。通常大部分客户端会基于这个原始响应来请求文件。可以用不涉及Python应用的Apache httpd或者Nginx下载文件。
本例将所有错误视为404 NOT FOUND
错误会引起误导,因为可能是其他环节出了问题。更复杂的错误处理会引入更多的try:/except:
块来提供更多信息反馈。
出于调试的目的,在生成的Web页面中提供了Python的栈跟踪。在没有调试需求的环境中,这是一种非常糟糕的做法。来自API的反馈应该只用于处理请求,无他。栈跟踪会向潜在的恶意用户提供过多信息。