美文网首页
Bottle源码 route 源码分析

Bottle源码 route 源码分析

作者: Yucz | 来源:发表于2020-10-31 09:44 被阅读0次

简书上为首发,拒绝抄袭,原文地址 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, {})
    # 先省略下面的代码,下面的代码是用来匹配包含正则表达式的路径的

根据代码分析,当我们传递进来的 urlROUTES_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)

这里面包含两条正则表达式

  1. :([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)
  2. :([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/ 开头,后面接着多个数字字符的路径

相关文章

网友评论

      本文标题:Bottle源码 route 源码分析

      本文链接:https://www.haomeiwen.com/subject/zxpxmktx.html