sti320a

勉強したことのまとめ

Werkzeugのチュートリアルを3時間くらいでやってみた

WSGI準拠のサーバーライブラリ「Werkzeug」のチュートリアルをやってみました。

Werkzeug とは?

Werkzeug(ヴェルクツォイク)は、Flaskにも使われている非常にシンプルなWSGIユーティリティライブラリです。WSGI(ウィズギー)に準拠したアプリケーションを簡単に作ることができます。

公式チュートリアルは、こちらの公式サイトで読むことができます。

Werkzeugのチュートリアルの概要

Werkzeugのチュートリアルでは、URL短縮サービスを作ります。 公式チュートリアルにはTinyURLのクローンを作ると書いてあるので、これを見るとイメージしやすいでしょう。

チュートリアルを実施するにあたり、Werkzeug以外に、テンプレートエンジンに「Jinja2」を、データベースに「redis」を使います。 また、Windows環境では、redisを使うために、MicrosoftArchiveから最新版の.msiをダウンロードし、インストールする必要があります。

このチュートリアルを実施した環境

  • Python 3.6.3
  • Werkzeug 0.14.1
  • pip 18.1
  • redis 3.0.1
  • Jinja2 2.10
  • Windows10 64bit

Werkzeugチュートリアルの準備

まず、公式チュートリアルに沿って、Jinja2 redis Werkzeugのライブラリをインストールします。

pip install Jinja2 redis Werkzeug

次に、redisが動作することを確認します。 OS X なら下記コマンドでredisをインストールできます。

brew install redis

UbuntuDebianなら下記コマンドを。

sudo apt-get install redis-server

前述の通り、RedisはWindows環境で動くことを想定して作られていません。 Microsoftの開発グループがWindows向けのRedisのportを開発してくれているので、これ を利用します。

チュートリアルで完成するサービスのイメージ

最終的には、下記画像のようなサービスができあがります。 短縮したいURLを入力すると、短縮URLが発行され、そこへアクセスすると、もとのURLページに遷移できます。

Shortly
完成イメージ

WSGI(ウィズギー)イントロダクション

Werkzeugは、WSGIアプリケーションを簡単に作成できるライブラリです。

では、WSGIとは何でしょうか?WSGI(ウィズギー)とは、PythonでWebアプリケーションとWebサーバーを接続する際に考案されたインターフェース定義です。

その昔、PythonのWebフレームワークが急増したことにより、Webアプリ開発者にある不都合が生じました。

フレームワークごとに、サーバーと接続するためのインターフェースが異なっていたため、同一のアプリケーションであっても、接続できるサーバーが制限される(あるサーバーには接続できるのに、別のサーバーには接続できない)という事態が生じてしまったのです。

そのため、インターフェースを統一して、どのフレームワークでも同じように各種サーバーに接続できるようにしよう、ということで生まれたのがWSGIでした。

WSGIアプリケーションの定義として、

  • WSGIアプリケーションは、呼び出し可能なオブジェクトとして定義する。このオブジェクトが呼び出される際、第一引数に環境変数が渡され、第二引数にステータスコードとレスポンスヘッダを受け取る呼び出し可能なオブジェクトが渡される。
  • 第二引数に渡されたオブジェクトを呼び出して、ステータスコードとレスポンスヘッダ情報を渡す。
  • 戻り値として、バイト文字列をyieldするiterableなオブジェクトを返す。

といったものがあります。

Werkzeugの役割

Werkzeugは、WSGIを容易に扱えるようにしています。 Werkzeugを使わない場合、WSGIアプリケーションを自作してHello, Worldをするには、以下のようなコードを書く必要があります。

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

これは前述のWSGIアプリケーションの定義を満たしています。 一方で、これをWerkzeugを利用して書くと、以下のようになります。

from werkzeug.wrappers import Response

def application(environ, start_response):
    response = Response('Hello World!', mimetype='text/plain')
    return response(environ, start_response)

URLのクエリパラメータを受け取る場合は以下のように書けます。

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)

URL短縮サービスShortlyを作る

それでは、チュートリアルに沿って、Werkzeugを利用したアプリケーションを作っていきましょう。

ファルダ構成

まずは、Folder構成を作ります。ルートディレクトリがshortly、その直下にCSSJavaScriptなどの静的リソースを含むstaticディレクトリと、htmlテンプレートを含むtemplatesディレクトリを作ります。

/shortly 
 - / static
 - / templates

アプリケーションの概要

shortly直下にshortly.pyを作成し、基本的な構造を実装します。 まず、必要なモジュールをimportします。

公式チュートリアルでは、Python2の実装になっていますが、 今回の環境ではPython3を使用しています。 urlparseはPython3では、urllib.parseにリネームされているため、 urllib.parseからurlparseをimportしています。

import os
import redis
# urlparseはurllib.parseからimportする 
from urllib.parse 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

次に、Shortlyクラスを作成します。 さらに、create_appでShortlyのインスタンスを生成します。

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
    })

    # SharedDataMiddlewareでstaticディレクトリ内のコンテンツを返せるようにする。
    if with_static:
        app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
            '/static':  os.path.join(os.path.dirname(__file__), 'static')
        })
    return app

最後に、create_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.pyを実行し、http://127.0.0.1:5000にアクセスすると、Hello World!が返ってくるはずです。 ここまでが全体の構成です。これを編集していき、Shortlyを完成させます。

templatesのレンダリング/redisとの接続

templatesのレンダリングおよび、redisとの接続ができるようにShortlyクラスを編集します。

def __init__(self, config):
    self.redis = redis.Redis(config['redis_host'], config['redis_port'])
    # templatesのパスを取得    
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    # Jinja2にtemplatesのパスを教えてあげる
    self.jinja_env = Environment(loader=FileSystemLoader(template_path), autoescape=True)</b>

