如何用python写个模板引擎

一.实现思路

  本文讲解如何使用python实现一个简单的模板引擎, 支持传入变量, 使用if判断和for循环语句, 最终能达到下面这样的效果:

渲染前的文本:
<h1>{{title}}</h1>
<p>十以内的奇数:</p>
<ul>
{% for i in range(10) %}
  {% if i%2==1 %}
    <li>{{i}}</li>
  {% end %}
{% end %}
</ul>

渲染后的文本,假设title="高等数学":
<h1>高等数学</h1>
<p>十以内的奇数:</p>
<ul>
<li>1</li>
<li>3</li>
<li>5</li>
<li>7</li>
<li>9</li>
</ul>

  要实现这样的效果, 第一步就应该将文本中的html代码和类似{% xxx %}这样的渲染语句分别提取出来, 使用下面的正则表达式可以做到:

re.split(r'(?s)({{.*?}}|{%.*?%}|{#.*?#})', html)

  用这个正则表达式处理刚才的文本, 结果如下:

  在提取文本之后, 就需要执行内部的逻辑了. python自带的exec函数可以执行字符串格式的代码:

exec('print("hello world")') # 这条语句会输出hello world

  因此, 提取到html的渲染语句之后, 可以把它改成python代码的格式, 然后使用exec函数去运行. 但是, exec函数不能返回代码的执行结果, 它只会返回None. 虽然如此, 我们可以使用下面的方式获取字符串代码中的变量:

global_namespace = {}
code = """
a = 1

def func():
  pass
"""
exec(code, global_namespace)
print(global_namespace) # {'a': 1, 'func': <function func at 0x00007fc61e3462a0>, '__builtins__': <module 'builtins' (built-in)>}

  因此, 我们只要在code这个字符串中定义一个函数, 让它能够返回渲染后的模板, 然后使用刚才的方式把这个函数从字符串中提取出来并执行, 就能得到结果了.

  基于上面的思路, 我们最终应该把html文本转化为下面这样的字符串:

# 这个函数不是我们写的, 是待渲染的html字符串转化过来的
def render(context: dict) -> str:
  result = []
  # 这一部分负责提取所有动态变量的值
  title = context['title']
  # 对于所有的html代码或者是变量, 直接放入result列表中
  result.extend(['<h1>', str(title), '</h1>\n<p>十以内的奇数:</p>\n<ul>\n'])
  # 对于模板中的for和if循环语句,则是转化为原生的python语句
  for i in range(10):
    if i % 2 == 1:
      result.extend(['\n    <li>', str(i), '</li>\n  '])
  result.append('\n</ul>')
  # 最后,让函数将result列表联结为字符串返回就行, 这样就得到了渲染好的html文本
  return ''.join(result)

  如何将html文本转化为上面这样的代码, 是这篇文章的关键. 上面的代码是由最开始那个html demo转化来的, 每一块我都做了注释. 如果没看明白的话, 就多看几遍, 不然肯定是看不懂下文的.

  总的来说, 要渲染一个模板, 思路如下:

二.字符串代码

  为了能够方便地生成python代码, 我们首先定义一个CodeBuilder类:

class CodeBuilder:
  INDENT_STEP = 4

  def __init__(self, indent_level: int = 0) -> None:
    self.indent_level = indent_level
    self.code = []
    self.global_namespace = None

  def start_func(self) -> None:
    self.add_line('def render(context: dict) -> str:')
    self.indent()
    self.add_line('result = []')
    self.add_line('append_result = result.append')
    self.add_line('extend_result = result.extend')
    self.add_line('to_str = str')

  def end_func(self) -> None:
    self.add_line("return ''.join(result)")
    self.dedent()

  def add_section(self) -> 'CodeBuilder':
    section = CodeBuilder(self.indent_level)
    self.code.append(section)
    return section

  def __str__(self) -> str:
    return ''.join(str(line) for line in self.code)

  def add_line(self, line: str) -> None:
    self.code.extend([' ' * self.indent_level + line + '\n'])

  def indent(self) -> None:
    self.indent_level += self.INDENT_STEP

  def dedent(self) -> None:
    self.indent_level -= self.INDENT_STEP

  def get_globals(self) -> dict:
    if self.global_namespace is None:
      self.global_namespace = {}
      python_source = str(self)
      exec(python_source, self.global_namespace)
    return self.global_namespace

  这个类作为字符串代码的容器使用, 它的本质是对字符串代码的封装, 在字符串的基础上增加了以下的功能:

代码缩进
  CodeBuilder维护了一个indent_level变量, 当调用它的add_line方法写入新代码的时候, 它会自动在代码开头加上缩进. 另外, 调用indent和dedent方法就能方便地增加和减少缩进.

生成函数
  由于定义这个类的目的就是在字符串里面写一个函数, 而这个函数的开头和结尾都是固定的, 所以把它直接写到对象的方法里面. 值得一提的是, 在start_func这个方法中, 我们写了这样三行代码:

append_result = result.append
extend_result = result.extend
to_str = str

  这样做是为了提高渲染模板的性能, 调用我们自己定义的函数, 需要的时间比调用result.append或者str等函数的时间少. 首先对于列表的append和extend两个方法来说, 每调用一次, python都需要在列表中的所有方法中找一次, 而直接把它绑定到我们自己定义的变量上, 就能避免python重复地去列表的方法中来找. 然后是str函数, 理论上, python查找局部变量的速度比查找内置变量的快, 因此我们使用一个局部变量to_str, python找到它的速度就比找str要快.

  上面这段话都是我从网上看到的, 实际测试了一下, 在python3.7上, 运行append_result需要的时间比直接调用result.append少了大约25%, to_str则没有明显的优化效果.

代码嵌套
  有的时候我们需要在一块代码中嵌套另外一块代码, 这时候可以调用add_section方法, 这个方法会创建一个新的CodeBuilder对象作为内容插入到原CodeBuilder对象里面, 这个和前端的div套div差不多.

  这个方法的好处是, 你可以在一个CodeBuilder对象中预先插入一个CodeBuilder对象而不用写入内容, 相当于先占着位置. 等条件成熟之后, 再回过头来写入内容. 这样就增加了字符串代码的可编辑性.

获取变量
  调用get_globals方法获取当前字符串代码内的所有全局变量.

三.Template模板

  在字符串代码的容器做好之后, 我们只需要解析html文本, 然后把它转化为python代码放到这个容器里面就行了. 因此, 我们定义如下的Template类:

class Template:
  html_regex = re.compile(r'(?s)({{.*?}}|{%.*?%}|{#.*?#})')
  valid_name_regex = re.compile(r'[_a-zA-Z][_a-zA-Z0-9]*$')

  def __init__(self, html: str, context: dict = None) -> None:
    self.context = context or {}
    self.code = CodeBuilder()
    self.all_vars = set()
    self.loop_vars = set()
    self.code.start_func()
    vars_code = self.code.add_section()
    buffered = []

    def flush_output() -> None:

      if len(buffered) == 1:
        self.code.add_line(f'append_result({buffered[0]})')
      elif len(buffered) > 1:
        self.code.add_line(f'extend_result([{", ".join(buffered)}])')
      del buffered[:]

    strings = re.split(self.html_regex, html)
    for string in strings:
      if string.startswith('{%'):
        flush_output()
        words = string[2:-2].strip().split()
        ops = words[0]
        if ops == 'if':
          if len(words) != 2:
            self._syntax_error("Don't understand if", string)
          self.code.add_line(f'if {words[1]}:')
          self.code.indent()
        elif ops == 'for':
          if len(words) != 4 or words[2] != 'in':
            self._syntax_error("Don't understand for", string)
          i = words[1]
          iter_obj = words[3]
          # 这里被迭代的对象可以是一个变量,也可以是列表,元组或者range之类的东西,因此使用_variable来检验
          try:
            self._variable(iter_obj, self.all_vars)
          except TemplateSyntaxError:
            pass
          self._variable(i, self.loop_vars)
          self.code.add_line(f'for {i} in {iter_obj}:')
          self.code.indent()
        elif ops == 'end':
          if len(words) != 1:
            self._syntax_error("Don't understand end", string)
          self.code.dedent()
        else:
          self._syntax_error("Don't understand tag", ops)
      elif string.startswith('{{'):
        expr = string[2:-2].strip()
        self._variable(expr, self.all_vars)
        buffered.append(f'to_str({expr})')
      else:
        if string.strip():
          # 这里使用repr把换行符什么的改成/n的形式,不然插到code字符串中会打乱排版
          buffered.append(repr(string))
    flush_output()
    for var_name in self.all_vars - self.loop_vars:
      vars_code.add_line(f'{var_name} = context["{var_name}"]')
    self.code.end_func()

  def _variable(self, name: str, vars_set: set) -> None:
    # 当解析html过程中出现变量,就调用这个函数
    # 一方面检验变量名是否合法,一方面记下变量名
    if not re.match(self.valid_name_regex, name):
      self._syntax_error('Not a valid name', name)
    vars_set.add(name)

  def _syntax_error(self, message: str, thing: str) -> None:
    raise TemplateSyntaxError(f'{message}: {thing}') # 这个Error类直接继承Exception就行

  def render(self, context=None) -> str:
    render_context = dict(self.context)
    if context:
      render_context.update(context)
    return self.code.get_globals()['render'](render_context)

  首先, 我们实例化了一个CodeBuilder对象作为容器使用. 在这之后, 我们定义了all_vars和loop_vars两个集合, 并在CodeBuilder生成的函数开头插了一个子容器. 这样做的目的是, 最终生成的函数应该在开头添加类似 var_name = context['var_name']之类的语句, 来提取传入的上下文变量的值. 但是, html中有哪些需要渲染的变量, 这是在渲染之后才知道的, 所以先在开头插入一个子容器, 并创建all_vars这个集合, 以便在渲染html之后把这些变量的赋值语句插进去. loop_vars则负责存放那些由于for循环产生的变量, 它们不需要从上下文中提取.

  然后, 我们创建一个bufferd列表. 由于在渲染html的过程中, 变量和html语句是不需要直接转为python语句的, 而是应该使用类似 append_result(xxx)这样的形式添加到代码中去, 所以这里使用一个bufferd列表储存变量和html语句, 等渲染到for循环等特殊语句时, 再调用flush_output一次性把这些东西全写入CodeBuilder中. 这样做的好处是, 最后生成的字符串代码可能会少几行.

  万事具备之后, 使用正则表达式分割html文本, 然后迭代分割结果并处理就行了. 对于不同类型的字符串, 使用下面的方式来处理:

html代码块
  只要有空格和换行符之外的内容, 就放入缓冲区, 等待统一写入代码

带的{{}}的变量
  只要变量合法, 就记录下变量名, 然后和html代码块同样方式处理

if条件判断 & for循环
  这两个处理方法差不多, 首先检查语法有无错误, 然后提取参数将其转化为python语句插入, 最后再增加缩进就行了. 其中for语句还需要记录使用的变量

end语句
  这条语句意味着for循环或者if判断结束, 因此减少CodeBuilder的缩进就行

  在解析完html文本之后, 清空bufferd的数据, 为字符串代码添加变量提取和函数返回值, 这样代码也就完成了.

四.结束

  最后, 实例化Template对象, 调用其render方法传入上下文, 就能得到渲染的模板了:

t = Template(html)
result = t.render({'title': '高等数学'})

以上就是如何用python写个模板引擎的详细内容,更多关于python写个模板引擎的资料请关注我们其它相关文章!

(0)

