【Python】300行代码搞定HTML模板渲染

一、前言
模板语言由HTML代码和逻辑控制代码组成,此处@PHP。通过模板语言可以快速的生成预想的HTML页面。应该算是后端渲染不可缺少的组成部分。
二、功能介绍
通过使用学习tornado、bottle的模板语言,我也效仿着实现可以独立使用的模板渲染的代码模块,模板语法来自tornado和bottle的语法。可以用来做一些简单的网页渲染,邮件内容生成等HTML显示方面。以下就是简单的语法使用介绍。
1.变量。使用{{ }}包裹起来,里面的变量为Python传入。模板渲染时会将传入的变量转换成字符串并填入对应位置。

# 模板文件内容 {{my_title}} - 锐客网 # py代码调用t_html 为上面的内容 Template(t_html).render(my_title="标题", session = some_obj)

【【Python】300行代码搞定HTML模板渲染】2. 转义。默认传入的数据都会进行HTML转义,可以使用{% raw value %}来将value的内容按原始字符串输出。
# 模板文件内容{% raw value %}
# Py调用内容 Template(t_html).render(my_title="")

3. 条件控制。支持Python的if,elif,else。条件代码需要放在{% %}内部,并且在条件结束后需要额外增加{% end %},用于标识条件控制语句块范围。
# 模板文件内容 {% if a > 1%} {% else %} {% end %} # py调用 Template(t_html).render(a=1)

4. 循环控制。支持Python的for和while。与条件控制一样也需要放在{% %}内部,并且结束处需要额外增加{% end %},用于标识循环控制语句块的范围。
# 模板文件内容 {% for i in range(10) %} {% end %} # py调用 Template(t_html).render()

5. 变量声明。如果需要在模板文件内声明一个变量,方便使用时,可以通过set来实现。具体格式为{% set v = xx %}。通过set声明的变量整个模板文件中都可以使用,包括在条件控制和循环控制中作为条件判断也可以。
# 模板文件内容 {% set a = 1 %}

三、源码
这个模板语言模块是在Python2.7上开发使用的,如果要在Python3+上使用需要对str和bytes进行一些处理即可,由于没有引用任何其他模块,可以很好的独立使用。
1 # -*- coding:utf-8 -*- 2 3 """ 模板语言""" 4 5 # TOKEN相关的定义 6 TOKEN_S_BRACE = "{" 7 TOKEN_S_BLOCK = "%" 8 TOKEN_EXPRESSION_L = "{{" 9 TOKEN_EXPRESSION_R = "}}" 10 TOKEN_BLOCK_L = "{%" 11 TOKEN_BLOCK_R = "%}" 12 TOKEN_KEY_SET = "set" 13 TOKEN_KEY_RAW = "raw" 14 TOKEN_KEY_IF = "if" 15 TOKEN_KEY_ELIF = "elif" 16 TOKEN_KEY_ELSE = "else" 17 TOKEN_KEY_FOR = "for" 18 TOKEN_KEY_WHILE = "while" 19 TOKEN_KEY_END = "end" 20 TOKEN_KEY_BREAK = "break" 21 TOKEN_KEY_CONTINUE = "continue" 22 TOKEN_SPACE = " " 23 TOKEN_COLON = ":" 24 # Token标记 {{}} {% %} 25 TOKEN_FLAG_SET = {TOKEN_S_BRACE, TOKEN_S_BLOCK} 26 # 简单的语句 27 TOKEN_KEY_SET_SIMPLE_EXPRESSION = {TOKEN_KEY_SET, TOKEN_KEY_RAW} 28 # 前置条件 29 TOKEN_KEY_PRE_CONDITION = { 30# end 必须在if/elif/else/for/while 后面 31TOKEN_KEY_END: {TOKEN_KEY_IF, TOKEN_KEY_ELIF, TOKEN_KEY_ELSE, 32TOKEN_KEY_FOR, TOKEN_KEY_WHILE}, 33# elif 必须在if 后面 34TOKEN_KEY_ELIF: {TOKEN_KEY_IF}, 35# else 必须在if/elif 后面 36TOKEN_KEY_ELSE: {TOKEN_KEY_IF, TOKEN_KEY_ELIF, TOKEN_KEY_FOR, TOKEN_KEY_WHILE}, 37 } 38 # 循环语句 39 TOKEN_KEY_LOOP = {TOKEN_KEY_WHILE, TOKEN_KEY_FOR} 40 # 循环的控制break continue 41 TOKEN_KEY_LOOP_CTRL = {TOKEN_KEY_BREAK, TOKEN_KEY_CONTINUE} 42 43 class ParseException(Exception): 44pass 45 46 class TemplateCode(object): 47def __init__(self): 48self.codeTrees = {"parent": None, "nodes": []} 49self.cursor = self.codeTrees 50self.compiled_code = None 51 52def create_code(self): 53"""创建一个代码子块""" 54child_codes = {"parent": self.cursor, "nodes": []} 55self.cursor["nodes"].append(child_codes) 56self.cursor = child_codes 57 58def close_code(self): 59""" 关闭一个代码子块 """ 60assert self.cursor["parent"] is not None, "overflow" 61self.cursor = self.cursor["parent"] 62 63def append_text(self, text): 64""" 添加文本 """ 65# 排除空行 66self.cursor["nodes"].append("_add(%r)" % text) 67 68def append_express(self, express, raw=False): 69""" 表达式 """ 70if raw: 71temp_exp = "_t_exp = _str_(%s)" % express 72else: 73temp_exp = "_t_exp = _esc_(%s)" % express 74self.cursor["nodes"].append(temp_exp) 75self.cursor["nodes"].append("_add(_t_exp)") 76 77def append_statement(self, statement): 78""" 语句 """ 79temp_statement = "%s" % statement 80self.cursor["nodes"].append(temp_statement) 81 82def reset(self): 83self.codeTrees = {"parent": None, "nodes": []} 84self.cursor = self.codeTrees 85self.compiled_code = None 86 87def build_code(self, filename): 88temp_code_buff = [] 89self.write_buff_with_indent(temp_code_buff, "def _template_render():", 0) 90self.write_buff_with_indent(temp_code_buff, "_codes = []", 4) 91self.write_buff_with_indent(temp_code_buff, "_add = _codes.append", 4) 92self.write_codes(temp_code_buff, self.codeTrees, 4) 93self.write_buff_with_indent(temp_code_buff, "return ''.join(_codes)", 4) 94temp_code = "".join(temp_code_buff) 95self.compiled_code = compile(temp_code,filename, "exec", dont_inherit=True) 96 97def write_codes(self, code_buff, codes, indent): 98for node in codes.get("nodes", []): 99if isinstance(node, dict): 100self.write_codes(code_buff, node, indent+4) 101else: 102self.write_buff_with_indent(code_buff, node, indent) 103 104def generate(self, **kwargs): 105temp_namespace = {} 106temp_namespace['_str_'] = self.to_utf8 107temp_namespace['_esc_'] = self.to_safe_utf8 108temp_namespace.update(kwargs) 109exec(self.compiled_code, temp_namespace) 110return temp_namespace['_template_render']() 111 112@staticmethod 113def write_buff_with_indent(code_buff, raw_str, indent): 114"""""" 115temp = (" " * indent) + raw_str + "\n" 116code_buff.append(temp) 117 118@staticmethod 119def to_utf8(raw_str): 120""" 转换 """ 121if isinstance(raw_str, str): 122return raw_str 123elif isinstance(raw_str, bytes): 124return raw_str.decode() 125return str(raw_str) 126 127@staticmethod 128def to_safe_utf8(raw_str): 129""" 过滤html转义 """ 130text = TemplateCode.to_utf8(raw_str) 131return text.replace("&", "& ").replace("<", "< ").replace(">", "> ") 132 class Template(object): 133"""模板类""" 134def __init__(self, input_obj,filename="", **namespace): 135"""模板初始化""" 136self.namespace = {} 137self.namespace.update(namespace) 138# 将数据丢进去解析生成编译代码 139self.lexer = TemplateLexer(input_obj, filename) 140 141def render(self, **kwargs): 142"""渲染模板 """ 143temp_name_space = {} 144temp_name_space.update(self.namespace) 145temp_name_space.update(kwargs) 146# 执行渲染 147return self.lexer.render(**kwargs) 148 149 class TemplateLexer(object): 150"""模板语法分析器 """ 151def __init__(self, input_obb, filename=""): 152if hasattr(input_obb, "read"): 153self.raw_string = input_obb.read() 154else: 155self.raw_string = input_obb 156self.filename = filename 157# 记录当前的位置 158self.pos = 0 159# 记录原始数据的总长度 160self.raw_str_len = len(self.raw_string) 161# 记录解析的数据 162self.code_data = https://www.it610.com/article/TemplateCode() 163# 开始解析 164self.parse_template() 165 166def match(self, keyword, pos=None): 167return self.raw_string.find(keyword, pos if pos is not None else self.pos) 168 169def cut(self, size=-1): 170"""剪取数据 size切割数据的大小,-1表示全部""" 171if size == -1: 172new_pos = self.raw_str_len 173else: 174new_pos = self.pos + size 175s = self.raw_string[self.pos: new_pos] 176self.pos = new_pos 177return s 178 179def remaining(self): 180"""获取剩余大小 """ 181return self.raw_str_len - self.pos 182 183def function_brace(self): 184""" 获取{{/ {% """ 185skip_index = self.pos 186while True: 187index = self.match(TOKEN_S_BRACE, skip_index)# {% {{ 188# 没找到 189if index == -1: 190return None, -1 191# 末尾 192if index >= self.raw_str_len: 193return None, -1 194# 匹配类型 195next_value = https://www.it610.com/article/self.raw_string[index + 1:index + 2] 196if next_value not in TOKEN_FLAG_SET: 197skip_index = index + 1 198# 说明不是关键类型 199continue 200brace = self.raw_string[index: index + 2] 201return brace, index 202return None, -1 203 204def read_content_with_token(self, index, begin_token, end_token): 205""" 206读取匹配token的内容 207""" 208end_index = self.match(end_token) 209if end_index == -1: 210return ParseException("{0} missing end token {1}".format(begin_token, end_token)) 211# 过滤 begin_token 212self.pos = index + len(begin_token) 213content = self.cut(end_index - self.pos) 214# 去除末尾 end_token 215self.cut(len(end_token)) 216return content 217 218def add_simple_block_statement(self, operator, suffix): 219if not suffix: 220raise ParseException("{0} missing content".format(operator)) 221if operator == TOKEN_KEY_SET: 222self.code_data.append_statement(suffix) 223elif operator == TOKEN_KEY_RAW: 224self.code_data.append_express(suffix, True) 225else: 226raise ParseException("{0} is undefined".format(operator)) 227 228def parse_template(self): 229"""解析模板 """ 230# TODO 检查模板文件是否更改过,如果没有则不需要重新解析 231self.code_data.reset() 232# 解析模板原文件 233self.__parse() 234# 生成编译code 235self.__compiled_code() 236 237def render(self, **kwargs): 238return self.code_data.generate(**kwargs) 239 240def __parse(self, control_operator=None, in_loop=False): 241"""开始解析""" 242while True: 243if self.remaining() <= 0: 244if control_operator or in_loop: 245raise ParseException("%s missing {%% end %%}" % control_operator) 246break 247# 读取 {{ {% 248brace, index = self.function_brace() 249# 说明没有找到 250if not brace: 251text = self.cut(index) 252self.code_data.append_text(text) 253continue 254else: 255text = self.cut(index - self.pos) 256if text: 257self.code_data.append_text(text) 258 259if brace == TOKEN_EXPRESSION_L: 260content = self.read_content_with_token(index, TOKEN_EXPRESSION_L, TOKEN_EXPRESSION_R).strip() 261if not content: 262raise ParseException("Empty Express") 263self.code_data.append_express(content) 264continue 265elif brace == TOKEN_BLOCK_L: 266content = self.read_content_with_token(index, TOKEN_BLOCK_L, TOKEN_BLOCK_R).strip() 267if not content: 268raise ParseException("Empty block") 269 270# 得到表达式 for x in x ; if x ; elif x ; else ; end ; set ; while x ; 271operator, _, suffix = content.partition(TOKEN_SPACE) 272if not operator: 273raise ParseException("block missing operator") 274 275suffix = suffix.strip() 276# 简单语句,set / raw 277if operator in TOKEN_KEY_SET_SIMPLE_EXPRESSION: 278self.add_simple_block_statement(operator, suffix) 279elif operator in TOKEN_KEY_LOOP_CTRL: 280if not in_loop: 281raise ParseException("{0} must in loop block".format(operator)) 282self.code_data.append_statement(operator) 283else: 284# 控制语句 检查匹配if 后面可以跟elif/else 285pre_condition = TOKEN_KEY_PRE_CONDITION.get(operator, None) 286if pre_condition: 287# 里面就是elif/else/end 288if control_operator not in pre_condition: 289raise ParseException("{0} must behind with {1}".format(operator, pre_condition)) 290elif operator == TOKEN_KEY_END: 291# 遇到{% end %}则结束 292self.code_data.close_code() 293return 294else: 295# 由于是依据if 进入 来计算elif ,因此elif与if是同级的 296self.code_data.close_code() 297self.code_data.append_statement(content + TOKEN_COLON) 298self.code_data.create_code() 299self.__parse(operator, in_loop or (operator in TOKEN_KEY_LOOP)) 300break 301# 添加控制语句及内部语句体 if for while 302self.code_data.append_statement(content + TOKEN_COLON) 303self.code_data.create_code() 304self.__parse(operator, in_loop or (operator in TOKEN_KEY_LOOP)) 305else: 306raise ParseException("Unkown brace") 307return 308 309def __compiled_code(self): 310"""生成 编译code """ 311self.code_data.build_code(self.filename) 312 if __name__ == "__main__": 313t = Template("{{hello}}") 314t.render(hello="你好"


    推荐阅读