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安装需要的库:
- $ pip install Jinja2 redis
你还需要确保在本地机器上正在运行着一个redis服务器。如果你在使用OS X,你可以使用brew来安装它:
- $ brew install redis
如果你使用ubuntu或debian,你可以使用apt-get:
- $ sudo apt-get install redis
redis是针对UNIX系统开发的,且从未真正地为在Windows上运行进行设计。然而为了开发目的,非官方的移植版本也可以很好地工作。你可以从github获得这些库。
[译者:可能还需要添加redis的python支持,在终端执行sudo easy_install redis即可。]
介绍Shortly
本教程中我们将会一起来使用Werkzeug创建一个简单的URL简化服务。请记住Werkzeug不是框架,而是一个可以创建你自己的框架或应用的、非常灵活的库。这里我们使用的方法只是诸多可用方法的一种。
数据存储方面,我们这里将会使用redis来代替关系型数据库,以保持其简洁性。这正是redis擅长的工作类型。
最终结果会如下图这样:
第0步:WSGI基础介绍
Werkzeug是一个WSGI功能库。WSGI本身是一个用来确保你的web应用能够与webserver进行对话,更重要的是,确保web应用之间能够一起配合工作的协议或约定。
在没有Werkzeug帮助下,用WSGI实现的一个基本“Hello World”应用看起来是这样的:
- def application(environ, start_response):
- start_response(‘200 OK’, [(‘Content-Type’, ‘text/plain’)])
- return [‘Hello World!’]
WSGI应用是你可以调用、传递一个environ字典和一个start_response函数的东西。environ包含所有的传入信息,start_response函数可以用来指示response的开始。使用Werkzeug之后,你将不再需要直接处理被提交上来的请求(request)和应答(response)对象。
请求数据获取environ对象,并允许你以一种良好的方式访问environ中的数据。response对象本身也是一个WSGI应用,提供了很多友好的创建response的方法。
下面的代码演示了如何编写带有response对象的应用:
- from werkzeug.wrappers import Response
- def application(environ, start_response):
- response = Response(‘Hello World!’, mimetype=‘text/plain’)
- return response(environ, start_response)
下面是一个可以查看URL中查询字符串的扩展版本(不同之处在于,它查找URL中的name参数的值,并替换单词”World”):
- from werkzeug.wrappers import Request, Response
- def application(environ, start_response):
- request = Request(environ)
- text = ‘Hello %s!’ % request.args.get(‘name’, ‘World’)
- response = Response(text, mimetype=‘text/plain’)
- return response(environ, start_response)
以上就是所有关于WSGI的你需知的内容。
第1步:创建文件夹
在开始之前,先创建本应用需要的目录结构:
- /shortly
- /static
- /templates
shortly文件夹不是一个python包,而仅仅用于放置我们的文件。在这个文件夹中,我们将会使用接下来的步骤直接放置我们的主模块。在static中的文件允许此应用的用户通过HTTP访问,这是存放css和javascript文件的地方。在templates中我们将会创建Jinja2版本的模板代码,在将来教程中创建的模板会保存在此目录之中。
第2步:基本架构
下面直入主题,创建我们应用的一个模块。先在shortly中创建shortly.py文件。在开始时需要一系列导入语句,我会在这里加入所有的import,甚至包括那些当前没有立即使用的,这样可以使得代码更加清晰。
- import os
- import redis
- import urlparse
- from werkzeug.wrappers import Request, Response
- from werkzeug.routing import Map, Rule
- from werkzeug.exceptions import HTTPException, NotFound
- from werkzeug.wsgi import SharedDataMiddleware
- from werkzeug.utils import redirect
- from jinja2 import Environment, FileSystemLoader
接下来可以创建我们应用的基本架构,以及一个创建它的新实例的函数。我们也可以选择带有一个WSGI中间件,并在web上导出所有static中的文件。
- class Shortly(object):
- def __init__(self, config):
- self.redis = redis.Redis(config[‘redis_host’], config[‘redis_port’])
- def dispatch_request(self, request):
- return Response(‘Hello World!’)
- def wsgi_app(self, environ, start_response):
- request = Request(environ)
- response = self.dispatch_request(request)
- return response(environ, start_response)
- def __call__(self, environ, start_response):
- return self.wsgi_app(environ, start_response)
- def create_app(redis_host=‘localhost’, redis_port=6379, with_static=True):
- app = Shortly({
- ‘redis_host’: redis_host,
- ‘redis_port’: redis_port
- })
- if with_static:
- app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
- ‘/static’: os.path.join(os.path.dirname(__file__), ‘static’)
- })
- return app
最后,我们可以添加一段启动本地开发服务器的代码,其中包括一个自动代码reloader和一个debugger:
- if __name__ == ‘__main__’:
- from werkzeug.serving import run_simple
- app = create_app()
- 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运行这个文件,并可以看到你本地机器上的一个服务:
- $ python shortly.py
- * Running on http://127.0.0.1:5000/
- * Restarting with reloader: stat() polling
它还会告诉你reloader被激活。它将会一些相关技术来检查是否有哪个磁盘上的文件被修改,并自动重新开始。
现在访问URL将会看到”Hello World!”。
第3步:环境
现在已经拥有了基础的应用类,我们可以让构造函数做一些有用的工作,并在其中提供一些方便使用的帮助函数。我们需要渲染模板并连接到redis,因此下面对这个类进行一些扩展:
- def __init__(self, config):
- self.redis = redis.Redis(config[‘redis_host’], config[‘redis_port’])
- template_path = os.path.join(os.path.dirname(__file__), ‘templates’)
- self.jinja_env = Environment(loader=FileSystemLoader(template_path),
- autoescape=True)
- def render_template(self, template_name, **context):
- t = self.jinja_env.get_template(template_name)
- return Response(t.render(context), mimetype=’text/html’)
第4步:路由
接下来是路由。路由是匹配并解析URL为我们可用的形式的过程。Werkzeug提供了一个灵活的内嵌路由系统,我们可以用它完成这项工作。它工作的方式是,你创建一个Map实例,并增加一些Rule对象。每个规则包含一个用来尝试针对一个endpoint匹配URL的模式模板。endpoint通常是一个字符串,可以用来唯一识别这个URL。我们还可以用它对URL做自动反转,不过这不是我们在本教程要做的工作。
将下面的代码添加至构造函数:
- self.url_map = Map([
- Rule(‘/’, endpoint=‘new_url’),
- Rule(‘/<short_id>’, endpoint=‘follow_short_link’),
- Rule(‘/<short_id>+’, endpoint=’short_link_details’)
- ])
这里我们创建了一个带有三个规则的URL map。“/”表示URL空间的根,这里我们将仅仅分派一个实现了创建一个新URL的逻辑的函数。然后的一个规则连接短链接到目标URL,另一个带有同样的规则,只是在短链接之后增加了一个加号(+),将其连接到短链接的细节信息。
所以,我们如何从endpoint找到一个函数呢?这取决于你自己。我们将在此教程中使用的方法是会调用类本身的一个on_加上endpoint的函数。这里是具体的实现:
- def dispatch_request(self, request):
- adapter = self.url_map.bind_to_environ(request.environ)
- try:
- endpoint, values = adapter.match()
- return getattr(self, ‘on_’ + endpoint)(request, **values)
- except HTTPException, e:
- return e
我们讲URL map绑定到当前环境,并获得一个URLAdapter。该适配器可以用来匹配请求,但也可以反转URL。其匹配方法将会返回endpoint和一个URL中值的字典。例如对于follow_short_link规则来说,它拥有一个变量部分称为short_id。当我们访问http://localhost:5000/foo时,我们将会得到其伴随的值:
- endpoint = ‘follow_short_link’
- values = {‘short_id’: u’foo’}
如果它没能匹配任何东西,则会唤起一个NotFound异常,这是一个HTTPException。所有的HTTP异常本身也都是WSGI应用,她们渲染一个默认的错误页面。因此我们仅需捕获所有这些信息,然后返回错误本身。
如果所有工作正常,我们便调用函数on_ + endpoint,并将请求作为参数传递给它,就好像所有的URL参数作为关键词参数,并返回那个函数返回的应答对象一般。
第5步:第一个视图
让我们开始第一个视图:对于新的URL的视图:
- def on_new_url(self, request):
- error = None
- url = ‘’
- if request.method = ‘POST’:
- url = request.form[‘url’]
- if not is_valid_url(url):
- error = ‘Please enter a valid URL’
- else:
- short_id = self.insert_url(url)
- return redirect(‘/%s+’ % short_id)
- return self.render_template(‘new_url.html’, error=error, url=url)
这里的逻辑应当非常易于理解。基本上是我们检查请求的方法是一个POST,这是我们验证URL,并在数据库中增加一个新的入口,然后重定位到细节页面。这意味着我们需要编写一个函数和一个帮助方法。对于URL验证,下面的方法就足够了:
- def is_valid_url(url):
- parts = urlparse.urlparse(url)
- return parts.scheme in (‘http’, ‘https’)
为了插入URL,我们所需做的就是在类中增加下面的一个小方法:
- def insert_url(self, url):
- short_id = self.redis.get(‘reverse-url:’ + url)
- if short_id is not None:
- return short_id
- url_num = self.redis.incr(‘last-url-id’)
- short_id = base36_encode(url_num)
- self.redis.set(‘url-target:’ + short_id, url)
- self.redis.set(‘reverse-url:’ + url, short_id)
- return short_id
reverse-url:加上URL会存储short id。如果URL已经被提交了,这肯定不会是None,而是我们需要的short id,我们便可以仅仅返回这个值。否则我们增加last-url-id键,并将其转换为基于base36的形式。然后我们存储这个链接和redis中的反转入口。下面是转换到base 36的函数:
- def base36_encode(number):
- assert number >= 0, ‘positive integer required’
- if number == 0:
- return ‘0’
- base36 = []
- while number != 0:
- number, i = divmod(number, 36)
- base36.append(‘0123456789abcdefghijklmnopqrstuvwxyz’[i])
- return ‘’.join(reversed(base36))
这样只要添加模板,这个视图就可以工作了。我们会在将来创建这个模板,现在先编写其他视图,然后在完成模板工作。
第6步:重定向视图
重定向视图比较简单。所有要做的工作就是在redis中查找链接后重定向。在此之外,我们还增加了一个计数器,这样就可以知道这些链接被点击的情况。
- def on_follow_short_link(self, request, short_id):
- link_target = self.redis.get(‘url-target:’ + short_id)
- if link_target is None:
- raise NotFound()
- self.redis.incr(‘click-count:’ +short_id)
- return redirect(link_target)
在URL不存在的情况下,我们会手工唤起一个NotFound异常,这将会冒泡到dispatch_request函数,并被转换为一个默认的404应答。
第7步:细节视图
链接细节视图非常类似,仅仅是对一个模板的再次渲染。在查询目标之外,我们还询问redis该链接被点击的次数,如果这个键尚不存在则默认是0:
- def on_short_link_details(self, request, short_id):
- link_target = self.redis.get(‘url-target:’ + short_id)
- if link_target is None:
- raise NotFound()
- click_count = int(self.redis.get(‘click-count:’ + short_id) or 0)
- return self.render_template(‘short_link_details.html’,
- link_target=link_target,
- short_id=short_id,
- click_count=click_count
- )
请注意redis通常使用字符串,因此你必须手动将click count转变成int。
第8步:模板
下面是所有的模板。只需将它们放置在templates文件夹。Jinja2支持模板继承,因此我们首先要做的是创建一个布局模板,在其中使用块(block)作为占位符。我们还需要设置Jinja2从而可以自动的从HTML规则中分离字符串,这样就不必在这上面花费时间。这样可以防止XSS攻击和渲染错误。
layout.html
- <!doctype html>
- <title>{% block title %}{% endblock %} | shortly</title>
- <link rel=stylesheet href=/static/style.css type=text/css>
- <div class=box>
- <h1><a href=/>shortly</a></h1>
- <p class=tagline>Shortly is a URL shortener written with Werkzeug {% block body %}{% endblock %}
- </div>
new_url.html
- {% extends "layout.html" %}
- {% block title %}Create New Short URL{% endblock %}
- {% block body %}
- <h2>Submit URL</h2>
- <form action="" method=post>
- {% if error %}
- <p class=error><string>Error:</strong> {{ error }}
- {% endif %}
- <p>URL:
- <input type=text name=url value="{{ url }}" class=urlinput>
- <input type=submit value="Shorten">
- </form>
- {% endblock %}
short_link_details.html
- {% extends "layout.html" %}
- {% block title %}Details about /{{ short_id }}{% endblock %}
- {% block body %}
- <h2><a href="/{{ short_id }}">/{{ short_id }}</a></h2>
- <dl>
- <dt>Full link
- <dd class=link><div>{{ link_target }}</div>
- <dt>Click count:
- <dd>{{ click_count }}
- </dl>
- {% endblock %}