相关推荐

  • Python的Flask框架标配模板引擎Jinja2的使用教程

    Jinja2需要Python2.4以上的版本. 安装 按照Jinja有多种方式,你可以根据需要选择不同的按照方式. 使用easy_install 或pip: #sudo easy_install Jinja2 #sudo pip install Jinja2 这两个工具可以自动从网站上下载Jinja,并安装到python目录的site-packages目录中. 从tar包安装: # 下载Jinja的安装包 # 解压缩 # sudo python setup.py install 基本API用法

  • Python Web开发模板引擎优缺点总结

    做 Web 开发少不了要与模板引擎打交道.我陆续也接触了 Python 的不少模板引擎,感觉可以总结一下了. 一.首先按照我的熟悉程度列一下:pyTenjin:我在开发 Doodle 和 91 外教时使用.Tornado.template:我在开发知乎日报时使用.PyJade:我在开发知乎日报时接触过.Mako:我只在一个早期就夭折了的小项目里用过.Jinja2:我只拿它做过一些 demo. 其他就不提了,例如 Django 的模板,据说又慢又难用,我根本就没接触过. 二.再说性能 很多测试就是

  • 为Python的Tornado框架配置使用Jinja2模板引擎的方法

    tornado 默认有一个模板引擎但是功能简单(其实我能用到的都差不多)使用起来颇为麻烦, 而jinja2语法与django模板相似所以决定使用他. 下载jinja2 还是用pip 下载(用的真是爽) pip install jinja2 这样就可以使用了. tornado与jinja2 整合 tornado和jinja2整合起来很简单(其实是网上找的比较简单), 不知道从那里找到的反正找到了,不说了直接上代码 #coding:utf-8 import tornado.web from jinj

  • Python实现的简单模板引擎功能示例

    本文实例讲述了Python实现的简单模板引擎功能.分享给大家供大家参考,具体如下: #coding:utf- 8 __author__="sdm" __author_email='sdmzhu3@gmail.com' __date__ ="$2009-8-25 21:04:13$" '' ' pytpl 类似 php的模板类 '' ' import sys import StringIO import os.path import os #模 板的缓存 _tpl_c

  • Python的Flask框架中的Jinja2模板引擎学习教程

    Flask的模板功能是基于Jinja2模板引擎来实现的.模板文件存放在当前目前下的子目录templates(一定要使用这个名字)下. main.py 代码如下: from flask import Flask, render_template app = Flask(__name__) @app.route('/hello') @app.route('/hello/<name>') def hello(name=None): return render_template('hello.html

  • Python 模板引擎的注入问题分析

    这几年比较火的一个漏洞就是jinjia2之类的模板引擎的注入,通过注入模板引擎的一些特定的指令格式,比如 {{1+1}} 而返回了 2 得知漏洞存在.实际类似的问题在Python原生字符串中就存在,尤其是Python 3.6新增 f 字符串后,虽然利用还不明确,但是应该引起注意. 最原始的 % userdata = {"user" : "jdoe", "password" : "secret" } passwd = raw_i

  • 深入解析Python的Tornado框架中内置的模板引擎

    template中的_parse方法是模板文法的解析器,而这个文件中一坨一坨的各种node以及block,就是解析结果的承载者,也就是说在经过parse处理过后,我们输入的tornado的html模板就变成了各种block的集合. 这些block和node的祖宗就是这个"抽象"类, _Node,它定义了三个方法定义,其中generate方法是必须由子类提供实现的(所以我叫它"抽象"类).  理论上来说,当一个类成为祖宗类时,必定意味着这个类包含了一些在子类中通用的行

  • 如何用python写个模板引擎

    一.实现思路 本文讲解如何使用python实现一个简单的模板引擎, 支持传入变量, 使用if判断和for循环语句, 最终能达到下面这样的效果: 渲染前的文本: <h1>{{title}}</h1> <p>十以内的奇数:</p> <ul> {% for i in range(10) %} {% if i%2==1 %} <li>{{i}}</li> {% end %} {% end %} </ul> 渲染后的文本

  • 如何用Python写一个简单的通讯录

    目录 用Python写一个简单的通讯录 一.构思 1.定义空列表和一个空字典来存储 2.定义功能选项 3.添加通讯录功能 3.2 删除学员功能 二.整体项目演示 用Python写一个简单的通讯录 一.构思 1.定义空列表和一个空字典来存储 list1=[] #用于储存字典中的信息 dict1={} #用于储存联系人信息 2.定义功能选项 def Menu(): print('请选择功能--------\n' '1.添加学员\n' '2.删除学员\n' '3.修改学员\n' '4.查询学员\n'

  • 详解如何用Python写个听小说的爬虫

    目录 书名和章节列表 音频地址 下载 完整代码 总结 在路上发现好多人都喜欢用耳机听小说,同事居然可以一整天的带着一只耳机听小说.小编表示非常的震惊.今天就用 Python 下载听小说 tingchina.com的音频. 书名和章节列表 随机点开一本书,这个页面可以使用 BeautifulSoup 获取书名和所有单个章节音频的列表.复制浏览器的地址,如:https://www.tingchina.com/yousheng/disp_31086.htm. from bs4 import Beaut

  • 如何用python写一个简单的词法分析器

    编译原理老师要求写一个java的词法分析器,想了想决定用python写一个. 目标 能识别出变量,数字,运算符,界符和关键字,用excel表打印出来. 有了目标,想想要怎么实现词法分析器. 1.先进行预处理,把注释,多余的空格,空行去掉. 2.一行一行扫描,行里逐字扫描,把界符和运算符当做分割符,遇到就先停下开始判断. 若是以 英文字母.$.下划线开头,则可能是变量和关键字,在判断是关键字还是变量. 若是数字开头,则判断下一位是不是也是数字,直到遇到非数字停止,在把数字取出来. 再来判断分割符是

  • 在Yii框架中使用PHP模板引擎Twig的例子

    Twig是一款快速.安全.灵活的PHP模板引擎,它内置了许多filter和tags,并且支持模板继承,能让你用最简洁的代码来描述你的模板.他的语法和Python下的模板引擎Jinjia以及Django的模板语法都非常像. 比如我们在PHP中需要输出变量并且将其进行转义时,语法比较累赘: 复制代码 代码如下: <?php echo $var ?><?php echo htmlspecialchars(\$var, ENT_QUOTES, 'UTF-8') ?> 但是在Twig中可以这

  • Python之web模板应用

    Python的web模板,其实就是在HTML文档中使用控制语句和表达语句替换HTML文档中的变量来控制HTML的显示格式,Python的web模板可以更加灵活和方便的控制HTML的显示,而且大大地减少了编程人员的工作量. 模板语法: 1.控制语句{% ... %}:控制语句需要用{% end %}来作为此语句结束标志,通常用来作循环控制.条件控制.模块控制等,可以更加方便的控制HTML内容的显示: 2.表达语句{{ ... }}:一条表达语句就相当于一条Python语句,不需要结束语句,{{和}

  • 一分钟教你用Python写一幅春联

    目录 1. 前言 2. 代码中需要导入的模块 3. 下载字模 4. 下载龙凤呈祥背景底图 5. 生成春联 6. 测试样例 总结 1. 前言 春联是中国传统文化中最具内涵的元素之一,它以对仗工整.简洁精巧的文字描绘美好形象,抒发美好愿望,是中国特有的文学形式,是华人们过年的重要习俗.每逢春节期间,无论城市还是农村,家家户户都要精选一副大红春联贴于门上,辞旧迎新,以增加节日的喜庆气氛.据考证,这一习俗起于宋代,盛于明代.有据可查的最早的春联是“三阳始布,四序初开”,始见于莫高窟藏经洞出土的文物中,撰

随机推荐