专栏名称: Python开发者
人生苦短,我用 Python。伯乐在线旗下账号「Python开发者」分享 Python 相关的技术文章、工具资源、精选课程、热点资讯等。
目录
相关文章推荐
Python开发者  ·  OpenAI ... ·  昨天  
Python爱好者社区  ·  梁文锋和杨植麟,论文撞车了!! ·  3 天前  
Python爱好者社区  ·  《Machine Learning ... ·  3 天前  
Python爱好者社区  ·  DeepSeek彻底爆了! ·  4 天前  
51好读  ›  专栏  ›  Python开发者

让我们一起来构建一个模板引擎(一)

Python开发者  · 公众号  · Python  · 2017-02-04 21:07

正文

(点击 上方蓝字 ,快速关注我们)


来源:伯乐在线专栏作者 - mozillazg

如有好文章投稿,请点击 → 这里了解详情

如需转载,发送「转载」二字查看说明


假设我们要生成下面这样的 html 字符串:


welcome, Tom

  • age: 20

  • weight: 100

  • height: 170


要求姓名以及 中的内容是根据变量动态生成的,也就是这样的:


welcome, {name}

    {info}


没接触过模板的同学可能会想到使用字符串格式化的方式来实现:


HTML = '''

welcome, {name}

    {info}

'''

def gen _ html ( person ) :

name = person [ 'name' ]

