一、什麼是werkzeug
werkzeug 官方的介紹說是一個 WSGI 工具包,不是一個web服務器,也不是一個web框架,它可以作爲一個 Web 框架的底層庫,因爲它封裝好了很多 Web 框架的東西,例如 Request,Response 等等;例如我最常用的 Flask 框架就是一 Werkzeug 爲基礎開發的。
二、Werkzeug實現密碼校驗功能
使用Werkzeug實現密碼散列,最關鍵的是使用其中security模塊的generater_password_hash()和check_password_hash()。
from werkzeug.security import generate_password_hash,check_password_hash
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)#id列
username = db.Column(db.String(64), unique=True, index=True) #username列
role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) #role_id列
password_hash = db.Column(db.String(128)) #密碼hash列
@property
def password(self): #訪問password屬性
raise AttributeError('password is not a readable attribute')
# 對保存到數據庫中的密碼進行加鹽
# 什麼是鹽?
# 通過一個隨機字符串或指定字符串與原有字符串進行混淆處理,等到一個新的字符串,這個過程稱爲加鹽處理。
@password.setter
def password(self,password): #設置password屬性
self.password_hash = generate_password_hash(password) #默認加鹽了
# 輸入密碼時進行校驗
def verify_password(self,password):#驗證密碼
return check_password_hash(self.password_hash,password)
def __repr__(self):
return '<user r="">' % self.username</user>
1、generate_password_hash(password, method=pbkdf2:sha1, slat_length=8)
method指定哈希變化的算法,默認sha1,salt_length指定‘鹽的長度’,默認是8位。password是必須指定的。
返回計算得出的128位哈希值
2、check_password_hash(hash,password)
hash是密碼散列值(一般存於數據庫),password是輸入需比較的密碼。
返回的是True或者False
三、local 模塊
1、簡介
在local
模塊中,Werkzeug實現了類似Python標準庫中threading.local
的功能。threading.local
是線程局部變量,也就是每個線程的私有變量,具有線程隔離性,可以通過線程安全的方式獲取或者改變線程中的變量。參照threading.local
,Werkzeug實現了比thread.local
更多的功能。
總結起來: 以上文檔解釋了對於“併發”問題,多線程並不是唯一的方式,在Python中還有“協程”。“協程”的一個顯著特點在於是一個線程執行,一個線程可以存在多個協程。也可以理解爲:協程會複用線程。對於
WSGI
應用來說,如果每一個線程處理一個請求,那麼thread.local
完全可以處理,但是如果每一個協程處理一個請求,那麼一個線程中就存在多個請求,用thread.local
變量處理起來會造成多個請求間數據的相互干擾。
對於上面問題,Werkzeug庫解決的辦法是local
模塊。local
模塊實現了四個類:
Local
LocalStack
LocalProxy
LocalManager
本文重點介紹前兩個類的實現。
2、Local類
Local
類能夠用來存儲線程的私有變量。在功能上這個threading.local
類似。與之不同的是,Local
類支持Python的協程。在Werkzeug庫的local模塊中,Local
類實現了一種數據結構,用來保存線程的私有變量,對於其具體形式,可以參考它的構造函數:
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
從上面類定義可以看出,Local
類具有兩個屬性:__storage__
和__ident_func__
。從構造函數來看,__storage__
是一個字典,而__ident_func__
是一個函數,用來識別當前線程或協程。
1. __ident_func__
關於當前線程或協程的識別,local
模塊引入get_ident
函數。如果支持協程,則從greenlet
庫中導入相關函數,否則從thread
庫中導入相關函數。調用get_ident
將返回一個整數,這個整數可以確定當前線程或者協程。
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
2. __storage__
__storage__
是一個字典,用來存儲不同的線程/協程,以及這些線程/協程中的變量。以下是一個簡單的多線程的例子,用來說明__storage__
的具體結構。
import threading
from werkzeug.local import Local
l = Local()
l.__storage__
def add_arg(arg, i):
l.__setattr__(arg, i)
for i in range(3):
arg = 'arg' + str(i)
t = threading.Thread(target=add_arg, args=(arg, i))
t.start()
l.__storage__
上面的例子,具體分析爲:
- 首先,代碼創建了一個
Local
的實例l
,並且訪問它的__storage__
屬性。由於目前還沒有數據,所以l.__storage__
的結果爲{}
; - 代碼創建了3個線程,每個線程均運行
add_arg(arg, i)
函數。這個函數會爲每個線程創建一個變量,並對其賦值; - 最後,再次訪問
l.__storage__
。這次,l
實例中將包含3個線程的信息。其結果爲:
{20212: {'arg0': 0}, 20404: {'arg1': 1}, 21512: {'arg2': 2}}
從以上結果可以看出,__storage__
這個字典的鍵表示不同的線程(通過get_ident
函數獲得線程標識數值),而值表示對應線程中的變量。這種結構將不同的線程分離開來。當某個線程要訪問該線程的變量時,便可以通過get_ident
函數獲得線程標識數值,進而可以在字典中獲得該鍵對應的值信息了。
3、LocalStack類
LocalStack
類和Local
類類似,但是它實現了棧數據結構。
在LocalStack
類初始化的時候,便會創建一個Local
實例,這個實例用於存儲線程/協程的變量。與此同時,LocalStack
類還實現了push
、pop
、top
等方法或屬性。調用這些屬性或者方法時,該類會根據當前線程或協程的標識數值,在Local
實例中對相應的數值進行操作。以下還是以一個多線程的例子進行說明:
from werkzeug.local import LocalStack, LocalProxy
import logging, random, threading, time
# 定義logging配置
logging.basicConfig(level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
# 生成一個LocalStack實例_stack
_stack = LocalStack()
# 定義一個RequestConetxt類,它包含一個上下文環境。
# 當調用這個類的實例時,它會將這個上下文對象放入
# _stack棧中去。當退出該上下文環境時,棧會pop其中
# 的上下文對象。
class RequestConetxt(object):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __enter__(self):
_stack.push(self)
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_tb is None:
_stack.pop()
def __repr__(self):
return '%s, %s, %s' % (self.a, self.b, self.c)
# 定義一個可供不同線程調用的方法。當不同線程調用該
# 方法時,首先會生成一個RequestConetxt實例,並在這
# 個上下文環境中先將該線程休眠一定時間,之後打印出
# 目前_stack中的信息,以及當前線程中的變量信息。
# 以上過程會循環兩次。
def worker(i):
with request_context(i):
for j in range(2):
pause = random.random()
logging.debug('Sleeping %0.02f', pause)
time.sleep(pause)
logging.debug('stack: %s' % _stack._local.__storage__.items())
logging.debug('ident_func(): %d' % _stack.__ident_func__())
logging.debug('a=%s; b=%s; c=%s' %
(LocalProxy(lambda: _stack.top.a),
LocalProxy(lambda: _stack.top.b),
LocalProxy(lambda: _stack.top.c))
)
logging.debug('Done')
# 調用該函數生成一個RequestConetxt對象
def request_context(i):
i = str(i+1)
return RequestConetxt('a'+i, 'b'+i, 'c'+i)
# 在程序最開始顯示_stack的最初狀態
logging.debug('Stack Initial State: %s' % _stack._local.__storage__.items())
# 產生兩個線程,分別調用worker函數
for i in range(2):
t = threading.Thread(target=worker, args=(i,))
t.start()
main_thread = threading.currentThread()
for t in threading.enumerate():
if t is not main_thread:
t.join()
# 在程序最後顯示_stack的最終狀態
logging.debug('Stack Finally State: %s' % _stack._local.__storage__.items())
以上例子的具體分析過程如下:
- 首先,先創建一個
LocalStack
實例_stack
,這個實例將存儲線程/協程的變量信息; - 在程序開始運行時,先檢查
_stack
中包含的信息; - 之後創建兩個線程,分別執行
worker
函數; worker
函數首先會產生一個上下文對象,這個上下文對象會放入_stack
中。在這個上下文環境中,程序執行一些操作,打印一些數據。當退出上下文環境時,_stack
會pop該上下文對象。- 在程序結束時,再次檢查
_stack
中包含的信息。
運行上面的測試例子,產生結果如下:
(MainThread) Stack Initial State: []
(Thread-1 ) Sleeping 0.31
(Thread-2 ) Sleeping 0.02
(Thread-2 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})]
(Thread-2 ) ident_func(): 13232
(Thread-2 ) a=a2; b=b2; c=c2
(Thread-2 ) Sleeping 0.49
(Thread-1 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})]
(Thread-1 ) ident_func(): 880
(Thread-1 ) a=a1; b=b1; c=c1
(Thread-1 ) Sleeping 0.27
(Thread-2 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})]
(Thread-2 ) ident_func(): 13232
(Thread-2 ) a=a2; b=b2; c=c2
(Thread-2 ) Done
(Thread-1 ) stack: [(880, {'stack': [a1, b1, c1]})]
(Thread-1 ) ident_func(): 880
(Thread-1 ) a=a1; b=b1; c=c1
(Thread-1 ) Done
(MainThread) Stack Finally State: []
注意:
- 當兩個線程在運行時,
_stack
中會存儲這兩個線程的信息,每個線程的信息都保存在類似{'stack': [a1, b1, c1]}
的結構中(注:stack鍵對應的是放入該棧中的對象,此處爲了方便打印了該對象的一些屬性)。 - 當線程在休眠和運行中切換時,通過線程的標識數值進行區分不同線程,線程1運行時它通過標識數值只會對屬於該線程的數值進行操作,而不會和線程2的數值混淆,這樣便起到線程隔離的效果(而不是通過鎖的方式)。
- 由於是在一個上下文環境中運行,當線程執行完畢時,
_stack
會將該線程存儲的信息刪除掉。在上面的運行結果中可以看出,當線程2運行結束後,_stack
中只包含線程1的相關信息。當所有線程都運行結束,_stack
的最終狀態將爲空。
四、wrappers模塊
1、簡介
Werkzeug庫中的wrappers
模塊主要對request
和response
進行封裝。request
包含了客戶端發往服務器的所有請求信息,response
包含了web應用返回給客戶端的所有信息。wrappers
模塊對請求和響應的封裝簡化了客戶端、服務器和web應用通信的流程。本文主要介紹wrappers
模塊中重要的類。
2、BaseRequest
BaseRequest
是一個非常基礎的請求類,它可以和其他的“混合”類結合在一起構建複雜的請求類。只要傳遞一個環境變量environ
(由WSGI
服務器根據請求產生),便可以構造一個BaseRequest
實例。其構造函數如下:
def __init__(self, environ, populate_request=True, shallow=False):
self.environ = environ
if populate_request and not shallow:
self.environ['werkzeug.request'] = self
self.shallow = shallow
初始化後,形成的實例request
便具有了一些屬性可以訪問,這些屬性只能以“只讀”的方式訪問。例如:
- url_charset
- want_form_data_parsed
- stream
- args
- data
- form
- values
- files
- cookies
- headers
- path
- full_path
- script_root
- url
- base_url
- url_root
- host_url
- host
- access_route
- remote_addr
BaseRequest
中還有兩個類方法比較常用:
from_values(cls, *args, kwargs)**
@classmethod
def from_values(cls, *args, **kwargs):
"""Create a new request object based on the values provided. If
environ is given missing values are filled from there. This method is
useful for small scripts when you need to simulate a request from an URL.
Do not use this method for unittesting, there is a full featured client
object (:class:`Client`) that allows to create multipart requests,
support for cookies etc.
This accepts the same options as the
:class:`~werkzeug.test.EnvironBuilder`.
.. versionchanged:: 0.5
This method now accepts the same arguments as
:class:`~werkzeug.test.EnvironBuilder`. Because of this the
`environ` parameter is now called `environ_overrides`.
:return: request object
"""
from werkzeug.test import EnvironBuilder
charset = kwargs.pop('charset', cls.charset)
kwargs['charset'] = charset
builder = EnvironBuilder(*args, **kwargs)
try:
return builder.get_request(cls)
finally:
builder.close()
這個類方法可以根據提供的參數構建一個請求。
application(cls, f)
@classmethod
def application(cls, f):
"""Decorate a function as responder that accepts the request as first
argument. This works like the :func:`responder` decorator but the
function is passed the request object as first argument and the
request object will be closed automatically::
@Request.application
def my_wsgi_app(request):
return Response('Hello World!')
:param f: the WSGI callable to decorate
:return: a new WSGI callable
"""
#: return a callable that wraps the -2nd argument with the request
#: and calls the function with all the arguments up to that one and
#: the request. The return value is then called with the latest
#: two arguments. This makes it possible to use this decorator for
#: both methods and standalone WSGI functions.
def application(*args):
request = cls(args[-2])
with request:
return f(*args[:-2] + (request,))(*args[-2:])
return update_wrapper(application, f)
這個類方法是一個裝飾器,可以用來裝飾WSGI
可調用對象或函數。
以上屬性和方法的具體用法可以參考 Request——werkzeug文檔。
3、BaseResponse
BaseResponse
類是一個響應類,用它可以封裝一個response
對象。response
對象最大的特點是它是一個WSGI
應用。
在之前介紹WSGI
規範的文章中曾介紹過Web服務器網關
,它簡化了服務器和web應用之間的通信過程,它要求服務器和web應用要遵循WSGI
規範進行開發。對於web應用而言,應用應該實現一個函數或者一個可調用對象,這樣WSGI
服務器可以通過調用myWebApp(environ, start_response)
從web應用獲得響應內容。
response
響應對象就是這樣一個WSGI
應用對象。在其實現過程中有一個__call__
方法,可以實現對一個response
對象的調用。代碼如下:
def __call__(self, environ, start_response):
"""Process this response as WSGI application.
:param environ: the WSGI environment.
:param start_response: the response callable provided by the WSGI
server.
:return: an application iterator
"""
app_iter, status, headers = self.get_wsgi_response(environ)
start_response(status, headers)
return app_iter
這樣,我們就可以很清楚地理解WSGI
應用的實現過程。下面是一個非常簡單的WSGI
應用。
from werkzeug.wrappers import Request, Response
def application(environ, start_response):
request = Request(environ)
response = Response("Hello %s!" % request.args.get('name', 'World!'))
return response(environ, start_response)
上面的小例子的實現步驟分析:
- 根據傳入web應用的
environ
構造請求對象request
; - web應用構造響應對象
response
; - 調用響應對象
response
。調用過程中產生三個值:app_iter
、status
、headers
,其中status
和headers
作爲參數傳遞給函數start_response
用於生成響應報文首行的相關信息,而app_iter
作爲響應的內容(它是一個可迭代對象)返回給WSGI網關
; WSGI網關
將返回的信息組成響應首行、響應首部、響應主體等,形成響應報文發回給客戶端。
BaseResponse
類中還有一些屬性和方法,以下屬性和方法的具體用法可以參考Response——werkzeug文檔。
- 屬性
- status_code
- status
- data
- is_stream
- is_sequence
- ······
- 方法
- call_on_close(func)
- close()
- freeze()
- force_type() 類方法
- from_app() 類方法
- set_data()
- get_data()
_ensure_sequence()
- make_sequence()
- iter_encoded()
- calculate_content_length()
- set_cookie()
- delete_cookie()
- get_wsgi_headers(environ)
- get_app_iter(environ)
- get_wsgi_response(environ)
__call__(environ, start_response)
- ······
4、Mixin類
BaseRequest
類和BaseResponse
類是請求和響應最基礎的類。wrappers
模塊中還提供了一些Mixin
類,用於擴展請求類和響應類。
有關請求類的Mixin
類主要有:
AcceptMixin
類 ——請求報文中關於客戶端希望接收的數據類型的類。ETagRequestMixin
類 ——請求報文中關於Etag和Cache的類。UserAgentMixin
類 ——請求報文中關於user_agent的類。AuthorizationMixin
類 ——請求報文中關於認證的類。CommonRequestDescriptorsMixin
類 ——通過這個類可以獲取請求首部中的相關信息。
有關響應類的Mixin
類主要有:
ETagResponseMixin
類 ——爲響應增加Etag和Cache控制的類。ResponseStreamMixin
類 ——爲響應可迭代對象提供一個“只寫”的接口的類。CommonResponseDescriptorsMixin
類 ——通過這個類可以獲取響應首部中的相關信息。WWWAuthenticateMixin
類 ——爲響應提供認證的類。
5、Request和Response
終於講到Request
類和Response
類了。
Request
類繼承自BaseRequest
類,並且結合一些請求相關的Mixin
類,具體如下:
class Request(BaseRequest, AcceptMixin, ETagRequestMixin,
UserAgentMixin, AuthorizationMixin,
CommonRequestDescriptorsMixin)
Response
類繼承自BaseResponse
類,並且結合一些響應相關的Mixin
類,具體如下:Python
class Response(BaseResponse, ETagResponseMixin, ResponseStreamMixin,
CommonResponseDescriptorsMixin,
WWWAuthenticateMixin)
至此,可以從wrappers
模塊中引入Request
類和Response
用於構建請求對象和響應對象。
四、routing模塊
Werkzeug庫的routing
模塊的主要功能在於URL解析。對於WSGI
應用來講,不同的URL對應不同的視圖函數,routing
模塊則會對請求信息的URL進行解析並匹配,觸發URL對應的視圖函數,以此生成一個響應信息。routing
模塊的解析和匹配功能主要體現在三個類上:Rule
、Map
和MapAdapter
。
1、Rule
類
Rule
類繼承自RuleFactory
類。一個Rule
的實例代表一個URL模式,一個WSGI
應用可以處理很多不同的URL模式,這也就是說可以產生很多不同的Rule
實例。這些Rule
實例最終會作爲參數傳遞給Map
類,形成一個包含所有URL模式的對象,通過這個對象可以解析並匹配請求對應的視圖函數。
關於Rule
類有一些常用的方法:
empty()
——在實際情況中,Rule
實例會和一個Map
實例進行綁定。通過empty()
方法可以將Rule
實例和Map
實例解除綁定。get_empty_kwargs()
——在empty()
方法中調用,可以獲得之前Rule
實例的參數,以便重新構造一個Rule
實例。get_rules(map)
——這個方法是對RuleFactory
類中get_rules
方法的重寫,返回Rule
實例本身。refresh()
——當修改Rule
實例(URL規則)後可以調用該方法,以便更新Rule
實例和Map
實例的綁定關係。bind(map, rebind=False)
——將Rule
實例和一個Map
實例進行綁定,這個方法會調用complie()
方法,會給Rule
實例生成一個正則表達式。complie()
——根據Rule
實例的URL模式,生成一個正則表達式,以便後續對請求的path
進行匹配。match(path)
——將Rule
實例和給定的path
進行匹配。在調用complie()
方法生成的正則表達式將會對path
進行匹配。如果匹配,將返回這個path
中的參數,以便後續過程使用。如果不匹配,將會由其他的Rule
實例和這個path
進行匹配。
注意: 在對給定的URL進行匹配的過程中,會使用一些Converters
。關於Converters
的信息後續加以介紹。
2、Map
類
通過Map
類構造的實例可以存儲所有的URL規則,這些規則是Rule
類的實例。Map
實例可以 通過後續的調用和給定的URL進行匹配。
關於Map
類有一些常用的方法:
add(rulefactory)
——這個方法在構造Map
實例的時候就會調用,它會將所有傳入Map
類中的Rule
實例和該Map
實例建立綁定關係。該方法還會調用Rule
實例的bind
方法。bind
方法 ——這個方法會生成一個MapAdapter
實例,傳入MapAdapter
的包括一些請求信息,這樣可以調用MapAdapter
實例的方法匹配給定URL。bind_to_environ
方法 ——通過解析請求中的environ
信息,然後調用上面的bind
方法,最終會生成一個MapAdapter
實例。
3、MapAdapter
類
MapAdapter
類執行URL匹配的具體工作。關於MapAdapter
類有一些常用的方法:
dispatch
方法 ——該方法首先會調用MapAdapter
實例的match()
方法,如果有匹配的Rule
,則會執行該Rule
對應的視圖函數。match
方法 ——該方法將會進行具體的URL匹配工作。它會將請求中的url和MapAdapter
實例中的所有Rule
進行匹配,如果有匹配成功的,則返回該Rule
對應的endpoint
和一些參數rv
。endpoint
一般會對應一個視圖函數,返回的rv
可以作爲參數傳入視圖函數中。
4、示例
爲了說明routing
模塊的工作原理,這裏使用Werkzeug
文檔中的一個例子,稍加改動後如下所示:
from werkzeug.routing import Map, Rule, NotFound, RequestRedirect, HTTPException
url_map = Map([
Rule('/', endpoint='blog/index'),
Rule('/<int:year>/', endpoint='blog/archive'),
Rule('/<int:year>/<int:month>/', endpoint='blog/archive'),
Rule('/<int:year>/<int:month>/<int:day>/', endpoint='blog/archive'),
Rule('/<int:year>/<int:month>/<int:day>/<slug>',
endpoint='blog/show_post'),
Rule('/about', endpoint='blog/about_me'),
Rule('/feeds/', endpoint='blog/feeds'),
Rule('/feeds/<feed_name>.rss', endpoint='blog/show_feed')
])
def application(environ, start_response):
urls = url_map.bind_to_environ(environ)
try:
endpoint, args = urls.match()
except HTTPException, e:
return e(environ, start_response)
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Rule points to %r with arguments %r' % (endpoint, args)]
if __name__ == '__main__':
from werkzeug.serving import run_simple
run_simple('localhost', 4000, application)
這裏我們使用werkzeug
自帶的服務器模塊構造了一個Web服務器,並且設計了一個簡單的WSGI
應用——application
。這個Web服務器可以根據URL的不同返回不同的結果。關於服務器的構造這裏不再贅述,以下部分簡單對URL Routing
過程進行分析:
1. 設計URL模式
設計URL模式的過程就是構造Rule
實例的過程。上面的例子中我們構造了8個Rule
實例,分別對應8個不同的URL模式。每個Rule
實例還對應一個endpoint
,這個endpoint
可以和視圖函數進行對應,以便訪問某個URL時,可以觸發與之對應的視圖函數。下面的例子展示了endpoint
和視圖函數的對應關係。
from werkzeug.wrappers import Response
from werkzeug.routing import Map, Rule
def on_index(request):
return Response('Hello from the index')
url_map = Map([Rule('/', endpoint='index')])
views = {'index': on_index}
2. 構造Map實例
構造Map實例時,會調用它的add(rulefactory)
方法。這個方法會在Map實例和各個Rule實例之間建立綁定關係,並通過調用Rule實例的bind()
方法爲每個Rule實例生成一個正則表達式。
例如,對於'/about'
這個URL,它對應的正則表達式爲:
1 |
'^\\|\\/about$' |
對於'/<int:year>/<int:month>/<int:day>/'
這個URL,它對應的正則表達式爲:
1 |
'^\\|\\/(?P<year>\\d+)\\/(?P<month>\\d+)\\/(?P<day>\\d+)(?<!/)(?P<__suffix__>/?)$' |
3. 構造MapAdapter實例
在設計WSGI
應用時,上述例子通過url_map.bind_to_environ(environ)
構建了一個MapAdapter實例。這個實例將請求的相關信息和已經創建好的Map
實例放在一起,以便進行URL匹配。
進行URL匹配的過程是通過調用MapAdapter實例的match()
方法進行的。實質上,這個方法會將請求中的path
傳入到所有Rule實例的match(path)
方法中,經過正則表達式的匹配來分析path
是否和某個Rule實例匹配。如果匹配則返回對應的endpoint
和其他的參數,這可以作爲參數傳入視圖函數。
4. 訪問URL可得相關結果
之後,訪問URL可以得到相對應的結果。
例如,訪問http://localhost:4000/2017/
,可以得到:
Rule points to 'blog/archive' with arguments {'year': 2017}
訪問http://localhost:4000/2017/3/20/
,可以得到:
Rule points to 'blog/archive' with arguments {'month': 3, 'day': 20, 'year': 2017}
訪問http://localhost:4000/about
,可以得到:
Rule points to 'blog/about_me' with arguments {}