打造mvc框架之实现高效的路由规则匹配

    在python的主流框架里面对于route路由规则的处理是相当的直接,都是使用python re正则表达式来处理 。想想这也是理所当然的,如果想要构建丰富的url匹配,那必须要用正则表达式的。 另外route路由器本身不会重新排序,他的顺序是你程序构建route规则的顺序。

该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。http://xiaorui.cc/?p=3215

简单说说,tornado、flask框架的路由添加方法。

tornado的匹配模式,这样的模式较统一,可以在一个入口管理所有的路由规则。

#xiaorui.cc
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("You requested the main page")

class StoryHandler(tornado.web.RequestHandler):
    def get(self, story_id):
        self.write("You requested the story " + story_id)

application = tornado.web.Application([
    (r"/home", MainHandler),
    (r"/story/([0-9]+)", StoryHandler),
])

下面是flask的装饰器添加发挥方式,当然他也有blueprints蓝图模式。 装饰器模式适合接口不多的情况,也算是方便。仁者见仁智者见智,在路由项较少的时候,我偏向于这个用法。 

#xiaorui.cc
@user.route('/<userId>/',methods=['GET', 'POST'])
@user.route('/<userId>/<username>',methods=['GET', 'POST'])
def show(userId, username=None):
    pass


@user.route('/<userId>')
def show(userId):
   return show_with_username(userId)

@user.route('/<userId>/<username>')
def show_with_username(userId,username=None):
   pass

flask、torando、bottle的route代码我都看了一遍,没什么让人惊艳的地方,就是一个个的正则匹配。 我有些好奇,为什么不把这些规则构建成hash匹配,或者是树trie结构等等。 如果安装这些主流route规则的方法来说,绝对的O(1)呀.

不理解 ! 

咱们先不说性能的问题,先把route规则的要点说明白。在web框架中,路由表中的任何一个规则都包含pattern(模式)和handler(处理函数或类)。当httpserver接收到一个http请求,server从接收到的请求中解析出url path,然后顺序遍历路由表,如果发现url path可以匹配某个pattern,则将此http request交给web应用中对应的handler去处理。  

再开发路由器之前,看看tornado是如何实现route匹配,是否高雅.

下面是tornado demo. 我们创建了两个路由规则。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("You requested the main page")

class StoryHandler(tornado.web.RequestHandler):
    def get(self, story_id):
        self.write("You requested the story " + story_id)

application = tornado.web.Application([
    (r"/home", MainHandler),
    (r"/story/([0-9]+)", StoryHandler),
])

下面是tornado添加路由的动作.


这是上面提过的URLSpec类,可以接受pattern, handler, kwargs。

class URLSpec(object):
    def __init__(self, pattern, handler, kwargs=None, name=None):
        if not pattern.endswith(''):
            pattern += ''
        self.regex = re.compile(pattern)
        assert len(self.regex.groupindex) in (0, self.regex.groups), \
            ("groups in url regexes must either be all named or all "
             "positional: %r" % self.regex.pattern)

        if isinstance(handler, str):
            handler = import_object(handler)

        self.handler_class = handler
        self.kwargs = kwargs or {}
        self.name = name
        self._path, self._group_count = self._find_groups()

下面是tornado如何匹配路由规则的。

def _find_handler(self):
        app = self.application
        handlers = app._get_host_handlers(self.request)
        if not handlers:
            self.handler_class = RedirectHandler
            self.handler_kwargs = dict(url="%s://%s/" % (self.request.protocol, app.default_host))
            return
        #根据host拿到相应的handlers,这handlers含有url --> handler的规则
        for spec in handlers:
            match = spec.regex.match(self.request.path)   #开始正则匹配
            if match:
                self.handler_class = spec.handler_class    # 跟url相对的处理类
                self.handler_kwargs = spec.kwargs
                if spec.regex.groups:
                    if spec.regex.groupindex:
                        self.path_kwargs = dict(
                            (str(k), _unquote_or_none(v))
                            for (k, v) in match.groupdict().items())
                    else:
                        self.path_args = [_unquote_or_none(s)
                                          for s in match.groups()]
                return
        if app.settings.get('default_handler_class'):
            self.handler_class = app.settings['default_handler_class']
            self.handler_kwargs = app.settings.get(
                'default_handler_args', {})
        else:
            self.handler_class = ErrorHandler
            self.handler_kwargs = dict(status_code=404)    #如果没有匹配规则,那么返回404


用过flask、bottle的人知道,他们是使用装饰器的方式来添加规则的。 抛开这两web框架自己怎么实现路由匹配。

实现原理也相当简单,把url + handler + method 扔到一个列表,当请求来的时候一个个的匹配。 

import re

class RouteMatch(object):
    def __init__(self, app, match, rest, info):
        self.app = app
        self.match = match
        self.rest = rest
        self.info = info

class Router(object):
    def __init__(self):
        self.rules = []

    def route(self, pat, methods=["GET"]):
        def decor(application):
            regex = re.compile(pat)
            self.rules.append((
                    (regex, methods, application), # for working
                    (pat,) # for reporting
                    ))

            return application
        return decor

    def resolve(self, method, path):
        tried = []
        original_path = path
        for (regex, methods, app), info in self.rules:
            if method in methods:
                match = regex.match(path)

                if match:
                    # Strip off the matched part of the path
                    rest = regex.sub("", path)
                    return RouteMatch(app, match, rest, info)
                else:
                    tried.append({'method': method, 'path': path, 'pattern': info[0], 'methods': methods})

        raise RouteNotFound(self, tried)

    def path_info(self, environ):
        return environ['PATH_INFO']

    def __call__(self, environ, start_response, path=None):
        method = environ['REQUEST_METHOD']

        if path is None:
            path = self.path_info(environ)

        result = self.resolve(method, path)

        if result.app is not None:
            kwargs = result.match.groupdict()

            if kwargs:
                args = ()
            else:
                kwargs = {}
                args = result.match.groups()

            environ['wsgiorg.routing_args'] = (args, kwargs)

            if isinstance(result.app, Router):
                return result.app(environ, start_response, path=result.rest)
            else:
                return result.app(environ, start_response)

使用方法:

router = Router()

@router.route("^/search/")
def blog_entries(environ, start_response):
   start_response("200 OK", [])
   return ["Some content"]

对于高效路由规则匹配,说实话我还没实现,没有实现的原因是不是自己写不出来,而是懒,另外我没有搞明白主流的框架为什么会使用这么低效的路由匹配规则。   先不管别人的实现,先描述下高效路由器的实现思路。

首先url根据 / 进行阶段,在路由正则里坚决不能写/字符串。 /a/b/c/search?keyword=xiaorui.cc ,  这时候我们可以把 a + b + c 做个md5,跟handler放在一个hash map里就可以了。 如果构建路由的时候,需要细粒度的method,那么可以a + b + c + method 映射handler。  这个情况比较适合于没有正则的规则。  如果有复杂的的正则模式怎么办?   首先我们的场景很少有那种从头到尾都是正则的,比如 /home\w*/add.*/article.*/page.*/scollid\w*  ,你丫吃干饭的,啥都不确定?   一般最少第一层是确定的,后面的几层不确定,那么我们使用最低深度法,如果最低层是第一层,匹配到第一层后,按照第一层的规则来熟悉的匹配。


URL样式:

/a/b\w*
/a1/b\w*
/a2/b\w*
/a1/c1/d1
/a1/c1/d2

构建出来的匹配结构.

                    /
    a              a1            a2
    b\w*     c1    b\w*       key
              d1   d2

END

  


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc

发表评论

邮箱地址不会被公开。 必填项已用*标注