歡迎來(lái)到 Werkzeug 教程,我們將會(huì)實(shí)現(xiàn)一個(gè)類似 TinyURL 的網(wǎng)站來(lái)儲(chǔ)存 URLS。我們將會(huì)使用的庫(kù)有模板引擎 Jinja 2,數(shù)據(jù)層支持 redis ,當(dāng)然還有 WSGI 協(xié)議層 Werkzeug。
你可以使用 pip 來(lái)安裝依賴庫(kù):
pip install Jinja2 redis
同時(shí)確定你的本地開(kāi)啟一個(gè) redis 服務(wù),如果你是OS X系統(tǒng),你可以使用 brew 來(lái)安裝 redis:
brew install redis
如果你是用 Ubuntu 或 Debian, 你可以使用 apt-get:
sudo apt-get install redis
Redis 專為 UNIX 系統(tǒng)開(kāi)發(fā),并沒(méi)有考慮為 Windows 設(shè)計(jì)。但對(duì)于開(kāi)發(fā)來(lái)說(shuō),非官方的版本已經(jīng)足夠了,你可以從 github 得到它。
在這個(gè)教程中,我們將一起用 Werkzeug 創(chuàng)建一個(gè)短網(wǎng)址服務(wù)。請(qǐng)注意,Werkzeug 并不是一個(gè)框架,它是一個(gè) WSGI 工具集的庫(kù),你可以通過(guò)它來(lái)創(chuàng)建你自己的框架或 Web 應(yīng)用。Werkzeug 是非常靈活的,這篇教程用到的一些方法只是 Werkzeug 的一部分。
在數(shù)據(jù)層,為了保持簡(jiǎn)單,我們使用 redis 來(lái)代替關(guān)系型數(shù)據(jù)庫(kù),而且 redis 也擅長(zhǎng)來(lái)做這些。
最終的結(jié)果將會(huì)看起來(lái)像這樣:
Werkzeug 是一個(gè) WSGI 工具包。WSGI 是一個(gè) Web 應(yīng)用和服務(wù)器通信的協(xié)議,Web 應(yīng)用可以通過(guò) WSGI 一起工作。
一個(gè)基本的 “Hello World” WSGI 應(yīng)用看起來(lái)是這樣的:
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello World!']
用過(guò) WSGI 應(yīng)用可以和環(huán)境通信,他有一個(gè)可調(diào)用的 start_response 。環(huán)境包含了所有進(jìn)來(lái)的信息。 start_response 用來(lái)表明已經(jīng)收到一個(gè)響應(yīng)。通過(guò) Werkzeug 你可以不必直接處理請(qǐng)求或者響應(yīng)這些底層的東西,它已經(jīng)為你封裝好了這些。
請(qǐng)求數(shù)據(jù)需要環(huán)境對(duì)象,Werkzeug 允許你以一個(gè)輕松的方式訪問(wèn)數(shù)據(jù)。響應(yīng)對(duì)象是一個(gè) WSGI應(yīng)用,提供了更好的方法來(lái)創(chuàng)建響應(yīng)。
下面教你怎么用響應(yīng)對(duì)象來(lái)寫一個(gè)應(yīng)用:
from werkzeug.wrappers import Response
def application(environ, start_response):
response = Response('Hello World!', mimetype='text/plain')
return response(environ, start_response)
這里有一個(gè)在 URL 中查詢字符串的擴(kuò)展版本(重點(diǎn)是 URL 中的 name 將會(huì)替代World):
from werkzeug.wrappers import Request, Response
def applicatio n(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)
到此為止,你已經(jīng)足夠了解 WSGI 了。
在開(kāi)始之前,首先為應(yīng)用創(chuàng)建一個(gè)目錄:
/shortly
/static
/templates
這個(gè)簡(jiǎn)潔的目錄不是一個(gè)python包,他用來(lái)存放我們的項(xiàng)目文件。我們的入口模塊將會(huì)放在 /shortly目錄的根目錄下。 /static 目錄用來(lái)放置CSS、Javascript等靜態(tài)文件,用戶可以通過(guò)HTTP協(xié)議直接訪問(wèn)。 /templates 目錄用來(lái)存放 Jinja2 模板文件,接下來(lái)你為項(xiàng)目創(chuàng)建的模板文件將要放到這個(gè)文件夾內(nèi)。
現(xiàn)在我們正式開(kāi)始為我們的項(xiàng)目創(chuàng)建模塊。在 shortly 目錄創(chuàng)建 shortly.py 文件。首先來(lái)導(dǎo)入一些東西。為了防止混淆,我把所有的入口放在這,即使他們不會(huì)立即使用:
import os
import redis
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
接下來(lái)我們來(lái)為我們的應(yīng)用創(chuàng)建基本的結(jié)構(gòu),并通過(guò)一個(gè)函數(shù)來(lái)創(chuàng)建應(yīng)用實(shí)例,通過(guò) WSGI中間件輸出 static 目錄的文件:
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
})
if with_static:
app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
'/static': os.path.join(os.path.dirname(__file__), 'static')
})
return app
最后我們添加一部分代碼來(lái)開(kāi)啟一個(gè)本地服務(wù)器,自動(dòng)加載代碼并開(kāi)啟調(diào)試器:
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)
思路很簡(jiǎn)單,我們的 Shortly 是一個(gè)實(shí)際的 WSGI 應(yīng)用。 call 方法直接調(diào)用 wsgi_app 。這樣做我們可以裝飾 wsgi_app 調(diào)用中間件,就像我們?cè)?create_app函數(shù)中做的一樣。 wsgi_app 實(shí)際上創(chuàng)建了一個(gè) Request 對(duì)象,之后通過(guò)dispatch_request 調(diào)用 Request 對(duì)象然后給 WSGI 應(yīng)用返回一個(gè) Response對(duì)象。正如你看到的:無(wú)論是創(chuàng)建 Shortly 類,還是還是創(chuàng)建 Werkzeug Request 對(duì)象來(lái)執(zhí)行 WSGI 接口。最終結(jié)果只是從 dispatch_request 方法返回另一個(gè) WSGI 應(yīng)用。
create_app 可以被用于創(chuàng)建一個(gè)新的應(yīng)用實(shí)例。他不僅可以通過(guò)參數(shù)配置應(yīng)用,還可以選擇性的添加中間件來(lái)輸出靜態(tài)文件。通過(guò)這種方法我們甚至可以不配置服務(wù)器就能訪問(wèn)靜態(tài)文件,這對(duì)開(kāi)發(fā)是很有幫助的。
現(xiàn)在你應(yīng)該可以通過(guò) python 執(zhí)行這個(gè)文件了,看看你本機(jī)的服務(wù):
$ python shortly.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader: stat() polling
它告訴你自動(dòng)加載已經(jīng)開(kāi)啟,他會(huì)通過(guò)各種各樣的技術(shù)來(lái)判斷硬盤上的文件是否改變來(lái)自動(dòng)重啟。
在瀏覽器輸入這個(gè)URL,你將會(huì)看到 “Hello World!”。
現(xiàn)在我們已經(jīng)有了一個(gè)應(yīng)用的基本類,可以通過(guò)構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)一些功能。通過(guò)構(gòu)造函數(shù)我們可以渲染模板、連接redis?,F(xiàn)在讓我們擴(kuò)展這個(gè)類:
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')
下一步是路由。我們可以通過(guò)路由來(lái)匹配和解析URL。Werkzeug 提供了一個(gè)靈活的集成路由。你需要?jiǎng)?chuàng)建一個(gè) Map 實(shí)例并添加一系列 Rule對(duì)象。每個(gè) rule 將會(huì)匹配 URL 并添加一個(gè) “endpoint”。endpoint 通常是一個(gè)用于標(biāo)記URL 的字符串。此外我們還可以使用它來(lái)翻轉(zhuǎn) URL,但這不是這篇教程我們要做的。
把下列代碼放入構(gòu)造函數(shù):
self.url_map = Map([
Rule('/', endpoint='new_url'),
Rule('/<short_id>', endpoint='follow_short_link'),
Rule('/<short_id>+', endpoint='short_link_details')
])
現(xiàn)在我們創(chuàng)造了一個(gè)包含三個(gè) URL 規(guī)則的字典。第一個(gè)規(guī)則, / 是根 URL 空間,我們可以調(diào)用一個(gè)邏輯函數(shù)來(lái)創(chuàng)建一個(gè)新 URL;第二個(gè)規(guī)則,根據(jù)規(guī)則指向一個(gè)目標(biāo)URL;最后一個(gè)規(guī)則,和第二個(gè)有相同的規(guī)則,但是它在最后添加一個(gè)(+)來(lái)顯示鏈接鏈接詳細(xì)信息。
那么 endpoint 是怎么指向一個(gè)函數(shù)的?這是需要你解決的。本篇教程中是通過(guò)類中 on_+ endpoint 方法。具體如下:
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, e:
return e
我們將 RUL 綁定到目前的環(huán)境返回一個(gè) URLAdapter 。適配器可以用于匹配請(qǐng)求也可以翻轉(zhuǎn) URLS。匹配方法將會(huì)返回 endpoint 和一個(gè) URL 值字典。這個(gè)follow_short_link 路由實(shí)例有一個(gè)變量 short_id 。當(dāng)我們?cè)跒g覽器輸入 http://localhost:5000/foo我們將會(huì)得到如下的值:
endpoint = 'follow_short_link'
values = {'short_id': u'foo'}
我們沒(méi)有匹配到任何東西,他將會(huì)拋出一個(gè) NotFound 異常,實(shí)質(zhì)是一個(gè) HTTPException 異常。所有的 HTTP 異常將會(huì)跳轉(zhuǎn) WSGI 應(yīng)用渲染的默認(rèn)錯(cuò)誤頁(yè)面。所以我們只需要捕獲并返回他們。
如果一切順利,我們用 request 作為參數(shù),所有的 URL 參數(shù)做作為關(guān)鍵字參數(shù)調(diào)用 on_+ endpoint 函數(shù)可以返回響應(yīng)對(duì)象。
讓我們開(kāi)始第一個(gè)視圖: new URLs 視圖:
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)
return self.render_template('new_url.html', error=error, url=url)
思想不難理解。首先我們檢查請(qǐng)求方法是不是 POST,然后驗(yàn)證得到的 URL 并插入到數(shù)據(jù)庫(kù)中,然后跳轉(zhuǎn)到一個(gè)詳細(xì)頁(yè)面。要實(shí)現(xiàn)這個(gè),意味著我們需要在寫一個(gè)函數(shù)和一個(gè)輔助方法下面是 URL 驗(yàn)證函數(shù):
def is_valid_url(url):
parts = urlparse.urlparse(url)
return parts.scheme in ('http', 'https')
為了向數(shù)據(jù)庫(kù)插入 URL,我們只需要在類中添加以下方法:
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
reverse-url: + URL 將會(huì)存放儲(chǔ)存ID。如果 URL 已經(jīng)被提交過(guò)那么只需要返回存儲(chǔ)ID值,否則我們?cè)黾?last-url-id 鍵值并轉(zhuǎn)化為 base36,接下來(lái)我們將存儲(chǔ)連接和轉(zhuǎn)換連接存儲(chǔ)到 redis。下面就是轉(zhuǎn)化為 base 36 的函數(shù):
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))
然而我們還沒(méi)有視圖的模板,不急,我們過(guò)一會(huì)就來(lái)寫模板。不過(guò)在這之前,我們先來(lái)完成另一個(gè)視圖。
重定向視圖很簡(jiǎn)單,它只需要從 redis 找到連接并重定向跳轉(zhuǎn)到它。另外我們還想添加一個(gè)計(jì)數(shù)器以便于統(tǒng)計(jì)連接被點(diǎn)擊頻率:
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_ta rget)
在這種情況下,如果 URL 不存在,我們將會(huì)拋出一個(gè) NotFound異常,通過(guò) dispatch_request 函數(shù)返回一個(gè) 404 響應(yīng)
鏈接描述視圖也是非常相似的,我們僅僅需要再渲染一個(gè)模板。除了目標(biāo) URL,我們還需要從 redis 查詢被點(diǎn)擊次數(shù),如果在 redis 中沒(méi)有記錄,我們把它設(shè)為 0:
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
)
要知道 redis 存的是字符串,所以你需要手動(dòng)點(diǎn)擊次數(shù)轉(zhuǎn)化為 :int 。
這里就是全部的模板,僅僅把它們放到 templates 文件夾就可以了。jinja2支持模板繼承,所以我們首先要?jiǎng)?chuàng)建一個(gè) layout 模板,并用 blocks 占位。接下來(lái)設(shè)置jinja2以便于自動(dòng)用html規(guī)則轉(zhuǎn)化字符串,我們不必自己花時(shí)間來(lái)做這些。同時(shí)它可以也防止 XSS 攻擊和渲染錯(cuò)誤頁(yè)面。
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 %}
添加樣式可以使頁(yè)面比丑陋的黑色和白色看起來(lái)好一些。下面是一個(gè)簡(jiǎn)單的樣式表:
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 倉(cāng)庫(kù)的 example 目錄可以找到這篇教程代碼,那里的版本可能有一些改進(jìn),比如一個(gè)定制的 404 頁(yè)面。
更多建議: