[譯] 一個模板引擎是如何工作的?

ChandraProc 8年前發布 | 14K 次閱讀 Python Tornado Python開發

原文: How a template engine works

我已經使用模板引擎很長一段時間了,現在終于有時間來了解一下模板引擎是如何工作的。

概述

簡單地說,模板引擎是一個工具,你可以用它來進行涉及到很多的文本數據的編程任務。最常見的用法是Web應用程序中的HTML生成。尤其是在Python中,如果你想要使用一個模板引擎,那么現在我們有幾種選擇,例如 jinja 或者 mako 。在這里,我們要通過深入tornado web框架的template模塊,找出一個模板引擎是如何工作的,這是一個簡單的系統,這樣我們就可以專注于過程的基本思路。

進入實現細節之前,讓我們首先來看看簡單的API使用:

from tornado import template

    PAGE_HTML = """
    <html>
      Hello, {{ username }}!
      <ul>
        {% for job in job_list %}
          <li>{{ job }}</li>
        {% end %}
      </ul>
    </html>
    """
    t = template.Template(PAGE_HTML)
    print t.generate(username='John', job_list=['engineer'])

這里,用戶名在PAGE_HTML中是動態的,工作列表也是。你可以安裝 tornado ,然后運行代碼來看看輸出。

實現

如果我們進一步看看 PAGE_HTML ,那我們很容易就可以發現,一個模板字符串有兩個部分,靜態文本部分和動態部分。我們使用特殊標記來區分開動態部分。總的來說,模板引擎應該接受模板字符串,然后原樣輸出靜態部分,它還需要利用給定的上下文來處理動態部分,然后生成正確的字符串結果。所以,基本上,一個模板引擎就是一個Python函數:

def template_engine(template_string, **context):
        # process here
        return result_string

在處理過程中,模板引擎有兩個階段:

  • 解析
  • 渲染

解析階段接受模板字符串,然后生成可以渲染的結果。將模板字符串想成源代碼的話,解析工具可以是一個編程語言解釋器或者編程語言編譯器。如果工具是解釋器,那么解析會生成一個數據結構,而渲染工具將會根據這個結構來生成結果文本。Django模板引擎解析工具就是這么一個解釋器。另外,解析生成一些可執行代碼,那么渲染工具僅僅執行代碼并生成結果。Jinja2, Mako和Tornado的template模塊都使用編譯器作為解析工具。

編譯

如上所述,現在,我們需要解析模板字符串,而tornado的template模塊中的解析工具將模板編譯成Python代碼。我們的解析工具只是一個Python函數,它生成Python代碼:

def parse_template(template_string):
        # compilation
        return python_source_code

在我們進入 parse_template 的實現之前,讓我們看看它所生成的代碼,下面是一個樣例模板源字符串:

<html>
      Hello, {{ username }}!
      <ul>
        {% for job in jobs %}
          <li>{{ job.name }}</li>
        {% end %}
      </ul>
    </html>

我們的 parse_template 函數將把這個模板編譯成Python代碼,它僅僅是一個函數,簡化版本如下:

def _execute():
        _buffer = []
        _buffer.append('\n<html>\n  Hello, ')
        _tmp = username
        _buffer.append(str(_tmp))
        _buffer.append('!\n  <ul>\n    ')
        for job in jobs:
            _buffer.append('\n      <li>')
            _tmp = job.name
            _buffer.append(str(_tmp))
            _buffer.append('</li>\n    ')
        _buffer.append('\n  </ul>\n</html>\n')
        return ''.join(_buffer)

現在,我們的模板被解析到一個名為 _execute 的函數中,該函數訪問全局命名空間的所有上下文變量。該函數創建一個字符串列表,然后將它們連在一起變成結果字符串。 username 位于局部變量 _tmp 中,查詢局部變量比查詢全局變量快得多。這里,還可以做一些其他的優化,例如:

_buffer.append('hello')

    _append_buffer = _buffer.append
    # faster for repeated use
    _append_buffer('hello')

{{ ... }} 中的字符串被分析附加到字符串緩沖列表中。在tornado template模塊中,對你的語句中可以包含的表達式并無限制,if和for塊直接被轉換成Python。

代碼

