python实现简易模板引擎
本文章翻译自A-template-engine.
该项目的作者为Ned Batchelder,他是一位经验丰富的软件工程师,是coverage.py(一个代码覆盖度检测工具)以及很多开源库的作者,同时也是一位享誉盛名的教育家。在这个项目中,他很短短几百行,实现了一个功能齐全的模板引擎。
什么是模板引擎
模板引擎(特指用于web开发的模板引擎),是为了使用户界面与业务数据分离而产生的,用于网站的模板引擎会根据传入的参数生成标准HTML文档。
通俗点讲,模板引擎就是动态生成HTML文件的东西,以博客网站为例,每个用户看到的界面内容可能都是不一样的,但大体框架是相同的,如果这些包含内容的HTML文件全是以静态形式储存在服务器的,那么当网站用户足够多的时候,服务器将会不堪重负。而模板引擎就是一个能根据用户动态生成HTML文件的东西。
初步探索
假设我们想要生成以下HTML内容:
<p>Welcome, Charlie!</p>
<p>Products:</p>
<ul>
<li>Apple: $1.00</li>
<li>Fig: $1.50</li>
<li>Pomegranate: $3.25</li>
</ul>
在这里,用户的名字(这里是 Charlie)和商品以及商品价格都是动态生成的。那么我们如何使用python生成这些内容呢?我们可以把要生成的html当成一个字符串,而其中动态生成的部分使用参数传入:
PAGE_HTML = """
<p>Welcome, {name}!</p>
<p>Products:</p>
<ul>
{products}
</ul>
"""
# The HTML for each product displayed.
PRODUCT_HTML = "<li>{prodname}: {price}</li>\n"
def make_page(username, products):
product_html = ""
for prodname, price in products:
product_html += PRODUCT_HTML.format(
prodname=prodname, price=format_price(price))
html = PAGE_HTML.format(name=username, products=product_html)
return html
这是可行的,但这样的方法很容易造成混乱,尤其是当网页很大结构复杂时,维护困难。
使用模板引擎
为了避免出现上述问题,我们可以使用模板引擎动态生成页面。也就是说,网站的一个页面对应一个html模板,其中动态部分的数据会使用特殊的语法表示,我们使用python处理这些语法。以上面的例子,一个模板引擎应该如下:
<p>Welcome, !</p>
<p>Products:</p>
<ul>
</ul>
比起我们实现的第一个版本,这个版本大部分都是html代码,大括号表示动态生成数据的部分。
支持的语法
编写模板之前,我们要编写的模板支持以下语法:
-
变量:
<p>Welcome, {{user_name}}!</p>
两个大括号之间的值表示变量,模板生成html时应该将其作为参数传入。
-
通过点号访问字典值,对象属性以及对象方法:
<p>The price is: {{product.price}}, with a {{product.discount}}% discount.</p>
比如上述price和discount都是product的属性
-
使用函数修改值:
<p>Short name: {{story.subject|slugify|lower}}</p>
其中管道连接的字符串表示为调用函数修改变量值。
-
支持if语句和for语句:
{% if user.is_logged_in %} <p>Welcome, {{ user.name }}!</p> {% endif %}
if语句可用来判断用户是否登录。
<p>Products:</p> <ul> {% for product in product_list %} <li>{{ product.name }}: {{ product.price|format_price }}</li> {% endfor %} </ul>
for语句用于循环遍历
-
支持注释:
{# This is the best template ever! #}
实现方法
如何实现模板引擎呢?一个常用的方法是读入模板,然后将模板生成python代码,执行这些代码时,就会生成我们想要的结果。以上面的模板为例:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
我们的引擎将会生成类似以下的代码:
def render_function(context, do_dots):
c_user_name = context['user_name']
c_product_list = context['product_list']
c_format_price = context['format_price']
result = []
append_result = result.append
extend_result = result.extend
to_str = str
extend_result([
'<p>Welcome, ',
to_str(c_user_name),
'!</p>\n<p>Products:</p>\n<ul>\n'
])
for c_product in c_product_list:
extend_result([
'\n <li>',
to_str(do_dots(c_product, 'name')),
':\n ',
to_str(c_format_price(do_dots(c_product, 'price'))),
'</li>\n'
])
append_result('\n</ul>\n')
return ''.join(result)
每一个模板都会转化成一个render_function函数,这个函数context参数是一个字典,
函数的开头三行是读入参数的过程,每个context数据值被读到以’c_‘前缀开头的局部变量中,使用这样的前缀是为了避免与后面的变量发生变量名重复。
result列表是一个字符串列表,是我们最后要返回的数据,也就是生成html文件所需要的内容。使用类似下面风格的代码:
append_result = result.append
extend_result = result.extend
to_str = str
是为了将函数存储在局部变量里,减少函数调用所需要的时间,当需要大量调用该函数时,可以有效的提高代码运行的效率。
后面我们通过不断调用append_result和extend_result函数生成result,最后返回它的字符串形式。
开始动手
模板类
我们的模板引擎的核心是模板类,我们的目标是可以使用一个字符串以及参数组成的字典来初始化它,同时调用它的render方法就能得到渲染结果:
# Make a Templite object.
templite = Templite('''
<h1>Hello !</h1>
''',
{'upper': str.upper},
)
# Later, use it to render some data.
text = templite.render({
'name': "Ned",
'topics': ['Python', 'Geometry', 'Juggling'],
})
下面是模板类的具体实现过程,为了使代码模块化,我们再定义另一个类:CodeBuilder。
CodeBuilder类的实现
该类主要是为了方便我们创建生成python代码,它可以添加多行代码,处理缩进,最后还可以返回编译后的python代码:
class CodeBuilder(object):
"""Build source code conveniently."""
def __init__(self, indent=0):
self.code = []
self.indent_level = indent
add_line
函数添加一个新行代码,同时注意缩进的处理:
def add_line(self, line):
"""Add a line of source to the code.
Indentation and newline will be added for you, don't provide them.
"""
self.code.extend([" " * self.indent_level, line, "\n"])
indent
和dedent
函数增大和减少缩进:
INDENT_STEP = 4
def indent(self):
"""Increase the current indent for following lines."""
self.indent_level += self.INDENT_STEP
def dedent(self):
"""Decrease the current indent for following lines."""
self.indent_level -= self.INDENT_STEP
add_section
函数返回另一个CodeBuilder对象,这样当我们想在特定的位置留出空间时,调用该方法即可。
def add_section(self):
"""Add a section, a sub-CodeBuilder."""
section = CodeBuilder(self.indent_level)
self.code.append(section)
return section
__str__
连接self.code
产生单个字符串,注意到self.code
可能包括其他CodeBuilder
对象,遇到这种情况时,它将会递归调用该方法。
get_globals
方法通过执行生成代码返回一个字典,包含变量名和变量的值。
def get_globals(self):
"""Execute the code, and return a dict of globals it defines."""
# A check that the caller really finished all the blocks they started.
assert self.indent_level == 0
# Get the Python source as a single string.
python_source = str(self)
# Execute the source, defining globals, and return them.
global_namespace = {}
exec(python_source, global_namespace)
return global_namespace
Templite类的具体实现
将一个模板成功编译成python代码的大部分过程会在这个类中实行。首先是初始化函数:
def __init__(self, text, *contexts):
"""Construct a Templite with the given `text`.
`contexts` are dictionaries of values to use for future renderings.
These are good for filters and global values.
"""
self.context = {}
for context in contexts:
self.context.update(context)
我们创建两个集合用于存放变量:
self.all_vars = set()
self.loop_vars = set()
然后就是调用CodeBuilder
生成代码的部分了:
code = CodeBuilder()
code.add_line("def render_function(context, do_dots):")
code.indent()
# 留出空间,因为我们还不知道要有多少变量
vars_code = code.add_section()
code.add_line("result = []")
code.add_line("append_result = result.append")
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")
以上是比较简单的部分,无论模板如何,这些代码总会被生成。接下来我们定义一个内部函数帮助我们缓存输出的字符串:
buffered = []
def flush_output():
"""Force `buffered` to the code builder."""
if len(buffered) == 1:
code.add_line("append_result(%s)" % buffered[0])
elif len(buffered) > 1:
code.add_line("extend_result([%s])" % ", ".join(buffered))
del buffered[:]
buffered
列表存放还没有被写到我们要生成的函数的源代码里面,在模板引擎处理模板的时候,我们将会添加字符串到buffered
,在遇到控制流语句时,flush_output
函数将会把字符串添加到函数源代码。比如,当我们执行(假设buffered
为空):
buffered.append("'hello'")
这时候执行flush_output
函数,下面这个语句
append_result('hello')
将会被添加到源代码。
回到我们的Templite类,当我们处理控制结构时,我们应该判断他们的语法是否符合我们的模板,ops_stack
是一个用来处理这些情况的堆栈。
ops_stack = []
当我们遇到控制语句,比如``时,就将if
出栈,如果没有if
,就可以返回错误。利用ops_stack
我们可以很快的检查错误。
接下来就是真正的解析语句了,我们将用到以下正则表达式:
tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
关于正则表达式的用法,这里就不赘述了,有需要可以自己查看文档。
对于以下字符串:
<p>Topics for {{name}}: {% for t in topics %}{{t}}, {% endfor %}</p>
若将其作为text
传入,将会被分成:
[
'<p>Topics for ', # literal
'{{name}}', # expression
': ', # literal
'{% for t in topics %}', # tag
'', # literal (empty)
'{{t}}', # expression
', ', # literal
'{% endfor %}', # tag
'</p>' # literal
]
文本被分割成列表后,我们就可以对它进行遍历:
for token in tokens:
处理注释的情况,直接忽略:
if token.startswith('{#'):
# Comment: ignore it and move on.
continue
处理{{...}}
之类的语句:
elif token.startswith('{{'):
# An expression to evaluate.
expr = self._expr_code(token[2:-2].strip())
buffered.append("to_str(%s)" % expr)
上面用到的_expr_code
方法会将一个模板表达式转换成python表达式,我们在后面定义它。
处理控制流语句{%....%}
:
elif token.startswith('{%'):
# Action tag: split into words and parse further.
flush_output()
words = token[2:-2].strip().split()
针对if
语句处理:
if words[0] == 'if':
# An if statement: evaluate the expression to determine if.
if len(words) != 2:
self._syntax_error("Don't understand if", token)
ops_stack.append('if')
code.add_line("if %s:" % self._expr_code(words[1]))
code.indent()
针对for
语句处理:
elif words[0] == 'for':
# A loop: iterate over expression result.
if len(words) != 4 or words[2] != 'in':
self._syntax_error("Don't understand for", token)
ops_stack.append('for')
self._variable(words[1], self.loop_vars)
code.add_line(
"for c_%s in %s:" % (
words[1],
self._expr_code(words[3])
)
)
code.indent()
我们首先检查了语法的正确性,然后调用_variable
方法检查变量语法,然后添加到我们提供的集合。
处理结束语句比如{% endif %}
或者{% endfor %}
:
elif words[0].startswith('end'):
# Endsomething. Pop the ops stack.
if len(words) != 1:
self._syntax_error("Don't understand end", token)
end_what = words[0][3:]
if not ops_stack:
self._syntax_error("Too many ends", token)
start_what = ops_stack.pop()
if start_what != end_what:
self._syntax_error("Mismatched end tag", end_what)
code.dedent()
错误处理,当遇到非if
,for
以及end
语句时:
else:
self._syntax_error("Don't understand tag", words[0])
最后一种情况,处理正常字符串(非模板语句):
else:
# Literal content. If it isn't empty, output it.
if token:
buffered.append(repr(token))
遍历结束后,所有模板都已经被处理,但别忘了,我们还有一个ops_stack
,此时它应该为空:
if ops_stack:
self._syntax_error("Unmatched action tag", ops_stack[-1])
flush_output()
现在,我们可以回过头来处理刚刚空下的vars_code
区域了,模板所有的变量被存放在all_vars
中,循环使用的变量存放在loop_vars
中:
for var_name in self.all_vars - self.loop_vars:
vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
至此,解析工作算是完全完成了,最后补上函数最后一行:
code.add_line("return ''.join(result)")
code.dedent()
定义渲染函数,它由CodeBuilder
对象的get_globals
方法获得:
self._render_function = code.get_globals()['render_function']
接下来定义_expr_code
方法,它将模板中的变量,表达式转化成python的表达式,比如:
{{user_name}}
或者
{{user.name.localized|upper|escape}}
将会被处理成python的表达方式。
def _expr_code(self, expr):
"""Generate a Python expression for `expr`."""
if "|" in expr:
pipes = expr.split("|")
code = self._expr_code(pipes[0])
for func in pipes[1:]:
self._variable(func, self.all_vars)
code = "c_%s(%s)" % (func, code)
elif "." in expr:
dots = expr.split(".")
code = self._expr_code(dots[0])
args = ", ".join(repr(d) for d in dots[1:])
code = "do_dots(%s, %s)" % (code, args)
else:
self._variable(expr, self.all_vars)
code = "c_%s" % expr
return code
自此,模板引擎的核心部分已基本完成,下面是一些辅助函数。
一些辅助函数
def _syntax_error(self, msg, thing):
"""Raise a syntax error using `msg`, and showing `thing`."""
raise TempliteSyntaxError("%s: %r" % (msg, thing))
def _variable(self, name, vars_set):
"""Track that `name` is used as a variable.
Adds the name to `vars_set`, a set of variable names.
Raises an syntax error if `name` is not a valid name.
"""
if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
self._syntax_error("Not a valid name", name)
vars_set.add(name)
渲染函数
def render(self, context=None):
"""Render this template by applying it to `context`.
`context` is a dictionary of values to use in this rendering.
"""
# Make the complete context we'll use.
render_context = dict(self.context)
if context:
render_context.update(context)
return self._render_function(render_context, self._do_dots)
_do_dots
函数:
def _do_dots(self, value, *dots):
"""Evaluate dotted expressions at runtime."""
for dot in dots:
try:
value = getattr(value, dot)
except AttributeError:
value = value[dot]
if callable(value):
value = value()
return value
全部代码请访问:项目源码
更多细节可以关注:A-template-engine.