假设咱们要生成下面这样的 html 字符串:html
<div> <p>welcome, Tom</p> <ul> <li>age: 20</li> <li>weight: 100</li> <li>height: 170</li> </ul> </div>
要求姓名以及 <ul></ul>
中的内容是根据变量动态生成的,也就是这样的:python
<div> <p>welcome, {name}</p> <ul> {info} </ul> </div>
没接触过模板的同窗可能会想到使用字符串格式化的方式来实现:git
HTML = ''' <div> <p>welcome, {name}</p> <ul> {info} </ul> </div> ''' def gen_html(person): name = person['name'] info_list = [ '<li>{0}: {1}</li>'.format(item, value) for item, value in person['info'].items() ] info = '\n'.join(info_list) return HTML.format(name=name, info=info)
这种方案有一个很明显的问题那就是,须要拼接两个 html 片断。 使用过模板技术的同窗应该很容易就想到,在 Web 开发中生成 HTML 的更经常使用的办法是使用模板:github
HTML = ''' <div> <p>welcome, {{ person['name'] }}</p> <ul> {% for item, value in person['info'].items() %} <li>{{ item }}: {{ value }}</li> {% endfor %} </ul> </div> ''' def gen_html(person): return Template(HTML).render({'person': person})
本系列文章要讲的就是如何从零开始实现一个这样的模板引擎( Template
)。正则表达式
咱们将使用将模板编译为 python 代码的方式来解析和渲染模板。 好比上面的模板将被编译为以下 python 代码:app
def render_function(): result = [] result.extend([ '<div>\n', '<p>welcome, ' str(person['name']), '</p>\n', '<ul>\n' ]) for item, value in person['info'].items(): result.extend([ '<li>', str(item), ': ', str(value), '</li>\n' ]) result.extend([ '</ul>\n' '</div>\n' ]) return ''.join(result)
而后经过 exec
执行生成的代码,以后再执行 render_function()
就能够获得咱们须要的 html 字符串了:ui
namespace = {'person': person} exec(code, namespace) render_function = namespace['render_function'] html = render_function()
模板引擎的核心技术就是这些了,下面让咱们一步一步的实现它吧。spa
咱们都知道 python 代码是高度依赖缩进的,因此咱们须要一个对象用来保存咱们生成代码时的当前缩进状况, 同时也保存已经生成的代码行(能够直接在 github 上下载 template1a.py ):调试
# -*- coding: utf-8 -*- # tested on Python 3.5.1 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) def __str__(self): """拼接全部代码行后的源码""" return '\n'.join(map(str, self.lines)) def __repr__(self): """方便调试""" return str(self)
forward
和 backward
方法能够用来控制缩进前进或后退一步,好比在生成 if
语句的时候:code
if age > 13: # 生成完这一行之后,须要切换缩进了 ``forward()`` ... ... # 退出 if 语句主体的时候,一样须要切换一次缩进 ``backward()`` ...
这个模板引擎的核心部分就是一个 Template
类,用法:
# 实例化一个 Template 对象 template = Template(''' <h1>hello, {{ name }}</h1> {% for skill in skills %} <p>you are good at {{ skill }}.</p> {% endfor %} ''') # 而后,使用一些数据来渲染这个模板 html = template.render( {'name': 'Eric', 'skills': ['python', 'english', 'music', 'comic']} )
一切魔法都在 Template
类里。下面咱们写一个基本的 Template
类(能够直接在 github 上下载 template1b.py ):
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 = [] # 生成 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): pass 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
以上就是 Template
类的核心方法了。咱们以后要作的就是实现和完善 _parse_text
方法。 当模板字符串为空时生成的代码以下:
>>> import template1b >>> template = template1b.Template('') >>> template.code_builder def __func_name(): __result = [] __result.extend([]) return "".join(__result)
能够看到跟上面[使用技术]那节所说生成的代码是相似的。下面咱们就一块儿来实现这个 _parse_text
方法。
首先要实现是对变量的支持,模板语法是 {{ variable }}
。 既然要支持变量,首先要作的就是把变量从模板中找出来,这里咱们可使用正则表达式来实现:
re_variable = re.compile(r'\{\{ .*? \}\}') >>> re_variable = re.compile(r'\{\{ .*? \}\}') >>> re_variable.findall('<h1>{{ title }}</h1>') ['{{ title }}'] >>>
知道了如何匹配变量语法,下面咱们要把变量跟其余的模板字符串分割开来,这里仍是用的 re
:
>> re_variable = re.compile(r'(\{\{ .*? \}\})') >>> re_variable.split('<h1>{{ title }}</h1>') ['<h1>', '{{ title }}', '</h1>']
这里的正则之因此加了个分组是由于咱们同时还须要用到模板里的变量。 分割开来之后咱们就能够对每一项进行解析了。支持 {{ variable }}
语法的 Template
类增长了以下代码 (能够直接在 github 上下载 template1c.py ):
class Template: def __init__(self, raw_text, indent=0, default_context=None, func_name='__func_name', result_var='__result'): # ... self.buffered = [] self.re_variable = re.compile(r'\{\{ .*? \}\}') self.re_tokens = re.compile(r'(\{\{ .*? \}\})') # 生成 def __func_name(): code_builder.add_line('def {}():'.format(self.func_name)) # ... def _parse_text(self): tokens = self.re_tokens.split(self.raw_text) for token in tokens: if self.re_variable.match(token): variable = token.strip('{} ') self.buffered.append('str({})'.format(variable)) else: self.buffered.append('{}'.format(repr(token)))
_parse_text
中之因此要用 repr
,是由于此时须要把 token
当成一个普通的字符串来处理, 同时须要考虑 token
中包含 "
和 '
的状况。 下面是几种有问题的写法:
'str({})'.format(token)
: 这种是把 token
当成变量来用了,生成的代码为 str(token)
'"{}"'.format(token)
: 这种虽然是把 token
当成了字符串,可是会有转义的问题,当 token
中包含 "
时生成的代码为 ""hello""
下面先来看一下新的 template1c.py
生成了什么样的代码:
>>> from template1c import Template >>> template = Template('<h1>{{ title }}</h1>') >>> template.code_builder def __func_name(): __result = [] __result.extend(['<h1>',str(title),'</h1>']) return "".join(__result)
没问题,跟预期的是同样的。再来看一下 render
的效果:
>>> template.render({'title': 'Python'}) '<h1>Python</h1>'
不知道你有没有发现,其实 {{ variable }}
不仅支持变量,还支持表达式和运算符:
>>> Template('{{ 1 + 2 }}').render() '3' >>> Template('{{ items[0] }}').render({'items': [1, 2, 3]}) '1' >>> Template('{{ func() }}').render({'func': list}) '[]'
这个既能够说是个 BUG 也能够说是个特性?, 看模板引擎是否打算支持这些功能了, 咱们在这里是打算支持这些功能 ;)。
既然支持了 {{ }}
那么支持注释也就很是好实现了。
打算支持的注释模板语法是 {# comments #}
,有了上面实现 {{ variable }}
的经验,实现注释是相似的代码 (能够直接在 github 上下载 template1d.py ):
class Template: def __init__(self, raw_text, indent=0, default_context=None, func_name='__func_name', result_var='__result'): # ... self.buffered = [] self.re_variable = re.compile(r'\{\{ .*? \}\}') self.re_comment = re.compile(r'\{# .*? #\}') self.re_tokens = re.compile(r'''( (?:\{\{ .*? \}\}) |(?:\{\# .*? \#\}) )''', re.X) # 生成 def __func_name(): # ... def _parse_text(self): tokens = self.re_tokens.split(self.raw_text) for token in tokens: if self.re_variable.match(token): # ... # 注释 {# ... #} elif self.re_comment.match(token): continue else: # ...
效果:
>>> from template1d import Template >>> template = Template('<h1>{{ title }} {# comment #}</h1>') >>> template.code_builder def __func_name(): __result = [] __result.extend(['<h1>',str(title),' ','</h1>']) return "".join(__result) >>> template.render({'title': 'Python'}) '<h1>Python </h1>'
至此,咱们的模板引擎已经支持了变量和注释功能。 那么如何实现支持 if
语句和 for
循环的标签语法呢:
{% if user.is_admin %} admin, {{ user.name }} {% elif user.is_staff %} staff {% else %} others {% endif %} {% for name in names %} {{ name }} {% endfor %}
我将在 第二篇文章 中向你详细的讲解。敬请期待。