Werkzeugチュートリアル—Werkzeugドキュメント

提供:Dev Guides
Werkzeug/docs/2.0.x/tutorial
移動先:案内検索

Werkzeugチュートリアル

Werkzeugチュートリアルへようこそ。このチュートリアルでは、URLをredisインスタンスに格納する TinyURL クローンを作成します。 このアプリケーションに使用するライブラリは、テンプレート用の 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はフレームワークではなく、独自のフレームワークまたはアプリケーションを作成するためのユーティリティを備えたライブラリであるため、非常に柔軟性があることに注意してください。 ここで使用するアプローチは、使用できる多くのアプローチの1つにすぎません。

データストアとして、ここではリレーショナルデータベースの代わりに redis を使用して、これを単純に保ちます。これは、 redis が得意とする種類の仕事だからです。

最終結果は次のようになります。

a screenshot of shortly

ステップ0:基本的なWSGIの概要

Werkzeugは、WSGI用のユーティリティライブラリです。 WSGI自体は、WebアプリケーションがWebサーバーと通信できること、さらに重要なことにWebアプリケーションが適切に連携することを保証するプロトコルまたは規則です。

Werkzeugの助けを借りないWSGIの基本的な「HelloWorld」アプリケーションは次のようになります。

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Hello World!']

WSGIアプリケーションは、environdictとstart_response呼び出し可能オブジェクトを呼び出して渡すことができるものです。 環境にはすべての着信情報が含まれています。start_response関数を使用して、応答の開始を示すことができます。 Werkzeugを使用すると、要求オブジェクトと応答オブジェクトが提供されているため、直接処理する必要はありません。

リクエストデータは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内のクエリ文字列を調べる拡張バージョンです(さらに重要なのは、URL内の name パラメーターを調べて、別の単語を「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

shortlyフォルダーはPythonパッケージではなく、ファイルをドロップする場所です。 このフォルダに直接、メインモジュールを次の手順で配置します。 静的フォルダー内のファイルは、HTTP経由でアプリケーションのユーザーが利用できます。 これはCSSとJavaScriptファイルが行く場所です。 テンプレートフォルダ内で、Jinja2にテンプレートを検索させます。 チュートリアルの後半で作成するテンプレートは、このディレクトリに配置されます。


ステップ2:基本構造

それでは、それを理解して、アプリケーション用のモジュールを作成しましょう。 shortly フォルダに shortly.py というファイルを作成しましょう。 最初はたくさんの輸入品が必要になります。 混乱しないように、すぐに使用されなくても、ここにすべてのインポートを取り込みます。

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

次に、アプリケーションの基本構造とその新しいインスタンスを作成する関数を作成できます。オプションで、Web上の static フォルダーにあるすべてのファイルをエクスポートするWSGIミドルウェアを使用します。

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に直接ディスパッチします。 これは、[X83X] をラップして、create_app関数の場合と同じようにミドルウェアを適用できるようにするためです。 次に、実際のwsgi_appメソッドはRequestオブジェクトを作成し、dispatch_requestメソッドを呼び出します。このメソッドは、Responseオブジェクトを返す必要があり、WSGIアプリケーションとして再度評価されます。 。 ご覧のとおり、カメはずっと下にいます。 作成したShortlyクラスと、Werkzeugのリクエストオブジェクトの両方がWSGIインターフェイスを実装しています。 その結果、dispatch_requestメソッドから別のWSGIアプリケーションを返すこともできます。

create_appファクトリ関数を使用して、アプリケーションの新しいインスタンスを作成できます。 一部のパラメーターを構成としてアプリケーションに渡すだけでなく、オプションで静的ファイルをエクスポートするWSGIミドルウェアを追加します。 このようにして、サーバーを構成してファイルを提供していない場合でも、静的フォルダーからファイルにアクセスできます。これは、開発に非常に役立ちます。


Intermezzo:アプリケーションの実行

これで、 python を使用してファイルを実行し、ローカルマシン上のサーバーを確認できるようになります。

$ python shortly.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader: stat() polling

また、リローダーがアクティブであることも通知します。 さまざまな手法を使用して、ディスク上のファイルが変更されたかどうかを判断し、自動的に再起動します。

URLにアクセスすると、「HelloWorld!」が表示されます。


ステップ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オブジェクトの束を追加することです。 各ルールには、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')
])

ここでは、3つのルールを使用して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がすでに送信されている場合、これはNoneにはならず、短い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)

この場合、URLが存在しない場合、手動でNotFound例外を発生させます。これにより、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は常に文字列で機能するため、クリック数を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 %}

ステップ9:スタイル

これが醜い白黒よりも見栄えがするために、ここに沿った簡単なスタイルシートがあります:

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ページなどのいくつかの小さな改良を加えたこのチュートリアルのバージョンを確認してください。