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.pyshortly 文件夹。一开始我们需要大量进口产品。我会把这里所有的进口货都搬进来,即使它们不是马上就用的,以防混淆:

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页。