本篇講解動態 url 和轉換器的用法及原理。
動態 url 實現原理
動態 url 由 werkzeug 通過轉換器 (converter) 來實現,爲說明動態 url 的使用方法,我們先從簡單的示例開始,逐步展開。假設我們要編寫一個向手機號碼發送短信的 Flask 程序,有如下一段代碼:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Index page'
@app.route('/message/<mobile_number>')
def send_message(mobile_number):
return 'Message was sent to {}'.format(mobile_number)
if __name__ == '__main__':
app.run()
由於 Flask 對路由過程作了較抽象的封裝,並不容易看出完整的過程,爲了便於理解,我們直接用下面的一段代碼來說明在 werkzeurg 中路由綁定 (bind) 和匹配 (match) 的過程。
from werkzeug.routing import Rule, Map
from werkzeug.serving import run_simple
from werkzeug.exceptions import HTTPException
rules = [
Rule('/', endpoint='index'),
Rule('/message/<mobile_number>', endpoint='mobile')
]
url_map = Map(rules)
def application(environ, start_response):
urls = url_map.bind_to_environ(environ)
try:
endpoint, args = urls.match()
except HTTPException as ex:
return ex(environ, start_response)
headers = [('Content-Type', 'text/plain')]
start_response('200 OK', headers)
body = 'Rule points to {} with arguments {}' \
.format(endpoint, args).encode('utf-8')
return [body]
if __name__ == "__main__":
run_simple('localhost', 5000, application)
這段代碼能架起一個簡單的 Web 服務器。當客戶端從 /messages/mobile_number 發起 GET 請求,程序返回如下信息:
Rule points to mobile with arguments {'mobile_number': '13811112222'}
這段代碼展示了 werkzueg 路由過程的三大階段:
創建 Rule 和 Map 的實例上篇已經講過。本篇從第二步開始講解。
綁定到特定環境
bind_to_environ()
方法在內部調用了 Map.bind()
方法, Map.bind()
方法創建 MapAdapter
的實例。MapAdapter
類負責 URL 匹配的工作。
以下是關鍵代碼及說明。
bind_to_environ()
方法在內部調用了Map.bind()
方法:
def bind_to_environ(self, environ, server_name=None, subdomain=None):
# 其他代碼略
return Map.bind(
self,
server_name,
script_name,
subdomain,
environ["wsgi.url_scheme"],
environ["REQUEST_METHOD"],
path_info,
query_args=query_args,
)
Map.bind()
方法創建MapAdapter
的實例:
def bind(
self,
server_name,
script_name=None,
subdomain=None,
url_scheme="http",
default_method="GET",
path_info=None,
query_args=None,
):
# 其他代碼略
return MapAdapter(
self,
server_name,
script_name,
subdomain,
url_scheme,
path_info,
default_method,
query_args,
)
MapAdapter.match() 方法
該方法的作用是,傳入 path_info 和 method,返回 tuple 類型包括 endpoint + arguments 的信息或者 rule + arguments 信息 (You get a tuple in the form (endpoint, arguments)
if there is a match (unless return_rule
is True, in which case you get a tuple in the form (rule, arguments)
))。該方法內部調用 Rule.match()
方法:
def match(self, path_info=None, method=None, return_rule=False, query_args=None):
self.map.update()
if path_info is None:
path_info = self.path_info
else:
path_info = to_unicode(path_info, self.map.charset)
if query_args is None:
query_args = self.query_args
method = (method or self.default_method).upper()
path = u"%s|%s" % (
self.map.host_matching and self.server_name or self.subdomain,
path_info and "/%s" % path_info.lstrip("/"),
)
have_match_for = set()
for rule in self.map._rules:
try:
#-----------------------------------
# 內部調用 Rule.match()方法
#-----------------------------------
rv = rule.match(path, method)
except RequestSlash:
raise RequestRedirect(
self.make_redirect_url(
url_quote(path_info, self.map.charset, safe="/:|+") + "/",
query_args,
)
)
except RequestAliasRedirect as e:
raise RequestRedirect(
self.make_alias_redirect_url(
path, rule.endpoint, e.matched_values, method, query_args
)
)
if rv is None:
continue
if rule.methods is not None and method not in rule.methods:
have_match_for.update(rule.methods)
continue
if self.map.redirect_defaults:
redirect_url = self.get_default_redirect(rule, method, rv, query_args)
if redirect_url is not None:
raise RequestRedirect(redirect_url)
if rule.redirect_to is not None:
if isinstance(rule.redirect_to, string_types):
def _handle_match(match):
value = rv[match.group(1)]
return rule._converters[match.group(1)].to_url(value)
redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to)
else:
redirect_url = rule.redirect_to(self, **rv)
raise RequestRedirect(
str(
url_join(
"%s://%s%s%s"
% (
self.url_scheme or "http",
self.subdomain + "." if self.subdomain else "",
self.server_name,
self.script_name,
),
redirect_url,
)
)
)
# 如果設置 return_rule,返回 rule+arguments(tuple)
if return_rule:
return rule, rv
# 否則返回 endpoint+arguments(tuple)
else:
return rule.endpoint, rv
if have_match_for:
raise MethodNotAllowed(valid_methods=list(have_match_for))
raise NotFound()
比如在本例中, 請求的 path 爲 /message/13811112222
,調用 match()
方法後,返回 {'mobile_number' : '13811112222'}
(dict 類型)
轉換器與動態 url
Flask 中實現動態 url 的方法是通過 Map
類的轉換器 (converter)來實現,converter 中定義了 regular expression,在創建 Map 實例的時候,添加 Rule 的時候會有一系列方法調用:
- Map 初始化方法調用
Map.add()
方法 Map.add()
方法針對每一個 Rule, 調用Rule.bind()
方法Rule.bind()
方法調用Rule.compile()
方法- 在
Rule.compile()
方法中,定義了一個內部方法_build_regex()
將 URL 解析爲 converter, arguments 和 variable:
然後根據 Rule 中 converter 名稱調用對應的 converter,沒有指定 converter 名稱則默認爲 UnicodeConverter:
Rule 的格式是<converter(arguments):name>
,符合規則的格式才能被正確解析,werkzueg 實現了 7 中預定義的 converter,能滿足絕大部分需求。在 routing.py 中,我們可以看到如下的代碼:
DEFAULT_CONVERTERS = {
"default": UnicodeConverter,
"string": UnicodeConverter,
"any": AnyConverter,
"path": PathConverter,
"int": IntegerConverter,
"float": FloatConverter,
"uuid": UUIDConverter,
}
class Map(object):
default_converters = ImmutableDict(DEFAULT_CONVERTERS)
# ...
7 種 converter,都直接或者間接繼承自 BaseConverter
。BaseConverter
類的代碼如下:
class BaseConverter(object):
"""Base class for all converters."""
regex = "[^/]+"
weight = 100
def __init__(self, map):
self.map = map
def to_python(self, value):
return value
def to_url(self, value):
if isinstance(value, (bytes, bytearray)):
return _fast_url_quote(value)
return _fast_url_quote(text_type(value).encode(self.map.charset))
每一種 converter 的 __init__()
方法確定了 converter 可以使用哪些 arguments。比如 UnicodeConverter 的 __init__()
方法是這樣的:
def __init__(self, map, minlength=1, maxlength=None, length=None):
# 代碼略
所以我們在使用 UnicodeConverter 的時候能夠使用 length
、minlenght
和 maxlenght
這些參數。比如下面的示例:
rules = [
Rule('/message/<string(length=11):mobile_number>', endpoint='mobile')
]
BaseConveter
類的 to_python()
方法在 MapAdapter.match()
方法中匹配成功後被調用,將請求的 path 中動態 url 部分解析出 argument value, 傳遞給該方法的value 參數。在上面的示例中,13811112222 手機號碼被解析出來,傳遞給 to_python()
方法。
to_url()
方法在 url_for()
函數反向構建url 的時候,將 arguments
參數傳遞給該方法。後面結合具體的示例來幫助大家理解。
UnicodeConverter
是默認的轉換器,用於實現 string 類型的動態 url。
自定義轉換器
剛剛給出的例子沒有針對手機號碼的校驗規則,假設我們要對請求中傳遞的手機號碼進行校驗,可以用自定義轉換器來實現。如果只是想增加校驗規則,在轉換器的 __init__()
方法中改寫 BaseConverter 的 regex
屬性。
自定義轉換器代碼如下:
# CustomConverter.py
from werkzeug.routing import BaseConverter
class MobileConverter(BaseConverter):
def __init__(self, map):
BaseConverter.__init__(self, map)
self.regex = r'1[35678]\d{9}'
然後在創建 Map 的時候 converters 參數從自定義轉換器賦值:
from CustomConverter import MobileConverter
mobile_converter = [{'mobile', MobileConverter}]
rules = [
Rule('/', endpoint='index'),
Rule('/message/<mobile:mobile_number>', endpoint='mobile')
]
url_map = Map(rules, converters=mobile_converter)
to_python()
方法如何使用呢?假設爲了增加友好性,將返回到前端的手機號碼用 -
分割,比如 13811112222 顯示爲 138-1111-2222。根據剛纔的說明, to_python()
方法在匹配成功後將被調用,所以可以改寫 BaseConverter 的 to_python()
方法,編寫如下代碼:
from werkzeug.routing import BaseConverter
class MobileConverter(BaseConverter):
def __init__(self, map):
BaseConverter.__init__(self, map)
self.regex = r'1[35678]\d{9}'
def to_python(self, value):
return '{}-{}-{}'.format(value[:3], value[3:7], value[7:12])
to_url()
方法在 url_for() 函數中被調用,url_for() 函數的 argument 參數被傳遞給該方法的 value 參數。下面的示例演示了 to_url()
的用法。
from werkzeug.routing import BaseConverter
class MobileConverter(BaseConverter):
def __init__(self, map):
BaseConverter.__init__(self, map)
self.regex = r'1[35678]\d{9}'
def to_python(self, value):
return '{}-{}-{}'.format(value[:3], value[3:7], value[7:12])
def to_url(self, value):
print(value)
return value
from flask import Flask, url_for
from CustomConverter import MobileConverter
app = Flask(__name__)
app.url_map.converters['mobile'] = MobileConverter
@app.route('/')
def index():
return 'Index page'
@app.route('/message/<mobile:mobile_number>')
def send_message(mobile_number):
print(url_for('send_message', mobile_number='13833334444'))
return 'Message was sent to {}'.format(mobile_number)
if __name__ == '__main__':
app.run()