Python Web 框架工具包 werkzeug

一、什麼是werkzeug 

werkzeug 官方的介紹說是一個 WSGI 工具包,不是一個web服務器,也不是一個web框架,它可以作爲一個 Web 框架的底層庫,因爲它封裝好了很多 Web 框架的東西,例如 Request,Response 等等;例如我最常用的 Flask 框架就是一 Werkzeug 爲基礎開發的。

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類還實現了pushpoptop等方法或屬性。調用這些屬性或者方法時,該類會根據當前線程或協程的標識數值,在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模塊主要對requestresponse進行封裝。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)

上面的小例子的實現步驟分析:

  1. 根據傳入web應用的environ構造請求對象request
  2. web應用構造響應對象response
  3. 調用響應對象response。調用過程中產生三個值:app_iterstatusheaders,其中statusheaders作爲參數傳遞給函數start_response用於生成響應報文首行的相關信息,而app_iter作爲響應的內容(它是一個可迭代對象)返回給WSGI網關
  4. 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、RequestResponse

終於講到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模塊的解析和匹配功能主要體現在三個類上:RuleMapMapAdapter

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和一些參數rvendpoint一般會對應一個視圖函數,返回的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 {}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章