# 追加
def render_template(self, template_name, **context):
    # __init__で指定したtemplatesのディレクトリから、引数template_nameにあたるファイルのパスを取得する   
    t = self.jinja_env.get_template(template_name)
    # テンプレートをレンダリングして返す    
    return Response(t.render(context), mimetype='text/html')

ルーティング

ルーティングのために下記のコードをコンストラクタ(__init__)に追加します。 Mapのインスタンスを生成し、Ruleオブジェクトを書いていきます。

Ruleオブジェクトの第一引数にURLを、第二引数にエンドポイントを書きます。

self.url_map = Map([
    Rule('/', endpoint='new_url'),   # ルートへのアクセスをエンドポイントnew_urlに結び付ける
    Rule('/<short_id>', endpoint='follow_short_link'), # /<short_id>へのアクセスをfollow_short_linkに
    Rule('/<short_id>+', endpoint='short_link_details') # /<short_id>+ へのアクセスをshot_link_detailsに結び付ける
])

URLとエンドポイントを対応づけたら、 それらのエンドポイントと、実行したい関数を対応させる必要があります。

チュートリアルでは、on_ + endpointという形式でShortlyクラスのメソッドを呼び出すようにします。 そのためにdispatch_requestメソッドを下記のように書き換えます。

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 as e:
        return e

URL Maprequestenvironmentをバインドして、URLAdapterを作ります。 URLAdaptermatchメソッドがエンドポイントと、URLに含まれるパラメータと値の辞書を返します。

先ほどのURL Mapで、follow_short_link<short_id>という変数を受けとります。 たとえば、http://localhost:5000/fooというURLにアクセスしたときに、このメソッドは、以下のような値を返します。

endpoint = 'follow_short_link'
values = {'short_id': u'foo'}

リクエストにマッチするものがなければ、NotFoundExceptionを返します。 NotFoundExceptionHTTPExceptionの一種であるので、HTTPExceptionの例外を返します。

以上が正しく動けば、on_ + endpointの関数を呼び出せるはずです。

トップページのView

ルーティングができたら、Viewを作ります。

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.decode(encoding="utf-8"))  #Python3のためデコードが必要
    return self.render_template('new_url.html', error=error, url=url)

Shortlyクラスにon_new_urlメソッドを追加します。 前述のルーティングで /<short_id>へのアクセスをエンドポイント follow_short_linkに対応付けて、 on_new_url メソッドが実行されるようにしました。

on_new_urlメソッドは、リクエストがPOSTであることの確認と、URLが有効かどうかを検証します。 有効であれば、short_idをredisに保存して、詳細ページにリダイレクトします。

URLが無効なであったり、リクエストがPOSTでない場合は、エラーメッセージを返します。

URLが有効かを判定するため、is_valid_url関数を(Shortlyクラスの外に)作成します。 単に、urlparseでurlスキームがhttphttpsのどちらかであることを検証して返すだけです。

def is_valid_url(url):
    parts = urlparse(url)  # 注意: urlparse.urlparseではない
    return parts.scheme in ('http', 'https')

次に、redisにURLとshort_idの組を保存するメソッドを作ります(Shortlyクラス内)。

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

redisに'reverse-url:' + urlに該当するkeyがあるかを調べて、既にある場合には、そのvalue(=short_id)を返します。 存在しない場合には、redis.incr('last-url-id')で新しいidを取得し、この後作成するbase36_encodeを通して、新しいshort_idを発行します。

そして、

{key, value} = {'url-target-'+short_id,   url}
{key, value} = {'reverse-url-'+url   short_id}

という形で、shor_idとURLを対応付けて保存します。 Shortlyクラスの外で、base36_encode関数を作成します。

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))

ここまできたら完成は目前です。

短縮URLにアクセスされたときのView

短縮URLへのアクセスを、もとの(短縮前の)URLにリダイレクトするためのViewを作ります。

URL Mapで/<short_id>へのリクエストをエンドポイントfollow_short_linkに渡すようにしていました。 そして、dispatch_requestメソッドにより、エンドポイントfollow_short_linkへアクセスがきたときに、on_follow_short_linkメソッドが実行されます。

on_follow_short_linkメソッドを作成し、このメソッドが短縮前のオリジナルのURLへリダイレクト処理を行うようにしましょう。 下記のメソッドをShortlyクラスに追加します。

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)

このメソッドの中身は単純です。 リクエストされたshort_idをもとに、redisに該当するURLがあるかを探して、 存在しなければNotFound、存在すればオリジナルのURLにリダイレクトします。

短縮URL発効後の詳細画面のView

最後に、短縮URL発効後の詳細画面(完了画面)のViewを実装します。 これまでに作成したトップページのViewや短縮URLへのリダイレクトViewとやっていることは同じです。

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
    )

これでViewやルーティングなどのロジックの実装は完成です。 htmlとcssを書いてサービスを完成させましょう。

テンプレート

下記3つのhtmlファイルを作成し、最初に作成したtemplatesディレクトリに保存します。 今回は説明しませんが、{{}}{%%}等は、テンプレートエンジンJinja2の記法です。

new_url.htmlshort_link_details.htmlに共通するテンプレートをlayout.htmlに定義し、 各ファイルでこれを呼び出しています。

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; }

最後に、http://127.0.0.1:5000にアクセスして、URL短縮サービスShortlyが動くことを確認しましょう。

まとめ

今回は、Werkzuegのチュートリアルを紹介しました。 requestやresponseオブジェクトを簡単に利用することができたり、 ブラウザでインタラクティブなデバッガーが使えたり、いろいろ便利です。

自分でFlaskやDjangoのようなフレームワークを作成する際に重宝しそうです。