简书上为首发,拒绝抄袭,原文地址 https://www.jianshu.com/p/a4a50c0b7ea7
Bottle Route
route 的常规使用场景分成如下三个场景进行分析
# 第一种 简单的 / 和 字符的组合,函数不包含参数
@route('/hello_world')
def hello_world():
return 'Hello World!'
# 第二种 包含 :指定名字
@route('/hello/:name')
def hello_url(name):
return 'Hello %s!' % name
# 第三种 包含 :指定名字 #xxxx#
@route('/number/:num#[0-9]+#')
def hello_number(num):
return "Your number is %d" % int(num)
1 route 装饰器的实现
def route(url, **kargs):
""" Decorator for request handler. Same as add_route(url, handler)."""
def wrapper(handler):
add_route(url, handler, **kargs)
return handler
return wrapper
由代码可见, route
装饰器返回的函数其实还是函数本身,装饰器唯一的作用就是调用 add_route
函数,将当前的路径和函数绑定在一起
2 add_route的实现
ROUTES_SIMPLE = {}
ROUTES_REGEXP = {}
def add_route(route, handler, method='GET', simple=False):
""" Adds a new route to the route mappings.
Example:
def hello():
return "Hello world!"
add_route(r'/hello', hello)"""
method = method.strip().upper()
if re.match(r'^/(\w+/)*\w*$', route) or simple:
ROUTES_SIMPLE.setdefault(method, {})[route] = handler
else:
route = compile_route(route)
ROUTES_REGEXP.setdefault(method, []).append([route, handler])
先看下正则表达式的 r'^/(\w+/)*\w*$'
,匹配规则简单分析如下
- 匹配以
/
开头的字符串 - 中间可以有0个多个
\w+/
,即多个类似如下的字符:aa/bb/cc/
- 最后以0个或者多个普通字符结尾
所以当调用 route()
,参数如下时会匹配上
/
/hello_world
/hello/aa/bb/cc
而匹配上后,add_route
函数的就只是将其放在 ROUTES_SIMPLE
中,key 为我们传递进来的字符串,value 为被装饰的函数
3 再看下当我们客户端发起访问的时候,我们服务端是怎么调用相应的函数的
先看下 WSGI
统一的接口 WSGIHandler
,当客户端发起访问请求的时候,都会进到这个函数里面,而客户端请求的所有信息都包含在 environ
参数里面,包括
- 请求的路径
- 请求的方式 GET/POST 等
- cookie
- 等等
def WSGIHandler(environ, start_response):
"""The bottle WSGI-handler."""
globalfjhvvi request
global response
request.bind(environ)
response.bind()
try:
handler, args = match_url(request.path, request.method)
if not handler:
raise HTTPError(404, "Not found")
# 如果找到了路径对应的函数,则调用该函数,返回函数的执行结果
output = handler(**args)
except BreakTheBottle, shard:
# 省略下面的代码
当我们从客户端的请求中获取路径path和方法method后,则调用match_url
来确定要调用那个函数
match_url
的实现如下
def match_url(url, method='GET'):
"""Returns the first matching handler and a parameter dict or (None, None).
This reorders the ROUTING_REGEXP list every 1000 requests. To turn this off, use OPTIMIZER=False"""
url = '/' + url.strip().lstrip("/")
# Search for static routes first
route = ROUTES_SIMPLE.get(method,{}).get(url,None)
if route:
return (route, {})
# 先省略下面的代码,下面的代码是用来匹配包含正则表达式的路径的
根据代码分析,当我们传递进来的 url
再 ROUTES_SIMPLE
中找到的话,则直接返回该函数和一个空字典。
所以当我们访问 http://localhost:8081/hello_world
时,会直接调用函数 hello_world
, 然后在浏览器中显示该函数的执行的返回值 'Hello World!'
3 包含正则的路径
如果传递进来的路径不是第一种路径的话,那么将会执行 compile_route
,进行匹配,代码实现如下
def compile_route(route):bcdaui
""" Compiles a route string and returns a precompiled RegexObject.
Routes may contain regular expressions with named groups to support url parameters.
Example: '/user/(?P<id>[0-9]+)' will match '/user/5' with {'id':'5'}
A more human readable syntax is supported too.
Example: '/user/:id/:action' will match '/user/5/kiss' with {'id':'5', 'action':'kiss'}
"""
oritin_route = route
# 什么时候左边会出现 $^/
# 右边会出现 $^
route = route.strip().lstrip('$^/ ').rstrip('$^ ')
route = re.sub(r':([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)',r'(?P<\1>\g<re>)',route)
second_route = route
route = re.sub(r':([a-zA-Z_]+)',r'(?P<\1>[^/]+)', route)
print 'origin route:\t', oritin_route, '\tsecond route:', second_route, '\tcompile route:', route
return re.compile('^/%s$' % route)
这里面包含两条正则表达式
:([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)
:([a-zA-Z_]+)',r'(?P<\1>[^/]+)
4 对第一条正则进行分析
当我们注册route时,传递的参数为 @route('/hello/:name')
,不会匹配上第一个正则,所以先不看这个正则。它会匹配第二个正则。我们先对第二个正则进行一个简单的分析
:([a-zA-Z_]+)
会匹配以 :
开头,包含多个字母的字符串
所以 :name
会被匹配到
匹配到之后,会调用 re.sub(:([a-zA-Z_]+)',r'(?P<\1>[^/]+))
调用re.sub
,时,正则匹配到的第一个组(简单的说,就是原字符串第一个括号里的内容,即 [a-zA-Z_]), 匹配到的是 name
, 将替换到第二个字符串的 \1
, 则替换后的字符为 (?P<name>[^/]+)
,
测试如下
route = "hello/:name"
route = re.sub(r':([a-zA-Z_]+)', r'(?P<\1>[^/]+)', route)
print(route)
输出为
hello/(?P<name>[^/]+)
假设我们输入的是 hello/:name
,则经过 re.sub
替换之后,将变成hello/(?P<name>[^/]+)
在 compile_route
的最后一条代码re.compile('^/%s$' % route)
所以,最终进入编译的正则表达式为 ^/hello/(?P<name>[^/]+)
,然后被存进 ROUTES_REGEXP
里
而hello/(?P<name>[^/]+)
将会匹配的语句如下
- 以
/hello/
开头 - 后面接着1个或者多个不为
/
的字符,并切这些字符将会被放到正则的 group 中,通过字段name可拿到
比如
[9]: pattern = r"^/hello/(?P<name>[^/]+)"
In [10]: m = re.match(pattern=pattern, string="/hello/messi")
In [11]: m.groupdict()
Out[11]: {'name': 'messi'}
当客户端发起请求,链接为 /hello/messi
时,则会进入 match_url
, 代码如下
def match_url(url, method='GET'):
# 前面的省略
# Now search regexp routes
routes = ROUTES_REGEXP.get(method,[])
for i in xrange(len(routes)):
match = routes[i][0].match(url)
if match:
handler = routes[i][1]
# 省略不相干代码
return (handler, match.groupdict())
return (None, None)
在匹配时,会将我们注册进 ROUTES_REGEXP
的每一条正则进行一个遍历,当匹配上的时候,返回对应的函数,以及正则匹配后的 groupdict()
, 这个将会当做函数的 args
,进行调用
再看下另一个例子
@route(/validate/:i/:f/:csv)
经过 compile_route
之后,会替换成 ^/validate/(?P<i>[^/]+)/(?P<f>[^/]+)/(?P<csv>[^/]+)$
In [13]: pattern = r"^/validate/(?P<i>[^/]+)/(?P<f>[^/]+)/(?P<csv>[^/]+)$"
In [14]: s = "/validate/aaa/bbb/ccc"
In [15]: m = re.match(pattern, s)
In [16]: m
Out[16]: <_sre.SRE_Match object; span=(0, 21), match='/validate/aaa/bbb/ccc'>
In [17]: m.groupdict()
Out[17]: {'csv': 'ccc', 'f': 'bbb', 'i': 'aaa'}
再看第二条正则表达式
:([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)
当我们注册 route 的参数为 @route('/number/:num#[0-9]+#')
, 则会匹配到这一个正则表达式
-
:([a-zA-Z_]+)
匹配:
后面跟上字母和下划线的组合,对应的就是:num
并且会将num
放进正则匹配的第一组 -
(?P<uniq>[^\w/])
匹配一个不为\w
(即正常字符) 和/
的一个字符,对应的就是#
号,并将这个#
放进组uniq
中 -
(?P<re>.+?)
则为.+
标识匹配一个或者多个任意字符.+?
则标识惰性匹配,匹配到最少符合这个格式的字符串。并将匹配完后的字符串放进组re
中,对应的就是[0-9]+
-
(?P=uniq)
表示匹配到的字符串要跟第二部中的一样,假设第二步中匹配到的是#
,则这一步也必须要为#
In [18]: pattern1 = r"(?P<uniq>[^\w\/])(?P<re>.+?)(?P=uniq)" # 惰性匹配
In [19]: pattern2 = r"(?P<uniq>[^\w\/])(?P<re>.+)(?P=uniq)" # 贪婪匹配
In [20]: s = "#123##"
In [21]: m1 = re.match(pattern1, s)
In [22]: m1.groupdict()
Out[22]: {'re': '123', 'uniq': '#'} # re组不包含 #
In [24]: m2 = re.match(pattern2, s)
In [25]: m2.groupdict()
Out[25]: {'re': '123#', 'uniq': '#'} # re组包含 #
匹配完之后,将会调用 re.sub
进行正则替换
route = "/number/:num#[0-9]+#"
route = re.sub(r':([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)', r'(?P<\1>\g<re>)', route)
print(route) # /number/(?P<num>[0-9]+)
\
为我们匹配的第一组即为 num
\g<re>
则替换组re
里的内容即 [0-9]+
所以替换后的字符为 /number/(?P<num>[0-9]+)
所以,这个函数将匹配以 /number/
开头,后面接着多个数字字符的路径
网友评论