Werkzeug教程

时间:2022-07-09 12:42:16

http://chaoxz2005.blog.163.com/blog/static/15036542012863405266/

http://www.dajo.com.cn/a/boke/python/2013/1125/146.html

这里我们将会创建一个仿制TinyURL的应用,将URLs存储到一个redis实例。为了这个应用,我们将会使用的库包括,用于模板的Jinja 2、用于数据库层的redis和用于WSGI层的Werkzeug。

你可以使用pip安装需要的库:

[plain] view plaincopy
 
  1. $ pip install Jinja2 redis

你还需要确保在本地机器上正在运行着一个redis服务器。如果你在使用OS X,你可以使用brew来安装它:

[plain] view plaincopy
 
  1. $ brew install redis

如果你使用ubuntu或debian,你可以使用apt-get:

[plain] view plaincopy
 
  1. $ sudo apt-get install redis

redis是针对UNIX系统开发的,且从未真正地为在Windows上运行进行设计。然而为了开发目的,非官方的移植版本也可以很好地工作。你可以从github获得这些库。

[译者:可能还需要添加redis的python支持,在终端执行sudo easy_install redis即可。]

介绍Shortly

本教程中我们将会一起来使用Werkzeug创建一个简单的URL简化服务。请记住Werkzeug不是框架,而是一个可以创建你自己的框架或应用的、非常灵活的库。这里我们使用的方法只是诸多可用方法的一种。

数据存储方面,我们这里将会使用redis来代替关系型数据库,以保持其简洁性。这正是redis擅长的工作类型。

最终结果会如下图这样:

Werkzeug教程

第0步:WSGI基础介绍

Werkzeug是一个WSGI功能库。WSGI本身是一个用来确保你的web应用能够与webserver进行对话,更重要的是,确保web应用之间能够一起配合工作的协议或约定。

在没有Werkzeug帮助下,用WSGI实现的一个基本“Hello World”应用看起来是这样的:

[python] view plaincopy
 
  1. def application(environ, start_response):
  2. start_response(‘200 OK’, [(‘Content-Type’, ‘text/plain’)])
  3. return [‘Hello World!’]

WSGI应用是你可以调用、传递一个environ字典和一个start_response函数的东西。environ包含所有的传入信息,start_response函数可以用来指示response的开始。使用Werkzeug之后,你将不再需要直接处理被提交上来的请求(request)和应答(response)对象。

请求数据获取environ对象,并允许你以一种良好的方式访问environ中的数据。response对象本身也是一个WSGI应用,提供了很多友好的创建response的方法。

下面的代码演示了如何编写带有response对象的应用:

[python] view plaincopy
 
  1. from werkzeug.wrappers import Response
  2. def application(environ, start_response):
  3. response = Response(‘Hello World!’, mimetype=‘text/plain’)
  4. return response(environ, start_response)

下面是一个可以查看URL中查询字符串的扩展版本(不同之处在于,它查找URL中的name参数的值,并替换单词”World”):

[python] view plaincopy
 
  1. from werkzeug.wrappers import Request, Response
  2. def application(environ, start_response):
  3. request = Request(environ)
  4. text = ‘Hello %s!’ % request.args.get(‘name’, ‘World’)
  5. response = Response(text, mimetype=‘text/plain’)
  6. return response(environ, start_response)

以上就是所有关于WSGI的你需知的内容。

第1步:创建文件夹

在开始之前,先创建本应用需要的目录结构:

[plain] view plaincopy
 
  1. /shortly
  2. /static
  3. /templates

shortly文件夹不是一个python包,而仅仅用于放置我们的文件。在这个文件夹中,我们将会使用接下来的步骤直接放置我们的主模块。在static中的文件允许此应用的用户通过HTTP访问,这是存放css和javascript文件的地方。在templates中我们将会创建Jinja2版本的模板代码,在将来教程中创建的模板会保存在此目录之中。

第2步:基本架构

下面直入主题,创建我们应用的一个模块。先在shortly中创建shortly.py文件。在开始时需要一系列导入语句,我会在这里加入所有的import,甚至包括那些当前没有立即使用的,这样可以使得代码更加清晰。

