美文网首页
Bottle SimpleTemplate 源码分析

Bottle SimpleTemplate 源码分析

作者: Yucz | 来源:发表于2020-10-31 09:41 被阅读0次

    简书上为首发,拒绝抄袭,原文地址 https://www.jianshu.com/p/a4a50c0b7ea7

    Bottle Template

    1 Bottle SimpleTemplate 的简单使用场景
    • 简单的花括号

      >>> tpl = SimpleTemplate('Hello {{name}}!')
      >>> tpl.render(name='World')
      u'Hello World!'
      
    • 带有 Python 语法关键字的语句

      >>> template('Hello {{name.title() if name else "stranger"}}!', name=None)
      u'Hello stranger!'
      >>> template('Hello {{name.title() if name else "stranger"}}!', name='mArC')
      u'Hello Marc!'
      
    源码分析

    BaseTemplate 基类

    • 构造函数中传递进来的有两个参数
      • template: 直接传递字符串,保存到
      • filename: 传递模板文件的名字,将从该文件中读取所有内容
    • 构造函数中,读取完 template 之后,就进行 self.parse, 将在子类中进行
    class BaseTemplate(object):
        def __init__(self, template='', filename='<template>'):
            self.source = filename
            if self.source != '<template>':
                fp = open(filename)
                template = fp.read()
                fp.close()
            self.parse(template)
        def parse(self, template): raise NotImplementedError
        def render(self, **args): raise NotImplementedError
        @classmethod
        def find(cls, name):
            files = [path % name for path in TEMPLATE_PATH if os.path.isfile(path % name)]
            if files:
                return cls(filename = files[0])
            else:
                raise TemplateError('Template not found: %s' % repr(name))
    

    SimpleTemplate

    class SimpleTemplate(BaseTemplate):
    
        re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|while|with|def|class)|(include.*)|(end.*)|(.*))')
        re_inline = re.compile(r'\{\{(.*?)\}\}')
        dedent_keywords = ('elif', 'else', 'except', 'finally')
    
        def parse(self, template):
            indent = 0
            strbuffer = []
            code = []
            self.subtemplates = {}
            class PyStmt(str):
                def __repr__(self): return 'str(' + self + ')'
                
            def flush():
                # 将缓存的 strbuffer 字符拼接起来,添加到 code 中
                # 这样有多行普通字符串,在执行编译代码中时,也只需要执行一条语句,节省时间
                if len(strbuffer):
                    code.append(" " * indent + "stdout.append(%s)" % repr(''.join(strbuffer)))
                    code.append("\n" * len(strbuffer)) # to preserve line numbers 
                    del strbuffer[:]
            for line in template.splitlines(True):
                m = self.re_python.match(line)
                # 包含 % 加 python 关键字的字符串
                if m:
                    # 先跳过,下面再分析
                else: 
                    # 先跳过,下面再分析
            flush()
            self.co = compile("".join(code), self.source, 'exec')
    
        def render(self, **args):
            ''' Returns the rendered template using keyword arguments as local variables. '''
            args['stdout'] = []
            args['_subtemplates'] = self.subtemplates
            eval(self.co, args, globals())
            return ''.join(args['stdout'])
    

    先看下 parse 函数的流程

    第一种简单场景的函数流程
    >>> tpl = SimpleTemplate('Hello {{name}}!')
    >>> tpl.render(name='World')
    u'Hello World!'
    

    当传递进来的 templateHello {{name}}!,不会匹配到 re_python 这个正则表达式,所以将会执行下面的代码

    if m:
        pass
    else:
        splits = self.re_inline.split(line) # text, (expr, text)*
        if len(splits) == 1: # 不包含 {{}} 的场景
            strbuffer.append(line)
        else:
            flush()
            for i in xrange(1, len(splits), 2):
                splits[i] = PyStmt(splits[i])
            code.append(" " * indent + "stdout.extend(%s)\n" % repr(splits))
    

    先对正则表达式 \{\{(.*?)\}\} 作一个简单的分析

    • 匹配以 {{ 开头, 和}}结束的字符串

    • 中间的 (.*?) 表示惰性匹配,将匹配最少的结果,如果我们传递进来的是 {{name}}}(右边有三个}) ,那么只会匹配 {{name}}, 不会把最后一个 } 给匹配上

      #贪婪匹配和惰性匹配
      
      # 惰性匹配不会匹配到 }
      In [4]: re_inline = re.compile(r'\{\{(.*?)\}\}')
      In [5]: m = re_inline.match("{{name}}}")
      In [6]: m.groups()
      Out[6]: ('name',)
      
      # 贪婪匹配会匹配到 }
      In [7]: re_inline2 = re.compile("\{\{(.*)\}\}")
      In [10]: m2 = re_inline2.match("{{name}}}")
      In [11]: m2.groups()
      Out[11]: ('name}',)
      
      

    再看下xrange(1, len(splits), 2)的用途,

    In [1]: import re
    
    In [2]: re_inline = re.compile(r'\{\{(.*?)\}\}')
    
    In [3]: re_inline.split("name")
    Out[3]: ['name']
    
    In [4]: re_inline.split("{{name}}")
    Out[4]: ['', 'name', '']
    # name 和 age 字段index为1, 3 即 range(1, 5, 2)
    In [5]: re_inline.split("My name is {{name}}. I am {{age}} years old.")
    Out[5]: ['My name is ', 'name', '. I am ', 'age', ' years old.']
    
    In [6]: range(1, 5, 2)
    Out[6]: [1, 3]
    

    如果字段是我们用花括号包起来的话,会调用splits[i] = PyStmt(splits[i])进行处理

    class PyStmt(str):
        def __repr__(self): return 'str(' + self + ')'
    

    假设我们传递进来的字符串为

    My name is {{name}}. I am {{age}} years old.

    经过下面函数处理后

    code.append(" " * indent + "stdout.extend(%s)\n" % repr(splits))
    

    code 的值如下

    ["stdout.extend(['My name is ', str(name), '. I am ', str(age), ' years old.'])\n"]
    

    接着会调用 compile 函数,将其编译,等到需要渲染的时候,再执行这段编译好的函数

    self.co = compile("".join(code), self.source, 'exec')
    

    接着再看下render函数

    def render(self, **args):
        ''' Returns the rendered template using keyword arguments as local variables. '''
        args['stdout'] = []
        args['_subtemplates'] = self.subtemplates
        eval(self.co, args, globals())
        return ''.join(args['stdout'])
    

    可以看出 code 中的 stdout 其实就是个 [], 再调用渲染时,其实就是通过 eval 函数执行我们编译后的 self.co, 渲染好的字符串通过 args 传递进去

    下面对整个流程做个分析

    In [2]: code = ["stdout.extend(['My name is ', str(name), '. I am ', str(age), years old.'])\n"]
    
    In [3]: co = compile("".join(code), "template", 'exec')
    
    In [4]: args = dict()
    
    In [5]: args["stdout"] = []
    
    In [6]: args["name"] = "messi"
    
    In [7]: args["age"] = 32
    
    In [8]: eval(co, args, globals())
    
    In [9]: args["stdout"]
    Out[9]: ['My name is ', 'messi', '. I am ', '32', ' years old.']
    
    In [10]: ''.join(args['stdout'])
    Out[10]: 'My name is messi. I am 32 years old.'
    

    至此,我们就知道 bottletemplate 的实现了,大体流程图如下
    简书不支持 mermaid,先贴上截图

    Screenshot from 2020-10-31 09-40-37.png
    graph TB
    A[遍历template中的一行] --> B(判断是否有符合正则的行数)
    B --> |有| C(转成python语句,再添加到code中)
    B --> |没有| D(将字符串原样添加进code)
    C --> E(对code代码执行compile)
    D --> E
    E --> F(eval执行编译的代码)
    F --> G(读取编译后的输出字符串)
    
    包含Python语法的字符串

    先看下正则表达式

    r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|while|with|def|class)|(include.*)|(end.*)|(.*))'
    

    分析如下

    1. 以0个或者多个 \s(空字符) 开头,后面接着一个 % ,后面在接着 0个或者多个\s字符
    2. 第一个大括号 ?:(n) 为非捕获括号,表示匹配任何其后紧接指定字符串 n 的字符串,所以这个大组并没有编号,当我们后面匹配时m.groups()[0] 并不是这个大括号
    3. 再对大括号里面的四个括号进行分析。四个括号之间使用 | 隔开,说明只要一行字符串中,有包含其中一个字段即可匹配
      1. 第一个括号,即第一组,匹配 python 关键字if|elif|else|try|except|finally|for|while|with|def|class
      2. 第二个括号,即第二组,匹配include.*
      3. 第三个括号,即第三组,匹配 end.*
      4. 第四个括号,即第四组,匹配 (.*)

    做个简单的测试

    In [8]: re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for
       ...: |while|with|def|class)|(include.*)|(end.*)|(.*))')
    
    In [9]: s = "%message = 'Hello world!'" # 符合第四组 .*
    
    In [10]: m = re_python.match(s)
    
    In [11]: m.groups()
    Out[11]: (None, None, None, "message = 'Hello world!'")
    
    In [12]: s2 = "    %for item in items:" # 符合第一组 %for
    
    In [13]: m2 = re_python.match(s2)
    
    In [14]: m2.groups()
    Out[14]: ('for', None, None, None)
    
    In [15]: s3 = "      %end" # 符合第三组
    
    In [16]: m3 = re_python.match(s3)
    
    In [17]: m3.groups()
    Out[17]: (None, None, 'end', None)
    
    In [18]: s4 = "    %include test.tpl" # 符合第二组
    
    In [19]: m4 = re_python.match(s4)
    
    In [20]: m4.groups()
    Out[20]: (None, 'include test.tpl', None, None)
    
    

    再看下 parse函数中, 对符合 re_python字符串的处理

    m = self.re_python.match(line)
    if m:
        flush()
        # 根据匹配后的 groups 区分
        keyword, include, end, statement = m.groups()
        if keyword:
            # 如果是 ('elif', 'else', 'except', 'finally'), 则需要退格,保证python语法的正确性
            if keyword in self.dedent_keywords:
                indent -= 1
            code.append(" " * indent + line[m.start(1):])
            indent += 1
        elif include:
            # 添加子模板
            tmp = line[m.end(2):].strip().split(None, 1)
            name = tmp[0]
            args = tmp[1:] and tmp[1] or ''
            self.subtemplates[name] = SimpleTemplate.find(name)
            code.append(" " * indent + "stdout.append(_subtemplates[%s].render(%s))\n" % (repr(name), args))
        elif end:
            # 结束循环时,将 indent -1, 保证语法正确性
            indent -= 1
            code.append(" " * indent + '#' + line[m.start(3):])
        elif statement:
            # 一般的 python 语句和注释
            code.append(" " * indent + line[m.start(4):])
    

    假设我们输入的 template为如下字符串

    %message = 'Hello world!'
    <html>
      <head>
        <title>{{title.title()}}</title>
      </head>
      <body>
        <h1>{{title.title()}}</h1>
        <p>{{message}}</p>
        <p>Items in list: {{len(items)}}</p>
        <ul>
        %for item in items:
          <li>
          %if isinstance(item, int):
            Zahl: {{item}}
          %else:
            %try:
              Other type: ({{type(item).__name__}}) {{repr(item)}}
            %except:
              Error: Item has no string representation.
            %end try-block (yes, you may add comments here)
          %end
          </li>
        %end
        </ul>
      </body>
    </html>
    

    调用函数如下, content为上面的文本内容

    template(content, title='Template Test', items=[1,2,3,'fly'])

    则处理完后的 code 如下

    ["message = 'Hello world!'\n",
     "stdout.append('<html>\\n  <head>\\n')",
     '\n\n',
     "stdout.extend(['    <title>', str(title.title()), '</title>\\n'])\n",
     "stdout.append('  </head>\\n  <body>\\n')",
     '\n\n',
     "stdout.extend(['    <h1>', str(title.title()), '</h1>\\n'])\n",
     "stdout.extend(['    <p>', str(message), '</p>\\n'])\n",
     "stdout.extend(['    <p>Items in list: ', str(len(items)), '</p>\\n'])\n",
     "stdout.append('    <ul>\\n')",
     '\n',
     'for item in items:\n',
     " stdout.append('      <li>\\n')",
     '\n',
     ' if isinstance(item, int):\n',
     "  stdout.extend(['        Zahl: ', str(item), '\\n'])\n",
     ' else:\n',
     '  try:\n',
     "   stdout.extend(['          Other type: (', str(type(item).__name__), ') ', str(repr(item)), '\\n'])\n",
     '  except:\n',
     "   stdout.append('          Error: Item has no string representation.\\n')",
     '\n',
     '  #end try-block (yes, you may add comments here)\n',
     ' #end\n',
     " stdout.append('      </li>\\n')",
     '\n',
     '#end\n']
    

    通过 eval 将逐行执行上面的每一条语句,将输出的内容保存到 stdout中,完成对模板的渲染。

    最终渲染完成的字符串如下

    <html>
      <head>
        <title>Template Test</title>
      </head>
      <body>
        <h1>Template Test</h1>
        <p>Hello world!</p>
        <p>Items in list: 4</p>
        <ul>
          <li>
            Zahl: 1
          </li>
          <li>
            Zahl: 2
          </li>
          <li>
            Zahl: 3
          </li>
          <li>
              Other type: (str) 'fly'
          </li>
        </ul>
      </body>
    </html>
    
    

    相关文章

      网友评论

          本文标题:Bottle SimpleTemplate 源码分析

          本文链接:https://www.haomeiwen.com/subject/azuxmktx.html