打造mvc框架之python template模板实现原理

造轮子

       喜欢造轮子的我,那肯定也是实现过一套mvc的web框架的人,不止一套。 先前是用gevent做wsgi,jinja2做的模板,peewee做orm,我做了一个控制器来衔接各个组件,  如果想使用gevent的协程处理每个request_handler需要做一些妥协,比如第三方库的选择。 

       完成这一步之后,我觉得我不过瘾,索性直接自己写模板,自己写个易懂的orm,最后自己实现了一个类似tornado的异步框架,仿照Werkzeug来解析http协议,每个请求通过epoll做io调度,代理里存在各种的callback,各种的代码分段,各种的销魂….        

      上个月跟同事张磊大哥一起去参加雨痕的 《如何构建高性能web框架》的分享收益很多。 趁着这个热乎劲写写如何实现一个python的web框架.  


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


雨痕这次虽然没有过多说明mvc的实现细节,更多的是阐述了web框架的组件的选择,轮子要怎么造,轮子的深度怎么控制.   我虽然在线上都使用过的自己开发的web框架,但依旧觉得实现过程有些low ! 这次分享收获很大,可惜的是当时没有录像啥的. 


正题开始:

咱们先把web框架的模板实现给整明白,这在mvc里是重要的一环。 话说 python下的template模板还是不少的,独立的有mako,jinja2,另外像tornado,bottle,django自己也实现了一套模板。python模板大多数用在html网页中。 其实也可以来实现动态配置啥的。 


怎么实现python的模板,换句话说大多数模板都咋实现的? 

这是个很有意思的话题,以前因为没看过template代码实现,总以为就是正则替换,后来静下心的时候想想,单个变量还是挺好弄的,但是如果有有条件判断,循环语句, 特例函数怎么搞? 
这两天我看tornado,bottle关于template的代码实现,其实发现template远比我们想的有趣,他是把模板字符串中一些语法标记,转换成python语句执行。

我这里借鉴下别人的template代码,这代码的精简干练,要比django好讲解。 

https://raw.githubusercontent.com/mozillazg/lsbate/master/part2/template2d.py

自定义模板的关键字

任何一个模板都有模板的关键字及模板语法, 我们这里的 {{ var }} {% if var %} {% endif %} {% for item in items %} 这样都是关键字.   python中各个模板使用方法大同小异,没啥本质区别。

关键字解析

关键字的用途是用来拆解数据的,主要是通过关键词抽取tab,我们要把这些tab转换成python语句.   这里说下解析抽取的语法. 

#xiaorui.cc
#html模板的内容
self.raw_text = """
<h1>{{ title }} counter: {{ 1+10 }}</h1>
<p>{% for item in items %}\n{{ item }}{% endfor %}</p>
"""
self.re_tokens = re.compile(r'''(
    (?:\{\{ .*? \}\})
    |(?:\{\# .*? \#\})
    |(?:\{% .*? %\})
)''', re.X)
tokens = self.re_tokens.split(self.raw_text)
返回的结果是被关键字用 , 分开. 
#xiaorui.cc
['<h1>', '{{ title }}', ' counter: ', '{{ 1+10 }}', '</h1>\n    <p>', '{% for item in items %}', '\n', '{{ item }}', '', '{% endfor %}', '</p>\n    ']

生成Python代码

我们拿到解析关键字的字符串后,还需要通过这些关键字来构建成python语句.  这里有个CodeBuilder类,这个类是控制python代码缩进的.

#xiaorui.cc
# 变量
self.re_variable = re.compile(r'\{\{ .*? \}\}')
# 注释
self.re_comment = re.compile(r'\{# .*? #\}')
# 标签
self.re_tag = re.compile(r'\{% .*? %\}')

def _parse_text(self):
    handlers = (
        (self.re_variable.match, self._handle_variable),   # {{ variable }}
        (self.re_tag.match, self._handle_tag),             # {% tag %}
        (self.re_comment.match, self._handle_comment),     # {# comment #}
    )
    default_handler = self._handle_string                  # 普通字符串

    for token in tokens:
        for match, handler in handlers:
            if match(token):
                handler(token)
                break
        else:
            default_handler(token)