[python] view plaincopy
 
  1. import os
  2. import redis
  3. import urlparse
  4. from werkzeug.wrappers import Request, Response
  5. from werkzeug.routing import Map, Rule
  6. from werkzeug.exceptions import HTTPException, NotFound
  7. from werkzeug.wsgi import SharedDataMiddleware
  8. from werkzeug.utils import redirect
  9. from jinja2 import Environment, FileSystemLoader

接下来可以创建我们应用的基本架构,以及一个创建它的新实例的函数。我们也可以选择带有一个WSGI中间件,并在web上导出所有static中的文件。

[python] view plaincopy
 
  1. class Shortly(object):
  2. def __init__(self, config):
  3. self.redis = redis.Redis(config[‘redis_host’], config[‘redis_port’])
  4. def dispatch_request(self, request):
  5. return Response(‘Hello World!’)
  6. def wsgi_app(self, environ, start_response):
  7. request =  Request(environ)
  8. response = self.dispatch_request(request)
  9. return response(environ, start_response)
  10. def __call__(self, environ, start_response):
  11. return self.wsgi_app(environ, start_response)
  12. def create_app(redis_host=‘localhost’, redis_port=6379, with_static=True):
  13. app = Shortly({
  14. ‘redis_host’:   redis_host,
  15. ‘redis_port’:   redis_port
  16. })
  17. if with_static:
  18. app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
  19. ‘/static’:  os.path.join(os.path.dirname(__file__), ‘static’)
  20. })
  21. return app

最后,我们可以添加一段启动本地开发服务器的代码,其中包括一个自动代码reloader和一个debugger:

[python] view plaincopy
 
  1. if __name__ == ‘__main__’:
  2. from werkzeug.serving import run_simple
  3. app = create_app()
  4. run_simple(‘127.0.0.1’, 5000, app, use_debugger=True, use_reloader=True)

这里的基本思想是,Shortly类是一个真实的WSGI应用。__call__函数直接分派至wsgi_app。这样便可以像在create_app函数中所作的那样,通过包装wsgi_app的方法来应用中间件。实际的wsgi_app方法接下来创建一个Request对象,并且调用dispatch_request,然后这个方法必须返回一个再次由WSGI应用评估的Response对象。正如你看到的:下面全是乌龟[译注:原文turtles all the way down,这是对由“不动的推动者”悖论提出的宇宙学中无限退化问题的一个诙谐的表述,表示循环往复的意思]。我们创建的Shortly类,以及Werkzeug中的任何请求对象,共同实现了WSGI接口。这样做的效果之一就是你甚至可以从dispatch_request方法中返回另一个WSGI应用。

create_app工厂方法可以用来创建我们的应用的一个新的实例。不仅仅会向应用传递一些如配置信息之类的参数,还可以选择增加一个导出静态文件的WSGI中间件。该方法甚至可以在我们没有设置服务器来提供static中的文件时,对这些文件进行访问。这对于开发来说是非常有帮助的。

插曲:运行应用

现在你应该能够使用python运行这个文件,并可以看到你本地机器上的一个服务:

[plain] view plaincopy
 
  1. $ python shortly.py
  2. * Running on http://127.0.0.1:5000/
  3. * Restarting with reloader: stat() polling

它还会告诉你reloader被激活。它将会一些相关技术来检查是否有哪个磁盘上的文件被修改,并自动重新开始。

现在访问URL将会看到”Hello World!”。

第3步:环境

现在已经拥有了基础的应用类,我们可以让构造函数做一些有用的工作,并在其中提供一些方便使用的帮助函数。我们需要渲染模板并连接到redis,因此下面对这个类进行一些扩展:

[python] view plaincopy
 
  1. def __init__(self, config):
  2. self.redis = redis.Redis(config[‘redis_host’], config[‘redis_port’])
  3. template_path = os.path.join(os.path.dirname(__file__), ‘templates’)
  4. self.jinja_env = Environment(loader=FileSystemLoader(template_path),
  5. autoescape=True)
  6. def render_template(self, template_name, **context):
  7. t = self.jinja_env.get_template(template_name)
  8. return Response(t.render(context), mimetype=’text/html’)