現在,讓我們看看實際實現。我們所使用的核心接口是 Template 類,當我們創建一個 Template 對象時,會對模板字符串進行編譯,接著可以用它來渲染一個給定的上下文。我們僅需一次編譯,就可以把該模板對象緩存起來,構造器的簡化版本如下:

class Template(object):
        def __init__(self, template_string):
            self.code = parse_template(template_string)
            self.compiled = compile(self.code, '<string>', 'exec')

compile 會將 source 編譯成一個code對象。稍后,我們可以使用一個 exec 語句來執行它。現在,構建 parse_template 函數,首先,我們需要將模板字符串解析成節點(node)列表,它清楚如何生成Python代碼,我們需要一個名為 _parse 的函數,稍后我們將看到這個函數,我們現在需要一些輔助器,用以讀取整個模板文件,我們有 _TemplateReader 類,在我們處理模板文件的時候,它為我們處理讀取。我們需要從頭開始,找到一些特殊的標記, _TemplateReader 將會記錄當前位置,并為我們提供執行方法:

class _TemplateReader(object):
        def __init__(self, text):
            self.text = text
            self.pos = 0

        def find(self, needle, start=0, end=None):
            pos = self.pos
            start += pos
            if end is None:
                index = self.text.find(needle, start)
            else:
                end += pos
                index = self.text.find(needle, start, end)
            if index != -1:
                index -= pos
            return index

        def consume(self, count=None):
            if count is None:
                count = len(self.text) - self.pos
            newpos = self.pos + count
            s = self.text[self.pos:newpos]
            self.pos = newpos
            return s

        def remaining(self):
            return len(self.text) - self.pos

        def __len__(self):
            return self.remaining()

        def __getitem__(self, key):
            if key < 0:
                return self.text[key]
            else:
                return self.text[self.pos + key]

        def __str__(self):
            return self.text[self.pos:]

為了生成Python代碼,我們需要 _CodeWriter 類,這個類編寫代碼行,并管理縮進,另外,它還是一個Python上下文管理器:

class _CodeWriter(object):
        def __init__(self):
            self.buffer = cStringIO.StringIO()
            self._indent = 0

        def indent(self):
            return self

        def indent_size(self):
            return self._indent

        def __enter__(self):
            self._indent += 1
            return self

        def __exit__(self, *args):
            self._indent -= 1

        def write_line(self, line, indent=None):
            if indent == None:
                indent = self._indent
            for i in xrange(indent):
                self.buffer.write("    ")
            print self.buffer, line

        def __str__(self):
            return self.buffer.getvalue()

在 parse_template 的開頭,我們首先創建一個模板讀取器:

def parse_template(template_string):
        reader = _TemplateReader(template_string)
        file_node = _File(_parse(reader))
        writer = _CodeWriter()
        file_node.generate(writer)
        return str(writer)

然后,我們將讀取器傳遞給 _parse 函數,并生成節點列表。所有這些節點都是模板文件節點的子節點。我們創建一個CodeWriter對象,文件節點將Python代碼寫入到CodeWriter中,然后返回生成的Python代碼。 _Node 類將會處理特殊情況下的Python代碼生成,稍后我們會看到。現在,回到 _parse 函數:

def _parse(reader, in_block=None):
        body = _ChunkList([])
        while True:
            # Find next template directive
            curly = 0
            while True:
                curly = reader.find("{", curly)
                if curly == -1 or curly + 1 == reader.remaining():
                    # EOF
                    if in_block:
                        raise ParseError("Missing {%% end %%} block for %s" %
                                         in_block)
                    body.chunks.append(_Text(reader.consume()))
                    return body
                # If the first curly brace is not the start of a special token,
                # start searching from the character after it
                if reader[curly + 1] not in ("{", "%"):
                    curly += 1
                    continue
                # When there are more than 2 curlies in a row, use the
                # innermost ones.  This is useful when generating languages
                # like latex where curlies are also meaningful
                if (curly + 2 < reader.remaining() and
                    reader[curly + 1] == '{' and reader[curly + 2] == '{'):
                    curly += 1
                    continue
                break

進入無限循環以查找剩余文件中的模板指令,如果抵達文件末端,則附加文本節點并退出,否則,說明找到了一個模板指令。

# Append any text before the special token
            if curly > 0:
                body.chunks.append(_Text(reader.consume(curly)))

