学习使用 Python 在 Web 服务器中创建并重用组件。Python 社区创建了 Web 服务器网关接口(Web Server Gateway Interface,WSGI),这是创建跨服务器和框架工作的 Python Web 组件的标准。它提供了一种利用许多不同的 Web 工具开发 Web 应用程序的方法。本文介绍了 WSGI 并展示如何开发出能够集成到设计出色的 Web 应用程序中的组件。
Web 成功的主要原因是它的灵活性。您会发现在设计、开发和部署 Web 站点和应用程序方面,每个开发人员几乎都有自己的特色。尽管有大量的选择,但是一个 Web 开发人员经常选择惟一的 Web 设计工具、页面风格、内容语言、Web 服务器、中间件和 DBMS 技术的组合,使用不同的实现语言和辅助工具包。为了给所有这些元素一起工作提供最大的灵活性,应该尽可能地通过组件提供 Web 的功能。这些组件应该执行有限数量的任务,并相互很好地配合工作。这么说很容易,但是在实践中完成这一要求是非常困难的,因为有许多实现 Web 技术的不同方法。
因此开发人员期望着 Web 组件协同工作标准的成熟。这些重要标准的一部分已经开发完成,而且最成功的 Web 开发平台已经成为它们的支柱。突出的例子包括 Java servlet API 和 Ruby on Rails 框架。一些长期流行的 Web 编程语言直到最近才提供了相同的组件化水平,而且借鉴了先前 Web 框架组件标准的经验。一个例子是 Zend Framework for PHP(参见 参考资料)。另一个例子是 Python 的 Web 服务器网关接口(WSGI)。
许多人抱怨流行的 Python 编程语言有太多的 Web 框架,从众所周知的 entrant(比如 Zope)到 under-the-radar 框架(比如 SkunkWeb)。有些人认为只要有一些基础标准化了,这种差异可能是件好事。Python 和 Web 专家 Phillip J. Eby 从事这样的标准化工作。他撰写的 Python Enhancement Proposal(PEP)333 定义了 WSGI。
WSGI 的目标是支持 Python 框架之间更大的协同工作能力。WSGI 的成功会产生一个插件组件生态系统,在这个系统中可以用您喜欢的框架获得最大的灵活性。在本文中,我将介绍 WSGI,主要关注它作为可重用 Web 组件架构的使用方法。在所有讨论和示例代码中,假设您正在使用 Python 2.4 或者更新的版本。
WSGI 的基本架构
WSGI 是在相当严格的约束条件下开发的,但是最重要的是需要对先于它的 Web 框架的向下兼容性。很遗憾,这个约束意味着 WSGI 不能像 Python 开发人员习惯的那么简洁和透明。通常,必须直接处理 WSGI 的开发人员只有那些需要构建框架和可重用组件的开发人员。大多数普通的 Web 开发人员会挑选容易使用的框架,而不接触 WSGI 的细节。
如果想开发可重用 Web 组件,就必须理解 WSGI。需要理解的第一件事是以 WSGI 的角度来看 Web 应用程序是如何构造的。图 1 说明了这个结构。
处理 XHTML 的大胆步骤
许多组件技术相当复杂,所以作为指导的例子最好是简单的东西。这不是使用 WSGI 的案例,事实上,我将要介绍一个非常实际的例子。许多开发者更喜欢使用 XHTML Web 页面,因为 XML 技术比 “标记汤” 的 HTML 更容易管理,而且 Web 站点倾向于更容易由机器读取。问题是并非所有的 Web 浏览器都能正确地支持 XHTML。清单 1(safexhtml.py)是一个 WSGI 中间件模块,它检查到来的请求,看浏览器是否支持 XHTML;如果不支持,就将任何 XHTML 响应翻译成普通的 HTML。可以使用这样的模块让所有主要应用程序代码产生 XHTML,而中间件负责任何需要翻译成 HTML 的地方。仔细查看 清单 1 并试着把它与前面描述的 WSGI 中间件过程相结合。我提供了足够的注释,您可以识别出代码的不同阶段。
清单 1(safexhtml.py). WSGI 中间件为不能处理 XHTML 的浏览器把 XHTML 翻译成 HTML
import cStringIO
from xml import sax
from Ft.Xml import CreateInputSource
from Ft.Xml.Sax import SaxPrinter
from Ft.Xml.Lib.HtmlPrinter import HtmlPrinter
XHTML_IMT = "application/xhtml+xml"
HTML_CONTENT_TYPE = 'text/html; charset=UTF-8'
class safexhtml(object):
"""
Middleware that checks for XHTML capability in the client and translates
XHTML to HTML if the client can't handle it
"""
def __init__(self, app):
#Set-up phase
self.wrapped_app = app
return
def __call__(self, environ, start_response):
#Handling a client request phase.
#Called for each client request routed through this middleware
#Does the client specifically say it supports XHTML?
#Note saying it accepts */* or application/* will not be enough
xhtml_ok = XHTML_IMT in environ.get('HTTP_ACCEPT', '')
#Specialized start_response function for this middleware
def start_response_wrapper(status, response_headers, exc_info=None):
#Assume response is not XHTML; do not activate transformation
environ['safexhtml.active'] = False
#Check for response content type to see whether it is XHTML
#That needs to be transformed
for name, value in response_headers:
#content-type value is a media type, defined as
#media-type = type "/" subtype *( ";" parameter )
if ( name.lower() == 'content-type'
and value.split(';')[0] == XHTML_IMT ):
#Strip content-length if present (needs to be
#recalculated by server)
#Also strip content-type, which will be replaced below
response_headers = [ (name, value)
for name, value in response_headers
if ( name.lower()
not in ['content-length', 'content-type'])
]
#Put in the updated content type
response_headers.append(('content-type', HTML_CONTENT_TYPE))
#Response is XHTML, so activate transformation
environ['safexhtml.active'] = True
break
#We ignore the return value from start_response
start_response(status, response_headers, exc_info)
#Replace any write() callable with a dummy that gives an error
#The idea is to refuse support for apps that use write()
def dummy_write(data):
raise RuntimeError('safexhtml does not support the deprecated
write() callable in WSGI clients')
return dummy_write
if xhtml_ok:
#The client can handle XHTML, so nothing for this middleware to do
#Notice that the original start_response function is passed
#On, not this middleware's start_response_wrapper
for data in self.wrapped_app(environ, start_response):
yield data
else:
response_blocks = [] #Gather output strings for concatenation
for data in self.wrapped_app(environ, start_response_wrapper):
if environ['safexhtml.active']:
response_blocks.append(data)
yield '' #Obey buffering rules for WSGI
else:
yield data
if environ['safexhtml.active']:
#Need to convert response from XHTML to HTML
xhtmlstr = ''.join(response_blocks) #First concatenate response
#Now use 4Suite to transform XHTML to HTML
htmlstr = cStringIO.StringIO() #Will hold the HTML result
parser = sax.make_parser(['Ft.Xml.Sax'])
handler = SaxPrinter(HtmlPrinter(htmlstr, 'UTF-8'))
parser.setContentHandler(handler)
#Don't load the XHTML DTDs from the Internet
parser.setFeature(sax.handler.feature_external_pes, False)
parser.parse(CreateInputSource(xhtmlstr))
yield htmlstr.getvalue()
return
import sys
from wsgiref.simple_server import make_server
from safexhtml import safexhtml
XHTML = open('test.xhtml').read()
XHTML_IMT = "application/xhtml+xml"
HTML_IMT = "text/html"
PORT = 8000
def app(environ, start_response):
print "using IMT", app.current_imt
start_response('200 OK', [('Content-Type', app.current_imt)])
#Swap the IMT used for response (alternate between XHTML_IMT and HTML_IMT)
app.current_imt, app.other_imt = app.other_imt, app.current_imt
return [XHTML]
app.current_imt=XHTML_IMT
app.other_imt=HTML_IMT
httpd = make_server('', PORT, safexhtml(app))
print 'Starting up HTTP server on port %i...'%PORT
# Respond to requests until process is killed
httpd.serve_forever()