第4步:路由

接下来是路由。路由是匹配并解析URL为我们可用的形式的过程。Werkzeug提供了一个灵活的内嵌路由系统,我们可以用它完成这项工作。它工作的方式是,你创建一个Map实例,并增加一些Rule对象。每个规则包含一个用来尝试针对一个endpoint匹配URL的模式模板。endpoint通常是一个字符串,可以用来唯一识别这个URL。我们还可以用它对URL做自动反转,不过这不是我们在本教程要做的工作。

将下面的代码添加至构造函数:

[python] view plaincopy
 
  1. self.url_map = Map([
  2. Rule(‘/’, endpoint=‘new_url’),
  3. Rule(‘/<short_id>’, endpoint=‘follow_short_link’),
  4. Rule(‘/<short_id>+’, endpoint=’short_link_details’)
  5. ])

这里我们创建了一个带有三个规则的URL map。“/”表示URL空间的根,这里我们将仅仅分派一个实现了创建一个新URL的逻辑的函数。然后的一个规则连接短链接到目标URL,另一个带有同样的规则,只是在短链接之后增加了一个加号(+),将其连接到短链接的细节信息。

所以,我们如何从endpoint找到一个函数呢?这取决于你自己。我们将在此教程中使用的方法是会调用类本身的一个on_加上endpoint的函数。这里是具体的实现:

[python] view plaincopy
 
  1. def dispatch_request(self, request):
  2. adapter = self.url_map.bind_to_environ(request.environ)
  3. try:
  4. endpoint, values = adapter.match()
  5. return getattr(self, ‘on_’ + endpoint)(request, **values)
  6. except HTTPException, e:
  7. return e

我们讲URL map绑定到当前环境,并获得一个URLAdapter。该适配器可以用来匹配请求,但也可以反转URL。其匹配方法将会返回endpoint和一个URL中值的字典。例如对于follow_short_link规则来说,它拥有一个变量部分称为short_id。当我们访问http://localhost:5000/foo时,我们将会得到其伴随的值:

[plain] view plaincopy
 
  1. endpoint = ‘follow_short_link’
  2. values = {‘short_id’: u’foo’}

如果它没能匹配任何东西,则会唤起一个NotFound异常,这是一个HTTPException。所有的HTTP异常本身也都是WSGI应用,她们渲染一个默认的错误页面。因此我们仅需捕获所有这些信息,然后返回错误本身。

如果所有工作正常,我们便调用函数on_ + endpoint,并将请求作为参数传递给它,就好像所有的URL参数作为关键词参数,并返回那个函数返回的应答对象一般。

第5步:第一个视图

让我们开始第一个视图:对于新的URL的视图:

[python] view plaincopy
 
  1. def on_new_url(self, request):
  2. error = None
  3. url = ‘’
  4. if request.method = ‘POST’:
  5. url = request.form[‘url’]
  6. if not is_valid_url(url):
  7. error = ‘Please enter a valid URL’
  8. else:
  9. short_id = self.insert_url(url)
  10. return redirect(‘/%s+’ % short_id)
  11. return self.render_template(‘new_url.html’, error=error, url=url)

这里的逻辑应当非常易于理解。基本上是我们检查请求的方法是一个POST,这是我们验证URL,并在数据库中增加一个新的入口,然后重定位到细节页面。这意味着我们需要编写一个函数和一个帮助方法。对于URL验证,下面的方法就足够了:

[python] view plaincopy
 
  1. def is_valid_url(url):
  2. parts = urlparse.urlparse(url)
  3. return parts.scheme in (‘http’, ‘https’)

为了插入URL,我们所需做的就是在类中增加下面的一个小方法:

[python] view plaincopy
 
  1. def insert_url(self, url):
  2. short_id = self.redis.get(‘reverse-url:’ + url)
  3. if short_id is not None:
  4. return short_id
  5. url_num = self.redis.incr(‘last-url-id’)
  6. short_id = base36_encode(url_num)
  7. self.redis.set(‘url-target:’ + short_id, url)
  8. self.redis.set(‘reverse-url:’ + url, short_id)
  9. return short_id

