Werkzeug教程¶
欢迎使用Werkzeug教程,我们将在其中创建 TinyURL 在Redis实例中存储URL的克隆。我们将用于此应用程序的库是 Jinja 2对于模板, redis 对于数据库层,当然,对于wsgi层是werkzeug。
你可以使用 pip 要安装所需的库,请执行以下操作:
pip install Jinja2 redis Werkzeug
还要确保在本地计算机上运行Redis服务器。如果您使用的是OS X,则可以使用 brew 安装方法:
brew install redis
如果您在Ubuntu或Debian上,可以使用apt-get::
sudo apt-get install redis-server
Redis是为Unix系统开发的,从未真正设计用于Windows。然而,出于发展的目的,非官方港口运作良好。你可以从 github .
简介¶
在本教程中,我们将一起使用Werkzeug创建一个简单的URL缩短器服务。请记住,Werkzeug不是一个框架,它是一个带有创建自己框架或应用程序的实用程序的库,因此非常灵活。我们在这里使用的方法只是您可以使用的众多方法之一。
作为数据存储,我们将使用 redis 这里不是一个关系数据库来保持这个简单,因为这是一种 redis 擅长。
最终结果如下:

第0步:WSGI基本介绍¶
Werkzeug是WSGI的实用程序库。WSGI本身是一种协议或约定,它确保Web应用程序可以与Web服务器通信,更重要的是,Web应用程序可以很好地协同工作。
在没有Werkzeug帮助的情况下,wsgi中的基本“hello world”应用程序如下:
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello World!'.encode('utf-8')]
wsgi应用程序可以调用并传递environ dict和 start_response
可调用。环境包含所有输入的信息, start_response
函数可用于指示响应的开始。使用Werkzeug,您不必直接处理请求和响应对象,因为请求和响应对象是用来处理它们的。
请求数据接受environ对象,并允许您以良好的方式从该environ访问数据。响应对象本身就是一个WSGI应用程序,它提供了一种更好的方法来创建响应。
以下是使用响应对象编写该应用程序的方法:
from werkzeug.wrappers import Response
def application(environ, start_response):
response = Response('Hello World!', mimetype='text/plain')
return response(environ, start_response)
这里是一个扩展版本,它查看URL中的查询字符串(更重要的是, name URL中的参数将“world”替换为另一个单词)::
from werkzeug.wrappers import Request, Response
def application(environ, start_response):
request = Request(environ)
text = f"Hello {request.args.get('name', 'World')}!"
response = Response(text, mimetype='text/plain')
return response(environ, start_response)
这就是你需要知道的关于WSGI的所有信息。
步骤1:创建文件夹¶
在开始之前,我们先创建此应用程序所需的文件夹:
/shortly
/static
/templates
short文件夹不是一个python包,而是我们放置文件的地方。然后,我们将在下面的步骤中将主模块直接放到这个文件夹中。静态文件夹中的文件可通过HTTP提供给应用程序的用户。这是css和javascript文件的存放位置。在模板文件夹中,我们将让Jinja2查找模板。稍后在教程中创建的模板将放在这个目录中。
第二步:基础结构¶
现在让我们直接进入它,为我们的应用程序创建一个模块。让我们创建一个名为 shortly.py 在 shortly 文件夹。一开始我们需要大量进口产品。我会把这里所有的进口货都搬进来,即使它们不是马上就用的,以防混淆:
import os
import redis
from werkzeug.urls import url_parse
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.utils import redirect
from jinja2 import Environment, FileSystemLoader
然后,我们可以为我们的应用程序创建基本结构,并创建一个函数来创建它的新实例,还可以选择使用一个wsgi中间件来导出 static 网上文件夹:
class Shortly(object):
def __init__(self, config):
self.redis = redis.Redis(
config['redis_host'], config['redis_port'], decode_responses=True
)
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
最后,我们可以添加一段代码,通过自动重新加载代码和调试程序来启动本地开发服务器:
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
.这样我们就可以包装了 wsgi_app
像我们在 create_app
功能。实际 wsgi_app
方法然后创建 Request
对象并调用 dispatch_request
方法,然后必须返回 Response
对象,然后再次将其评估为wsgi应用程序。如你所见:乌龟一直向下。两者都是 Shortly
我们创建的类以及werkzeug中的任何请求对象实现了wsgi接口。因此,您甚至可以从 dispatch_request
方法。
这个 create_app
工厂函数可用于创建应用程序的新实例。它不仅将一些参数作为配置传递给应用程序,还可以选择添加一个导出静态文件的wsgi中间件。这样,即使我们没有配置服务器来提供静态文件夹中的文件,我们也可以访问这些文件,这对开发非常有帮助。
Intermezzo:运行应用程序¶
现在,您应该能够使用 python 在本地计算机上看到一个服务器:
$ python shortly.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader: stat() polling
它还告诉您重新加载已激活。它将使用各种技术来确定磁盘上的文件是否发生了更改,然后自动重新启动。
只需访问网址,你就会看到“你好,世界!“。
第三步:环境¶
现在我们已经有了基本的应用程序类,我们可以让构造函数做一些有用的事情,并在上面提供一些有用的帮助。我们将需要能够呈现模板并连接到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
物体。每个规则都有一个模式,它将尝试根据该模式匹配URL和一个“端点”。端点通常是一个字符串,可以用来唯一地标识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映射。 /
对于url空间的根,我们将只发送到实现逻辑以创建新url的函数。然后是一个跟踪到目标URL的短链接的链接,另一个使用相同的规则但加上 (+
)最后显示链接详细信息。
那么,我们如何找到从端点到函数的方法呢?这由你决定。在本教程中,我们将通过调用 on_
+类本身的终结点。以下是工作原理:
def dispatch_request(self, request):
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return getattr(self, f'on_{endpoint}')(request, **values)
except HTTPException as e:
return e
我们将URL映射绑定到当前环境并返回 URLAdapter
.适配器可以用于匹配请求,也可以用于反向URL。match方法将返回URL中的端点和值字典。例如,规则 follow_short_link
有一个变量部分调用 short_id
.当我们去 http://localhost:5000/foo
我们将返回以下值:
endpoint = 'follow_short_link'
values = {'short_id': 'foo'}
如果它不匹配任何内容,它将提高 NotFound
例外,这是 HTTPException
.所有HTTP异常本身也是wsgi应用程序,它们呈现默认错误页。所以我们只需捕获所有的错误并返回错误本身。
如果一切正常,我们调用函数 on_
+结束并将请求作为参数以及所有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(f"/{short_id}+")
return self.render_template('new_url.html', error=error, url=url)
这种逻辑应该很容易理解。基本上,我们检查请求方法是否为post,在这种情况下,我们验证URL并向数据库添加一个新条目,然后重定向到详细页面。这意味着我们需要编写一个函数和一个助手方法。对于URL验证来说,这已经足够好了:
def is_valid_url(url):
parts = url_parse(url)
return parts.scheme in ('http', 'https')
为了插入URL,我们只需要类上的这个小方法:
def insert_url(self, url):
short_id = self.redis.get(f'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(f'url-target:{short_id}', url)
self.redis.set(f'reverse-url:{url}', short_id)
return short_id
reverse-url:
+该URL将存储短ID。如果该URL已提交,则不会是“无”,我们只需返回该值,即短ID。否则,我们将增加 last-url-id
键并将其转换为base36。然后我们将链接和反向条目存储在redis中。在这里,要转换为基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(f'url-target:{short_id}')
if link_target is None:
raise NotFound()
self.redis.incr(f'click-count:{short_id}')
return redirect(link_target)
在这种情况下,我们将提出 NotFound
如果URL不存在,则手工异常,它将冒泡到 dispatch_request
并转换为默认404响应。
步骤7:细节视图¶
链接细节视图非常类似,我们只是再次呈现一个模板。除了查找目标之外,我们还要求redis提供单击链接的次数,如果该键尚不存在,则默认为零:
def on_short_link_details(self, request, short_id):
link_target = self.redis.get(f'url-target:{short_id}')
if link_target is None:
raise NotFound()
click_count = int(self.redis.get(f'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支持模板继承,所以我们要做的第一件事就是创建一个布局模板,其中块充当占位符。我们还设置了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><strong>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 %}
第九步:风格¶
为了让它看起来比丑陋的黑白效果更好,下面是一个简单的样式表:
static/style.css :
body { background: #E8EFF0; margin: 0; padding: 0; }
body, input { font-family: 'Helvetica Neue', Arial,
sans-serif; font-weight: 300; font-size: 18px; }
.box { width: 500px; margin: 60px auto; padding: 20px;
background: white; box-shadow: 0 1px 4px #BED1D4;
border-radius: 2px; }
a { color: #11557C; }
h1, h2 { margin: 0; color: #11557C; }
h1 a { text-decoration: none; }
h2 { font-weight: normal; font-size: 24px; }
.tagline { color: #888; font-style: italic; margin: 0 0 20px 0; }
.link div { overflow: auto; font-size: 0.8em; white-space: pre;
padding: 4px 10px; margin: 5px 0; background: #E5EAF1; }
dt { font-weight: normal; }
.error { background: #E8EFF0; padding: 3px 8px; color: #11557C;
font-size: 0.9em; border-radius: 2px; }
.urlinput { width: 300px; }
奖金:改进¶
查看Werkzeug存储库中示例字典中的实现,查看本教程的一个版本,其中有一些小的改进,如自定义404页。