模板引擎是wap開發的一大利器,方便我們生成復雜的動態頁面。我們學習一下如何用Python實現一個模板引擎
一、目標分析
首先,我們需要明白我們的模板引擎需要干什么。觀察下方的HTML,其中不僅包含原生的HTML元素,還包含一些其它的標簽({{ }}、{%%})。開發者通過使用這些符號,實現將動態的數據片段嵌入其中。這些符號在很多模板引擎中都是差不多的。
<!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>
而我們的模板引擎就是負責將這些靜態模板結合上下文來生成一個純HTML的字符串。模板引擎的任務就是翻譯模板,使用上下文中提供的動態數據替換這些代碼片段。
舉個栗子,如果我們提供如下的上下文給模板引擎去翻譯上面的HTML頁面
context = {"obj": {"title":"這是標題","content":"這是內容","foot":"這是頁腳","list":["td1","td2","td3","td5"],"show":"True"}}
那么它的翻譯結果應該是這樣
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>這是標題</title>
</head>
<body>
<div class="content">
<p> 這是內容 </p>
<table>
<td> td1 </td>
<td> td2 </td>
<td> td3 </td>
<td> td5 </td>
</table>
<p> 這是頁腳 </p>
</div>
</body>
</html>
二、實現方法
那么問題就來了,我們怎么讓我們的Python去認識,分析這些片段呢?
有兩個主要的選擇:一是向Django的模板引擎那樣,通過解析HTML產生一個數據結構表示模板的結構,然后遍歷那個數據結構,基于找到的指令裝配結果文本,二是將原始HTML解析成某種形式的可直接執行的代碼,然后執行代碼就能產生結果。
我們將要實現的引擎就是將模板編譯成Python代碼。只需要提供所需的上下文并執行編譯后的代碼,即可獲得所需的渲染后的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編譯成了一個函數。執行函數,就能生成我們所需要的完整HTML了。
三、模板引擎
現在我們的目標明確了。就是去解析HTML元素,編譯成一個Python函數。
先做點簡單的,我們需要構造一個方法用于生成格式化的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):
"""將代碼行添加到生成器中"""
# 構造器主要為我們做了三個工作
# 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):
"""執行代碼,并且返回它定義的全局變量字典"""
assert self.indent_level == 0
python_source = str(self)
global_namespace = {}
exec(python_source, global_namespace)
return global_namespace
有了上面的代碼生成器,我們可以很方便的添加Python代碼了。
# 初始化一個構造器
code = CodeBuilder()
# 添加函數名和參數
code.add_line("def render_function(context, do_dots):")
# 添加一個縮進
code.indent()
# 繼續添加后面的代碼
code.add_line("result = []")
code.add_line("append_result = result.append")
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")
未完待續...
我們的最核心模板類:
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 = []
# 使用正則表達式分割數據
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轉換成Python表達式"""
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 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
以上的內容主要參考了500lines這個項目中的template-engine部分,關于更詳細的內容分析可以閱讀下方的參考文章。同時也極力推薦500lines這個項目,使用最精簡的代碼讓我們能學習很多有意思的東西。
參考文章
源碼地址