模板引擎是wap開發(fā)的一大利器,方便我們生成復(fù)雜的動態(tài)頁面。我們學(xué)習(xí)一下如何用Python實現(xiàn)一個模板引擎
一秸架、目標(biāo)分析
首先小腊,我們需要明白我們的模板引擎需要干什么救鲤。觀察下方的HTML,其中不僅包含原生的HTML元素秩冈,還包含一些其它的標(biāo)簽({{ }}本缠、{%%})。開發(fā)者通過使用這些符號入问,實現(xiàn)將動態(tài)的數(shù)據(jù)片段嵌入其中丹锹。這些符號在很多模板引擎中都是差不多的。
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>{{ obj.title }}</title>
</head>
<body>
{# Hello world #}
<div class="content">
<p> {{ obj.content }} </p>
<table>
{% for value in obj.list %}
<td> {{ value }} </td>
{% endfor %}
</table>
{% if obj.show %}
<p> {{ obj.foot }} </p>
{% endif %}
</div>
</body>
</html>
而我們的模板引擎就是負(fù)責(zé)將這些靜態(tài)模板結(jié)合上下文來生成一個純HTML的字符串芬失。模板引擎的任務(wù)就是翻譯模板楣黍,使用上下文中提供的動態(tài)數(shù)據(jù)替換這些代碼片段。
舉個栗子棱烂,如果我們提供如下的上下文給模板引擎去翻譯上面的HTML頁面
context = {"obj": {"title":"這是標(biāo)題","content":"這是內(nèi)容","foot":"這是頁腳","list":["td1","td2","td3","td5"],"show":"True"}}
那么它的翻譯結(jié)果應(yīng)該是這樣
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>這是標(biāo)題</title>
</head>
<body>
<div class="content">
<p> 這是內(nèi)容 </p>
<table>
<td> td1 </td>
<td> td2 </td>
<td> td3 </td>
<td> td5 </td>
</table>
<p> 這是頁腳 </p>
</div>
</body>
</html>
二租漂、實現(xiàn)方法
那么問題就來了,我們怎么讓我們的Python去認(rèn)識颊糜,分析這些片段呢哩治?
有兩個主要的選擇:一是向Django的模板引擎那樣,通過解析HTML產(chǎn)生一個數(shù)據(jù)結(jié)構(gòu)表示模板的結(jié)構(gòu)衬鱼,然后遍歷那個數(shù)據(jù)結(jié)構(gòu)业筏,基于找到的指令裝配結(jié)果文本,二是將原始HTML解析成某種形式的可直接執(zhí)行的代碼,然后執(zhí)行代碼就能產(chǎn)生結(jié)果鸟赫。
我們將要實現(xiàn)的引擎就是將模板編譯成Python代碼蒜胖。只需要提供所需的上下文并執(zhí)行編譯后的代碼,即可獲得所需的渲染后的HTML惯疙。
先觀察一下將上面的HTML編譯后的Python代碼
def render_function(context, do_dots):
c_obj = context['obj']
result = []
append_result = result.append
extend_result = result.extend
to_str = str
extend_result(["\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset='utf-8'>\n <title>",to_str(do_dots(c_obj, 'title')),'</title>\n</head>\n<body>\n ','\n <div class="content">\n <p> ',to_str(do_dots(c_obj, 'content')),' </p>\n \n <table>\n '])
for c_value in do_dots(c_obj, 'list'):
extend_result(['\n <td> ',to_str(c_value),' </td>\n '])
append_result('\n </table>\n \n ')
if do_dots(c_obj, 'show'):
extend_result(['\n <p> ',to_str(do_dots(c_obj, 'foot')),' </p>\n '])
append_result(' \n </div>\n</body>\n</html>\n')
return ''.join(result)
觀察一下上面的Python代碼翠勉,我們的模板引擎將HTML編譯成了一個函數(shù)。執(zhí)行函數(shù)霉颠,就能生成我們所需要的完整HTML了对碌。
三、模板引擎
現(xiàn)在我們的目標(biāo)明確了蒿偎。就是去解析HTML元素朽们,編譯成一個Python函數(shù)怀读。
先做點簡單的,我們需要構(gòu)造一個方法用于生成格式化的Python代碼骑脱,最主要的是管理Python中的縮進和換行
class CodeBuilder(object):
"""一個簡單的 Python 代碼生成器"""
def __init__(self, indent=0):
# CodeBuilder生成的所有代碼片段都存儲在該列表中
self.code = []
# 生成的代碼的縮進等級
self.indent_level = indent
def __str__(self):
"""返回完整的Python代碼"""
return "".join(str(c) for c in self.code)
def add_line(self, line):
"""將代碼行添加到生成器中"""
# 構(gòu)造器主要為我們做了三個工作
# 1菜枷、縮進
# 2、原始代碼的添加
# 3叁丧、換行處理
self.code.extend([" "*self.indent_level, line, "\n"])
def add_section(self):
"""添加一個代碼組"""
section = CodeBuilder(self.indent_level)
self.code.append(section)
return section
INDENT_STEP = 4
def indent(self):
"""增加縮進"""
self.indent_level += self.INDENT_STEP
def dedent(self):
"""減小縮進"""
self.indent_level -= self.INDENT_STEP
def get_globals(self):
"""執(zhí)行代碼啤誊,并且返回它定義的全局變量字典"""
assert self.indent_level == 0
python_source = str(self)
global_namespace = {}
exec(python_source, global_namespace)
return global_namespace
有了上面的代碼生成器,我們可以很方便的添加Python代碼了拥娄。
# 初始化一個構(gòu)造器
code = CodeBuilder()
# 添加函數(shù)名和參數(shù)
code.add_line("def render_function(context, do_dots):")
# 添加一個縮進
code.indent()
# 繼續(xù)添加后面的代碼
code.add_line("result = []")
code.add_line("append_result = result.append")
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")
未完待續(xù)...
我們的最核心模板類:
class Templite(object):
def __init__(self, text, *contexts):
# 存儲所有的上下文
self.context = {}
for context in contexts:
self.context.update(context)
self.all_vars = set()
self.loop_vars = set()
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():
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[:]
ops_stack = []
# 使用正則表達式分割數(shù)據(jù)
tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
for token in tokens:
if token.startswith("{#"):
# 模板中的注釋部分
continue
elif token.startswith("{{"):
# 處理模板中的變量部分
expr = self._expr_code(token[2:-2].strip())
buffered.append("to_str(%s)" % expr)
elif token.startswith("{%"):
flush_output()
words = token[2:-2].strip().split()
if words[0] == '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()
elif words[0] == 'for':
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()
elif words[0].startswith('end'):
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()
else:
self._syntax_error("Don't understand tag", words[0])
else:
if token:
buffered.append(repr(token))
if ops_stack:
self._syntax_error("Unmatched action tag", ops_stack[-1])
flush_output()
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()
print(str(code))
self._render_function = code.get_globals()['render_function']
def _expr_code(self, expr):
"""將expr轉(zhuǎn)換成Python表達式"""
if "|" in expr:
# 表達式中有過濾管道
pipes = expr.split("|")
code = self._expr_code(pipes[0])
# 取出所有的過濾函數(shù)蚊锹,依次生成執(zhí)行語句
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 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_context = dict(self.context)
if context:
render_context.update(context)
return self._render_function(render_context, self._do_dots)
def _do_dots(self, value, *dots):
for dot in dots:
try:
value = getattr(value, dot)
except AttributeError:
value = value[dot]
if callable(value):
value = value()
return value
以上的內(nèi)容主要參考了500lines這個項目中的template-engine部分,關(guān)于更詳細的內(nèi)容分析可以閱讀下方的參考文章稚瘾。同時也極力推薦500lines這個項目牡昆,使用最精簡的代碼讓我們能學(xué)習(xí)很多有意思的東西。
參考文章
源碼地址