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 Home 64bit
Werkzeugチュートリアルの準備
まず、公式チュートリアルに沿って、Jinja2 redis Werkzeugのライブラリをインストールします。
pip install Jinja2 redis Werkzeug
次に、redisが動作することを確認します。
OS X なら下記コマンドでredisをインストールできます。
brew install redis
UbuntuやDebianなら下記コマンドを。
sudo apt-get install redis-server
前述の通り、RedisはWindows環境で動くことを想定して作られていません。
Microsoftの開発グループがWindows向けのRedisのportを開発してくれているので、これ
を利用します。
チュートリアルで完成するサービスのイメージ
最終的には、下記画像のようなサービスができあがります。
短縮したいURLを入力すると、短縮URLが発行され、そこへアクセスすると、もとのURLページに遷移できます。
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
、その直下にCSSやJavaScriptなどの静的リソースを含む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():
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 Map
とrequest
のenvironment
をバインドして、URLAdapter
を作ります。
URLAdapter
のmatch
メソッドがエンドポイントと、URLに含まれるパラメータと値の辞書を返します。
先ほどのURL Map
で、follow_short_link
は<short_id>
という変数を受けとります。
たとえば、http://localhost:5000/foo というURLにアクセスしたときに、このメソッドは、以下のような値を返します。
endpoint = 'follow_short_link'
values = {'short_id': u'foo'}
リクエストにマッチするものがなければ、NotFoundException
を返します。
NotFoundException
はHTTPException
の一種であるので、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スキームがhttp
かhttps
のどちらかであることを検証して返すだけです。
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.decode(encoding="utf-8"),
short_id=short_id,
click_count=click_count
)
これでViewやルーティングなどのロジックの実装は完成です。
htmlとcssを書いてサービスを完成させましょう。
テンプレート
下記3つのhtmlファイルを作成し、最初に作成したtemplates
ディレクトリに保存します。
今回は説明しませんが、{{}}
や{%%}
等は、テンプレートエンジンJinja2の記法です。
new_url.html
とshort_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のようなフレームワークを作成する際に重宝しそうです。