你所不知道的 Flask Part1:Route 初探
前言
我自己都記不清楚上一次寫博客是什麼時候了(笑),上一次挖的坑現在還沒填完,乾脆,開個新坑吧,你不知道的 Flask ,記錄下自己用 Flask 過程中一些很好玩的東西,當然很大可能我又會中途棄坑
開篇
引子
之前遇到一個很奇怪的需求,需要在flask中支持正則表達式比如,@app.route(/api/(.*?)) 這樣,在視圖函數被調用的時候,能傳入 URL 中正則匹配的值。不過 Flask 路由中默認不支持這樣的方法,那麼我們該怎麼辦?我們先思考五分鐘吧?
好了,我先給出解決方案吧
from flask import Flasknfrom werkzeug.routing import BaseConverternclass RegexConverter(BaseConverter):n def __init__(self, map, *args):n self.map = mapn self.regex = args[0]nnnapp = Flask(__name__)napp.url_map.converters[regex] = RegexConvertern
在經過這樣的設置後我們便可以按照我們剛才的需求寫代碼了
@app.route(/docs/model_utils/<regex(".*"):url>)ndef hello(url=None):nn print(url)n
在這裡,我們函數中傳入的url變數,就是我們代碼中所匹配到的值
但是為什麼這樣就OK了呢?
詳解
首先,我們要弄清楚一個東西,Flask 是 基於 Werkzurg 的一個框架,Flask 的 Route 機制基於 Werkzurg 上更進一步封裝所得到的,OK,我們上面所以實現的 Converter 便是利用了 Werkzurg 中的 Route 的特性
好了,我先給出官方文檔 custom-converters
然後我們來仔細講講,
首先,Werkzurg 中存在著一種機制叫做 Converter ,簡而言之就是通過一定的特殊語法,將 URL 中的特定部分,轉化成特定的 Python 變數,其語法格式為 /url/<converter_name("表達式"):變數名> 看起來有點複雜對吧,OK 用我們之前的例子來講一下吧,你看,我們之前定義了一個 /docs/model_utils/<regex(".*"):url> 的 URL ,其中後面部分就是利用了我們提到的 Converter 語法。具體的含義是,這個部分的 url 交給 regex 這個 Converter 來處理,最終生成的變數名為 url。
好了,我們來說說自定義 Converter 參數中的注意事項,在構建一個自己的 Converter 過程中,我們將按照如下的方式編寫代碼
class RegexConverter(BaseConverter):n def __init__(self, map, regex,*args):n self.map = mapn self.regex = regexn
map 是指 werkzurg.routing 中的 Map 對象,而 regex 則是指你所寫的表達式。其中 map 的作用我們將放在下一章進行講解,(又立flag了,笑)。
好了這裡差不多完成了,我們來看看 Flask 喔,不,werkzurg 中怎麼實現的這樣的方法吧
簡明代碼剖析
最前面,你首先得有一點 flask 裝飾器路由的知識,詳情可以參考這篇文章,菜鳥閱讀 Flask 源碼系列(1):Flask的router初探
首先在 werkzurg 框架的 routing 文件中,存在著這樣一段代碼
_rule_re = re.compile(rn (?P<static>[^<]*) # static rule datan <n (?:n (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter namen (?:((?P<args>.*?)))? # converter argumentsn : # variable delimitern )?n (?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable namen >n, re.VERBOSE)n_simple_rule_re = re.compile(r<([^>]+)>)n_converter_args_re = re.compile(rn ((?P<name>w+)s*=s*)?n (?P<value>n True|False|n d+.d+|n d+.|n d+|n w+|n [urUR]?(?P<stringval>"[^"]*?"|[^]*)n )s*,n, re.VERBOSE | re.UNICODE)nndef parse_converter_args(argstr):n argstr += ,n args = []n kwargs = {}nn for item in _converter_args_re.finditer(argstr):n value = item.group(stringval)n if value is None:n value = item.group(value)n value = _pythonize(value)n if not item.group(name):n args.append(value)n else:n name = item.group(name)n kwargs[name] = valuenn return tuple(args), kwargsnnndef parse_rule(rule):n """Parse a rule and return it as generator. Each iteration yields tuplesn in the form ``(converter, arguments, variable)``. If the converter isn `None` its a static url part, otherwise its a dynamic one.nn :internal:n """n pos = 0n end = len(rule)n do_match = _rule_re.matchn used_names = set()n while pos < end:n m = do_match(rule, pos)n if m is None:n breakn data = m.groupdict()n if data[static]:n yield None, None, data[static]n variable = data[variable]n converter = data[converter] or defaultn if variable in used_names:n raise ValueError(variable name %r used twice. % variable)n used_names.add(variable)n yield converter, data[args] or None, variablen pos = m.end()n if pos < end:n remaining = rule[pos:]n if > in remaining or < in remaining:n raise ValueError(malformed url rule: %r % rule)n yield None, None, remainingn
首先,_rule_re 以及 _converter_args_re 兩段是很騷的正則表達式,不過作者已經給出了足夠的注釋,大家可以對照著正則表達式的語法進行學習一個,然後 parse_converter_args 以及 parse_rule則是利用正則表達式對其進行解析操作。
OK,我們緊接著往下查看
def compile(self):n """Compiles the regular expression and stores it."""n assert self.map is not None, rule not boundnn if self.map.host_matching:n domain_rule = self.host or n else:n domain_rule = self.subdomain or nn self._trace = []n self._converters = {}n self._weights = []n regex_parts = []nn def _build_regex(rule):n for converter, arguments, variable in parse_rule(rule):n if converter is None:n regex_parts.append(re.escape(variable))n self._trace.append((False, variable))n for part in variable.split(/):n if part:n self._weights.append((0, -len(part)))n else:n if arguments:n c_args, c_kwargs = parse_converter_args(arguments)n else:n c_args = ()n c_kwargs = {}n convobj = self.get_converter(n variable, converter, c_args, c_kwargs)n regex_parts.append((?P<%s>%s) % (variable, convobj.regex))n self._converters[variable] = convobjn self._trace.append((True, variable))n self._weights.append((1, convobj.weight))n self.arguments.add(str(variable))nn _build_regex(domain_rule)n regex_parts.append(|)n self._trace.append((False, |))n _build_regex(self.is_leaf and self.rule or self.rule.rstrip(/))n if not self.is_leaf:n self._trace.append((False, /))nn if self.build_only:n returnn regex = r^%s%s$ % (n u.join(regex_parts),n (not self.is_leaf or not self.strict_slashes) andn (?<!/)(?P<__suffix__>/?) or n )n self._regex = re.compile(regex, re.UNICODE)n
這是 werkzurg 框架的 routing 文件中 Rule 類種的一部分的源碼,其中在 `def _build_regex(rule):` 之前的是一些準備代碼,然後我們接著往下看,`for converter, arguments, variable in parse_rule(rule):` 這一段代碼,就是 URL 解析,通過調用 `parse_rule` 函數來實現對我們之前提到的 converter 語法進行解析,緊接著,如果 URL 里不存在我們 Converter 的語法,則 `converter` 為空,我們執行處理其餘 URL 的邏輯,如果 `converter` 存在,進行下面的流程,首先,如果我們在 Converter 語法中設定了解析表達式,那麼我們利用 `parse_converter_args` 函數來處理我們的表達式,方便後續的操作,處理完成後,我們利用 `get_converter` 方法來初始化我們的 Converter , 代碼如下:
def get_converter(self, variable_name, converter_name, args, kwargs):n """Looks up the converter for the given parameter.nn .. versionadded:: 0.9n """n if converter_name not in self.map.converters:n raise LookupError(the converter %r does not exist % converter_name)n return self.map.converters[converter_name](self.map, *args, **kwargs)n
以我們之前的 demo 為例,
from flask import Flasknfrom werkzeug.routing import BaseConverternclass RegexConverter(BaseConverter):n def __init__(self, map, *args):n self.map = mapn self.regex = args[0]nnnapp = Flask(__name__)napp.url_map.converters[regex] = RegexConvertern
我們已經添加了一個名為 regex 的 Converter 對象,在 get_converter 方法中我們傳入了值為 regex 的 converter_name 變數,緊接著,我們初始化了一個 RegexConverter 對象的實例,然後返回這個實例
def compile(self):n """Compiles the regular expression and stores it."""n assert self.map is not None, rule not boundnn if self.map.host_matching:n domain_rule = self.host or n else:n domain_rule = self.subdomain or nn self._trace = []n self._converters = {}n self._weights = []n regex_parts = []nn def _build_regex(rule):n for converter, arguments, variable in parse_rule(rule):n if converter is None:n regex_parts.append(re.escape(variable))n self._trace.append((False, variable))n for part in variable.split(/):n if part:n self._weights.append((0, -len(part)))n else:n if arguments:n c_args, c_kwargs = parse_converter_args(arguments)n else:n c_args = ()n c_kwargs = {}n convobj = self.get_converter(n variable, converter, c_args, c_kwargs)n############################################################# 無恥分割線n regex_parts.append((?P<%s>%s) % (variable, convobj.regex))n self._converters[variable] = convobjn self._trace.append((True, variable))n self._weights.append((1, convobj.weight))n self.arguments.add(str(variable))nn _build_regex(domain_rule)n regex_parts.append(|)n self._trace.append((False, |))n _build_regex(self.is_leaf and self.rule or self.rule.rstrip(/))n if not self.is_leaf:n self._trace.append((False, /))nn if self.build_only:n returnn regex = r^%s%s$ % (n u.join(regex_parts),n (not self.is_leaf or not self.strict_slashes) andn (?<!/)(?P<__suffix__>/?) or n )n self._regex = re.compile(regex, re.UNICODE)n
在分割線後面的代碼中,我們對處理後的 url 進行一些收尾的操作,以我們之前的 demo 為例,我們設定的 /docs/model_utils/<regex(".*"):url> URL 最終轉化成 /docs/model_utils/(?P<url>.*) ,編譯成 re 對象後賦值給 Rule 實例中的 _regex 變數
好了,我們知道處理的部分後,我們大致來看一下怎麼匹配並生成值的吧
def match(self, path, method=None):n """Check if the rule matches a given path. Path is a string in then form ``"subdomain|/path"`` and is assembled by the map. Ifn the map is doing host matching the subdomain part will be the hostn instead.nn If the rule matches a dict with the converted values is returned,n otherwise the return value is `None`.nn :internal:n """n if not self.build_only:n m = self._regex.search(path)n if m is not None:n groups = m.groupdict()n # we have a folder like part of the url without a trailingn # slash and strict slashes enabled. raise an exception thatn # tells the map to redirect to the same url but with an # trailing slashn if self.strict_slashes and not self.is_leaf and n not groups.pop(__suffix__) and n (method is None or self.methods is None orn method in self.methods):n raise RequestSlash()n # if we are not in strict slashes mode we have to removen # a __suffix__n elif not self.strict_slashes:n del groups[__suffix__]nn result = {}n for name, value in iteritems(groups):n try:n value = self._converters[name].to_python(value)n except ValidationError:n returnn result[str(name)] = valuen if self.defaults:n result.update(self.defaults)nn if self.alias and self.map.redirect_defaults:n raise RequestAliasRedirect(result)nn return resultn
這也是 werkzurg 框架的 routing 文件中 Rule 類種的一部分的源碼,在這段代碼中,首先利用 re 對象中的 search 方法,檢測當前傳入的 Path 是否匹配,如果匹配的話,進入後續的處理流程,還記得我們之前最終生成的 /docs/model_utils/(?P<url>.*) 么,這裡面利用了正則表達式命名組的語法糖,在這裡,匹配成功後,Python 的 re 庫里給我們提供了一個 groupdict 讓我們取出命名組裡所代表的值。然後我們調用 conveter 實例裡面的 to_python 方法來對我們匹配出來的值進行處理(註:這是 Converter 系列對象中的一個可重載方法,我們可以通過重載這個方法,來對我們匹配到的值進行一些邏輯處理,這個我們還是後面再講吧,flag++),然後我們把最終的 result 值返回。
最後的最後,Flask 在獲取 werkzurg 給出的匹配結果後,將匹配的值,放在 request 實例中的 view_args 變數上,最後通過 dispatch_request 對象傳遞給我們的視圖函數,代碼如下
def dispatch_request(self):n """Does the request dispatching. Matches the URL and returns then return value of the view or error handler. This does not have ton be a response object. In order to convert the return value to an proper response object, call :func:`make_response`.nn .. versionchanged:: 0.7n This no longer does the exception handling, this code wasn moved to the new :meth:`full_dispatch_request`.n """n req = _request_ctx_stack.top.requestn if req.routing_exception is not None:n self.raise_routing_exception(req)n rule = req.url_rulen # if we provide automatic options for this URL and then # request came with the OPTIONS method, reply automaticallyn if getattr(rule, provide_automatic_options, False) n and req.method == OPTIONS:n return self.make_default_options_response()n # otherwise dispatch to the handler for that endpointn return self.view_functions[rule.endpoint](**req.view_args)n
好了,我們的代碼剖析就到此結束
最後想說幾句
Flask + Werkzurg 是一套設計實現的非常精妙的組合,不過我們在日常的使用中常常忽略了裡面的美麗的風景,所以這也是我想寫這樣剖析代碼筆記的文章的原因
好了,給老鐵們留幾個思考題,歡迎評論區討論
- Flask 為什麼不默認支持正則表達式的輸入
- 諸如 PathConverter 這樣 Werkzurg 內置的 Converter 為什麼在寫表達式的時候可以這樣 /<path:wikipage>/edit 寫,而忽略其中的表達式
- 前面提到的 parse_converter_args 方法的代碼詳解
好了,就先這樣吧2333
對了,保佑我文章里立的 Flag 都能實現(笑)
推薦閱讀:
※個人充電,想學一門替代python的語言?
※使用python語言如何保密源代碼以防止逆向工程?
※關於python Django與Flask學習的一些疑惑?
※有趣的圖形:用Python繪製帶餅圖的散點圖兼論marker的隱藏功能