在我們處理了特殊的token后,如果有靜態部分,則將其附加到文本節點。

start_brace = reader.consume(2)

獲取起始括號,它應該是 '{{' 或者 '{%' 。

# Expression
            if start_brace == "{{":
                end = reader.find("}}")
                if end == -1 or reader.find("\n", 0, end) != -1:
                    raise ParseError("Missing end expression }}")
                contents = reader.consume(end).strip()
                reader.consume(2)
                if not contents:
                    raise ParseError("Empty expression")
                body.chunks.append(_Expression(contents))
                continue

起始括號是 '{{' ,說明這里有一個表達式,僅需獲取表達式內容,然后附加一個 _Expression 節點。

# Block
            assert start_brace == "{%", start_brace
            end = reader.find("%}")
            if end == -1 or reader.find("\n", 0, end) != -1:
                raise ParseError("Missing end block %}")
            contents = reader.consume(end).strip()
            reader.consume(2)
            if not contents:
                raise ParseError("Empty block tag ({% %})")
            operator, space, suffix = contents.partition(" ")
            # End tag
            if operator == "end":
                if not in_block:
                    raise ParseError("Extra {% end %} block")
                return body
            elif operator in ("try", "if", "for", "while"):
                # parse inner body recursively
                block_body = _parse(reader, operator)
                block = _ControlBlock(contents, block_body)
                body.chunks.append(block)
                continue
            else:
                raise ParseError("unknown operator: %r" % operator)

這里,有一個塊,通常,我們會遞歸獲得塊體,并附加一個 _ControlBlock 節點,而塊體應該是一個節點列表。如果遇到一個 {% end %} ,則說明塊結束了,退出該函數。

是時候找出 _Node 類的秘密了,它非常簡單:

class _Node(object):
        def generate(self, writer):
            raise NotImplementedError()
class _ChunkList(_Node):
        def __init__(self, chunks):
            self.chunks = chunks

        def generate(self, writer):
            for chunk in self.chunks:
                chunk.generate(writer)

一個 _ChunkList 僅是一個節點列表。

class _File(_Node):
        def __init__(self, body):
            self.body = body

        def generate(self, writer):
            writer.write_line("def _execute():")
            with writer.indent():
                writer.write_line("_buffer = []")
                self.body.generate(writer)
                writer.write_line("return ''.join(_buffer)")

_File 節點將 _execute 函數寫入到CodeWriter。

class _Expression(_Node):
        def __init__(self, expression):
            self.expression = expression

        def generate(self, writer):
            writer.write_line("_tmp = %s" % self.expression)
            writer.write_line("_buffer.append(str(_tmp))")


    class _Text(_Node):
        def __init__(self, value):
            self.value = value

        def generate(self, writer):
            value = self.value
            if value:
                writer.write_line('_buffer.append(%r)' % value)

_Text 和 _Expression 節點也相當簡單,只是附加從模板源獲得的東西。

class _ControlBlock(_Node):
        def __init__(self, statement, body=None):
            self.statement = statement
            self.body = body

        def generate(self, writer):
            writer.write_line("%s:" % self.statement)
            with writer.indent():
                self.body.generate(writer)

對于一個 _ControlBlock 節點,我們需要縮進并帶縮進編寫子節點列表。

現在,讓我們回到渲染部分,通過使用 Template 對象的 generate 方法,我們渲染一個上下文, generate 方法僅僅是調用編譯好了的Python代碼:

def generate(self, **kwargs):
        namespace = {}
        namespace.update(kwargs)
        exec self.compiled in namespace
        execute = namespace["_execute"]
        return execute()

exec 函數在給定的全局命名空間內執行編譯好的代碼,然后,從全局命名空間內抓取 _execute 函數,然后調用它。

下一步

就是這樣啦,將模板編譯成Python函數,然后執行以獲取結果。tornado的template模塊比我們這里討論的特性要多得多,但我們已經了解到了基本思想,如果感興趣的話,你可以發現更多:

  • 模板繼承
  • 模板包含
  • 更多控制邏輯,例如else, elif, try, 等等
  • 空格控制
  • 轉義
  • 更多模板指令

 

來自:https://github.com/ictar/pythondocument/blob/master/Others/一個模板引擎是如何工作的?.md

 

 本文由用戶 ChandraProc 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!