def _handle_variable(self, token):
    """处理变量"""
    variable = token.strip('{} ')
    self.buffered.append('str({})'.format(variable))

def _handle_comment(self, token):
    """处理注释"""
    pass

def _handle_string(self, token):
    """处理字符串"""
    self.buffered.append('{}'.format(repr(token)))

def _handle_tag(self, token):
    """处理标签"""
    self.flush_buffer()
    tag = token.strip('{%} ')
    tag_name = tag.split()[0]
    self._handle_statement(tag, tag_name)

def _handle_statement(self, tag, tag_name):
    """处理 if/for"""
    if tag_name in ('if', 'elif', 'else', 'for'):
        if tag_name in ('elif', 'else'):  #如果tag_name是elif,else,那么python的ident减去4个. 这样跟上面的if ,for齐平.
            self.code_builder.backward()
        self.code_builder.add_line('{}:'.format(tag))
        self.code_builder.forward()    #ident增加4个
    elif tag_name in ('break',):
        self.code_builder.add_line(tag)
    elif tag_name in ('endif', 'endfor'):
        self.code_builder.backward()

结果的返回结果是:
['def __func_name():', '    __result = []', "    __result.extend(['<h1>',str(title),' counter: ',str(1+10),'</h1>\\n    <p>'])", '    for item in items:', "        __result.extend(['\\n',str(item),''])", "    __result.extend(['</p>\\n    '])", '    return "".join(__result)']
格式化后这样的:

def __func_name():
    __result = []
    __result.extend(['\n    <h1>',str(title),' counter: ',str(1+10),'</h1>\n    <p>'])
    for item in items:
        __result.extend(['\n',str(item),''])
    __result.extend(['</p>\n    '])
    return "".join(__result)

字符串如何当成对象? 这里也不扯反射,自省模式啥的,没用.   python有个exec函数专门用来解决字符串对象化执行的. exec语句本来是用来执行存在于字符串、文件中的Python语句,很适合咱们这场景.

最后生成模板解析后的格式

namespace含有我们传递的字段数据,code_builder是有函数意义的字符串。 exec函数实现 func string –> func object . 
exec(func_string,namespace), exec会返回给我们func_string对象信息,及变量环境. 然后在namespace[func_name]()就可以正常执行的。
def render(self, context=None):
    """渲染模版"""
    namespace = {}
    namespace.update(self.default_context)
    if context:
        namespace.update(context)
    exec(str(self.code_builder), namespace)
    result = namespace[self.func_name]()
    return result
如果还是不理解exec在合理的用法,我这里演示下exec在template的用途.
In [1]: str = """
def set():
    print url
    return url
"""

In [2]: namespace = {"url":"xiaorui.cc"}

In [3]: exec(str,namespace)

In [4]: namespace['set']()
xiaorui.cc
Out[4]: 'xiaorui.cc'


总结, 这样虽然实现了模板,但是对于使用过jinja2的人来说,会发现这个自定义的模板功能显得很单薄。  没有filters自定义函数,另外安全也是个大问题,尤其我们这边使用了exec 大杀器, escape也是个有意思的话题。


下面是python template实现的全部代码:

#coding:utf-8
import re


class CodeBuilder:
    INDENT_STEP = 4     # 每次缩进的空格数

    def __init__(self, indent=0):
        self.indent = indent    # 当前缩进
        self.lines = []         # 保存一行一行生成的代码

    def forward(self):
        """缩进前进一步"""
        self.indent += self.INDENT_STEP

    def backward(self):
        """缩进后退一步"""
        self.indent -= self.INDENT_STEP

    def add(self, code):
        self.lines.append(code)

    def add_line(self, code):
        self.lines.append(' ' * self.indent + code)
        print 111111,self.lines

    def __str__(self):
        """拼接所有代码行后的源码"""
        return '\n'.join(map(str, self.lines))

    def __repr__(self):
        """方便调试"""
        return str(self)