reverse-url:加上URL会存储short id。如果URL已经被提交了,这肯定不会是None,而是我们需要的short id,我们便可以仅仅返回这个值。否则我们增加last-url-id键,并将其转换为基于base36的形式。然后我们存储这个链接和redis中的反转入口。下面是转换到base 36的函数:

[python] view plaincopy
 
  1. def base36_encode(number):
  2. assert number >= 0, ‘positive integer required’
  3. if number == 0:
  4. return ‘0’
  5. base36 = []
  6. while number != 0:
  7. number, i = divmod(number, 36)
  8. base36.append(‘0123456789abcdefghijklmnopqrstuvwxyz’[i])
  9. return ‘’.join(reversed(base36))

这样只要添加模板,这个视图就可以工作了。我们会在将来创建这个模板,现在先编写其他视图,然后在完成模板工作。

第6步:重定向视图

重定向视图比较简单。所有要做的工作就是在redis中查找链接后重定向。在此之外,我们还增加了一个计数器,这样就可以知道这些链接被点击的情况。

[python] view plaincopy
 
  1. def on_follow_short_link(self, request, short_id):
  2. link_target = self.redis.get(‘url-target:’ + short_id)
  3. if link_target is None:
  4. raise NotFound()
  5. self.redis.incr(‘click-count:’ +short_id)
  6. return redirect(link_target)

在URL不存在的情况下,我们会手工唤起一个NotFound异常,这将会冒泡到dispatch_request函数,并被转换为一个默认的404应答。

第7步:细节视图

链接细节视图非常类似,仅仅是对一个模板的再次渲染。在查询目标之外,我们还询问redis该链接被点击的次数,如果这个键尚不存在则默认是0:

[python] view plaincopy
 
  1. def on_short_link_details(self, request, short_id):
  2. link_target = self.redis.get(‘url-target:’ + short_id)
  3. if link_target is None:
  4. raise NotFound()
  5. click_count = int(self.redis.get(‘click-count:’ + short_id) or 0)
  6. return self.render_template(‘short_link_details.html’,
  7. link_target=link_target,
  8. short_id=short_id,
  9. click_count=click_count
  10. )

请注意redis通常使用字符串,因此你必须手动将click count转变成int。

第8步:模板

下面是所有的模板。只需将它们放置在templates文件夹。Jinja2支持模板继承,因此我们首先要做的是创建一个布局模板,在其中使用块(block)作为占位符。我们还需要设置Jinja2从而可以自动的从HTML规则中分离字符串,这样就不必在这上面花费时间。这样可以防止XSS攻击和渲染错误。

layout.html

[html] view plaincopy
 
  1. <!doctype html>
  2. <title>{% block title %}{% endblock %} | shortly</title>
  3. <link rel=stylesheet href=/static/style.css type=text/css>
  4. <div class=box>
  5. <h1><a href=/>shortly</a></h1>
  6. <p class=tagline>Shortly is a URL shortener written with Werkzeug {% block body %}{% endblock %}
  7. </div>

new_url.html

[html] view plaincopy
 
  1. {% extends "layout.html" %}
  2. {% block title %}Create New Short URL{% endblock %}
  3. {% block body %}
  4. <h2>Submit URL</h2>
  5. <form action="" method=post>
  6. {% if error %}
  7. <p class=error><string>Error:</strong> {{ error }}
  8. {% endif %}
  9. <p>URL:
  10. <input type=text name=url value="{{ url }}" class=urlinput>
  11. <input type=submit value="Shorten">
  12. </form>
  13. {% endblock %}

short_link_details.html

[html] view plaincopy
 
    1. {% extends "layout.html" %}
    2. {% block title %}Details about /{{ short_id }}{% endblock %}
    3. {% block body %}
    4. <h2><a href="/{{ short_id }}">/{{ short_id }}</a></h2>
    5. <dl>
    6. <dt>Full link
    7. <dd class=link><div>{{ link_target }}</div>
    8. <dt>Click count:
    9. <dd>{{ click_count }}
    10. </dl>
    11. {% endblock %}