info _ list = [

'

  • {0}: {1}
  • ' . format ( item , value )

    for item , value in person [ 'info' ]. items ()

    ]

    info = '\n' . join ( info _ list )

    return HTML . format ( name = name , info = info )


    这种方案有一个很明显的问题那就是,需要拼接两个 html 片段。 使用过模板技术的同学应该很容易就想到,在 Web 开发中生成 HTML 的更常用的办法是使用模板:


    HTML = '''

    welcome, {{ person['

    name '] }}

      {% for item, value in person[' info '].items() %}

    • {{ item }}: {{ value }}
    • {% endfor %}

    '''

    def gen _ html ( person ) :

    return Template ( HTML ). render ({ 'person' : person })


    本系列文章要讲的就是如何从零开始实现一个这样的模板引擎( Template )。


    使用技术


    我们将使用将模板编译为 python 代码的方式来解析和渲染模板。 比如上面的模板将被编译为如下 python 代码:


    def render _ function () :

    result = []

    result . extend ([

    '

    \n' ,

    '

    welcome, '

    str ( person [ 'name' ]),

    '

    \n' ,

    '

      \n'

    ])

    for item , value in person [ 'info' ]. items () :

    result . extend ([

    '

  • '
  • ,

    str ( item ),

    ': ' ,

    str ( value ),

    '\n'

    ])

    result . extend ([

    '\n'

    '

    \n'

    ])

    return '' . join ( result )


    然后通过 exec 执行生成的代码,之后再执行 render_function() 就可以得到我们需要的 html 字符串了:


    namespace = { 'person' : person }

    exec ( code , namespace )

    render_function = namespace [ 'render_function' ]

    html = render_function ()


    模板引擎的核心技术就是这些了,下面让我们一步一步的实现它吧。


    CodeBuilder


    我们都知道 python 代码是高度依赖缩进的,所以我们需要一个对象用来保存我们生成代码时的当前缩进情况, 同时也保存已经生成的代码行(可以直接在 github 上下载 template1a.py ):


    # -*- coding : utf - 8 -*-

    # tested on Python 3 . 5 . 1

    class CodeBuilder :

    INDENT _ STEP = 4 # 每次缩进的空格数

    def __ init __( self , indent = 0 ) :

    self . indent = indent # 当前缩进

    self . lines = [] # 保存一行一行生成的代码

    def forward ( self ) :

    """缩进前进一步"""

    self . indent += self . INDENT _ STEP

    def backward ( self ) :

    """缩进后退一步"""

    self . indent -= self . INDENT _ STEP

    def add ( self , code ) :

    self . lines . append ( code )

    def add _ line ( self , code ) :

    self . lines . append ( ' ' * self . indent + code )

    def __ str __( self ) :

    """拼接所有代码行后的源码"""

    return '\n' . join ( map ( str , self . lines ))

    def __ repr __( self ) :

    """方便调试"""

    return str ( self )


    forward 和 backward 方法可以用来控制缩进前进或后退一步,比如在生成 if 语句的时候:


    if age > 13 : # 生成完这一行以后,需要切换缩进了 ``forward()``

    ...

    ... # 退出 if 语句主体的时候,同样需要切换一次缩进 ``backward()``

    ...


    Template


    这个模板引擎的核心部分就是一个 Template 类,用法:


    # 实例化一个 Template 对象

    template = Template ( '''

    hello, {{ name }}

    {% for skill in skills %}

    you are good at {{ skill }}.

    {% endfor %}

    ''' )

    # 然后,使用一些数据来渲染这个模板

    html = template . render (

    { 'name' : 'Eric' , 'skills' : [ 'python' , 'english' , 'music' , 'comic' ]}

    )


    一切魔法都在 Template 类里。下面我们写一个基本的 Template 类(可以直接在 github 上下载 template1b.py ):


    class Template :

    def __ init __( self , raw _ text , indent = 0 , default _ context = None ,

    func_name = '__func_name' , result_var = '__result' ) :

    self . raw _ text = raw _ text

    self . default _ context = default _ context or {}

    self . func _ name = func _ name

    self . result _ var = result _ var

    self . code _ builder = code _ builder = CodeBuilder ( indent = indent )

    self . buffered = []

    # 生成 def __ func _ name () :

    code _ builder . add _ line ( 'def {}():' . format ( self . func _ name ))

    code _ builder . forward ()

    # 生成 __ result = []

    code _ builder . add _ line ( '{} = []' . format ( self . result _ var ))

    self ._ parse _ text ()

    self . flush _ buffer ()

    # 生成 return "" . join (__ result )

    code _ builder . add _ line ( 'return "".join({})' . format ( self . result _ var ))

    code _ builder . backward ()

    def _ parse _ text ( self ) :

    pass

    def flush _ buffer ( self ) :

    # 生成类似代码 : __ result . extend ([ '

    '

    , name , '' ])

    line = '{0}.extend([{1}])' . format (

    self . result _ var , ',' . join ( self . buffered )

    )

    self . code _ builder . add _ line ( line )

    self . buffered = []

    def render ( self , context = None ) :

    namespace = {}

    namespace . update ( self . default _ context )

    if context :

    namespace . update ( context )

    exec ( str ( self . code _ builder ), namespace )

    result = namespace [ self . func _ name ]()

    return result


    以上就是 Template 类的核心方法了。我们之后要做的就是实现和完善 _parse_text 方法。 当模板字符串为空时生成的代码如下:


    >>> import template1b

    >>> template = template1b . Template ( '' )

    >>> template . code_builder

    def __func_name () :

    __result = []

    __result . extend ([])

    return "" . join ( __result )


    可以看到跟上面[使用技术]那节所说生成的代码是类似的。下面我们就一起来实现这个 _parse_text 方法。


    变量


    首先要实现是对变量的支持,模板语法是 {{ variable }} 。 既然要支持变量,首先要做的就是把变量从模板中找出来,这里我们可以使用正则表达式来实现:


    re_variable = re . compile ( r '\{\{ .*? \}\}' )

    >>> re_variable = re . compile ( r '\{\{ .*? \}\}' )

    >>> re_variable . findall ( '

    {{ title }}

    ' )

    [ '{{ title }}' ]

    >>>


    知道了如何匹配变量语法,下面我们要把变量跟其他的模板字符串分割开来,这里还是用的 re:


    >> re _ variable = re . compile ( r '(\{\{ .*? \}\})' )

    >>> re_variable.split('

    {{ title }} ')

    ['

    ', '{{ title }}', ' ' ]


    这里的正则之所以加了个分组是因为我们同时还需要用到模板里的变量。 分割开来以后我们就可以对每一项进行解析了。支持 {{ variable }} 语法的 Template 类增加了如下代码 (可以直接在 github 上下载 template1c.py ):


    class Template :

    def __ init __( self , raw _ text , indent = 0 , default _ context = None ,

    func_name = '__func_name' , result_var = '__result' ) :

    # ...

    self . buffered = []

    self . re _ variable = re . compile ( r '\{\{ .*? \}\}' )

    self . re _ tokens = re . compile ( r '(\{\{ .*? \}\})' )

    # 生成 def __ func _ name () :

    code _ builder . add _ line ( 'def {}():' . format ( self . func _ name ))

    # ...

    def _ parse _ text ( self ) :

    tokens = self . re _ tokens . split ( self . raw _ text )

    for token in tokens :

    if self . re _ variable . match ( token ) :

    variable = token . strip ( '{} ' )

    self . buffered . append ( 'str({})' . format ( variable ))

    else :

    self . buffered . append ( '{}' . format ( repr ( token )))


    _parse_text 中之所以要用 repr ,是因为此时需要把 token 当成一个普通的字符串来处理, 同时需要考虑 token 中包含 " 和 ' 的情况。 下面是几种有问题的写法:


    • 'str({})'.format(token): 这种是把 token 当成变量来用了,生成的代码为 str(token)







    请到「今天看啥」查看全文