class Template:

    def __init__(self, raw_text, indent=0, default_context=None,
                 func_name='__func_name', result_var='__result'):
        self.raw_text = raw_text
        self.default_context = default_context or {}
        self.func_name = func_name
        self.result_var = result_var
        self.code_builder = code_builder = CodeBuilder(indent=indent)
        self.buffered = []

        # 变量
        self.re_variable = re.compile(r'\{\{ .*? \}\}')
        # 注释
        self.re_comment = re.compile(r'\{# .*? #\}')
        # 标签
        self.re_tag = re.compile(r'\{% .*? %\}')
        # 用于按变量,注释,标签分割模版字符串
        self.re_tokens = re.compile(r'''(
            (?:\{\{ .*? \}\})
            |(?:\{\# .*? \#\})
            |(?:\{% .*? %\})
        )''', re.X)

        # 生成 def __func_name():
        code_builder.add_line('def {}():'.format(self.func_name))
        code_builder.forward()
        # 生成 __result = []
        code_builder.add_line('{} = []'.format(self.result_var))
        # 解析模版
        self._parse_text()

        self.flush_buffer()
        # 生成 return "".join(__result)
        code_builder.add_line('return "".join({})'.format(self.result_var))
        code_builder.backward()

    def _parse_text(self):
        """解析模版"""
        tokens = self.re_tokens.split(self.raw_text)
        handlers = (
            (self.re_variable.match, self._handle_variable),   # {{ variable }}
            (self.re_tag.match, self._handle_tag),             # {% tag %}
            (self.re_comment.match, self._handle_comment),     # {# comment #}
        )
        default_handler = self._handle_string

        for token in tokens:
            for match, handler in handlers:
                if match(token):
                    handler(token)
                    break
            else:
                default_handler(token)

    def _handle_variable(self, token):
        """处理变量"""
        variable = token.strip('{} ')
        self.buffered.append('str({})'.format(variable))

    def _handle_comment(self, token):
        """处理注释"""
        pass

    def _handle_string(self, token):
        """处理字符串"""
        self.buffered.append('{}'.format(repr(token)))

    def _handle_tag(self, token):
        """处理标签"""
        # 将前面解析的字符串,变量写入到 code_builder 中
        # 因为标签生成的代码需要新起一行
        self.flush_buffer()
        tag = token.strip('{%} ')
        tag_name = tag.split()[0]
        self._handle_statement(tag, tag_name)

    def _handle_statement(self, tag, tag_name):
        """处理 if/for"""
        if tag_name in ('if', 'elif', 'else', 'for'):
            # elif 和 else 之前需要向后缩进一步
            if tag_name in ('elif', 'else'):
                self.code_builder.backward()
            # if True:, elif True:, else:, for xx in yy:
            self.code_builder.add_line('{}:'.format(tag))
            # if/for 表达式部分结束,向前缩进一步,为下一行做准备
            self.code_builder.forward()
        elif tag_name in ('break',):
            self.code_builder.add_line(tag)
        elif tag_name in ('endif', 'endfor'):
            # if/for 结束,向后缩进一步
            self.code_builder.backward()

    def flush_buffer(self):
        # 生成类似代码: __result.extend(['<h1>', name, '</h1>'])
        line = '{0}.extend([{1}])'.format(
            self.result_var, ','.join(self.buffered)
        )
        self.code_builder.add_line(line)
        self.buffered = []

    def render(self, context=None):
        """渲染模版"""
        namespace = {}
        namespace.update(self.default_context)
        if context:
            namespace.update(context)
        exec(str(self.code_builder), namespace)
        result = namespace[self.func_name]()
        return result

if __name__ == "__main__":
    template = Template('<h1>{{ title }} {{ 1+10 }}</h1><p>{% for item in items %}\n{{ item }}{% endfor %}</p>')
    template.code_builder
    print template.render({'title': 'Python',"items":["xiaorui.cc","rfyiamcool","github.com/rfyiamcool"]})

END.


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

4 Responses

  1. 2016年4月14日 / 上午6:37

    谢谢分享,雨痕有ppt么

  2. 皓禹 2016年4月13日 / 下午4:57

    还是没有加我啊

  3. 张少志 2016年4月13日 / 上午10:09

    牛逼呀

  4. orangleliu 2016年4月13日 / 上午10:05

    牛呀 我看完还是不会写

皓禹进行回复 取消回复

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