Manjusaka

Manjusaka

テンプレートエンジンの動作メカニズムの詳細解説

私はさまざまなテンプレートエンジンを長い間使用してきましたが、ついにテンプレートエンジンがどのように機能するのかを研究する時間ができました。

概要#

簡単に言うと、テンプレートエンジンは大量のテキストデータを扱うプログラミングタスクを完了するためのツールです。一般的に、私たちは web アプリケーションでテンプレートエンジンを利用して HTML を生成します。 Python では、テンプレートエンジンを使用したい場合、jinjamako など、いくつかの選択肢があります。これから、tornado のテンプレートエンジンを利用して、テンプレートエンジンの動作原理を説明します。 tornado には、比較的シンプルな組み込みのテンプレートエンジンがあり、その原理を深く分析するのに便利です。

テンプレートエンジンの実装原理を研究する前に、まずはシンプルなインターフェース呼び出しの例を見てみましょう。

    from tornado import template

    PAGE_HTML = """
    <html>
      こんにちは、{{ 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'])

このコード内の username は動的に生成され、job リストも同様です。 tornado をインストールしてこのコードを実行することで、最終的な結果を確認できます。

詳細#

PAGE_HTML を注意深く観察すると、このテンプレート文字列は 2 つの部分で構成されていることがわかります。一方は固定の文字列、もう一方は動的に生成される内容です。動的に生成される部分には特別な記号を使用してマークします。全体の作業フローの中で、テンプレートエンジンは固定の文字列を正しく出力し、マークした動的に生成される文字列を正しい結果に置き換える必要があります。

テンプレートエンジンを使用する最も簡単な方法は、以下のように 1 行の python コードで解決できます:

    deftemplate_engine(template_string, **context):# process herereturn result_string

全体の作業プロセスでは、テンプレートエンジンは次の 2 つの段階に分かれて文字列を操作します:

  • 解析
  • レンダリング

解析段階では、準備した文字列を解析し、レンダリング可能な形式にフォーマットします。これは rendered.Consider によって解析可能な文字列である可能性があり、解析器は言語のインタープリターまたはコンパイラーである可能性があります。解析器がインタープリターである場合、解析プロセス中にデータを格納するための特別なデータ構造が生成され、レンダラーはそのデータ構造全体を遍歴してレンダリングを行います。例えば、Django のテンプレートエンジンの解析器はインタープリターに基づくツールです。それに加えて、解析器は実行可能なコードを生成することもあり、レンダラーはそのコードを実行して対応する結果を生成します。 Jinja2MakoTornado では、テンプレートエンジンは解析ツールとしてコンパイラーを使用しています。

コンパイル#

上記で述べたように、私たちは作成したテンプレート文字列を解析する必要があります。そして、tornado のテンプレート解析器は、作成したテンプレート文字列を実行可能な Python コードにコンパイルします。私たちの解析ツールは Python コードを生成する責任があり、単一の Python 関数で構成されています:

    def parse_template(template_string):
      # compilation
      return python_source_code

parse_template のコードを分析する前に、まずはテンプレート文字列の例を見てみましょう:

    <html>
      こんにちは、{ { username } }!
      <ul>
        { % for job in jobs % }
          <li>{ { job.name } }</li>
        { % end % }
      </ul>
    </html>

テンプレートエンジンの parse_template 関数は、上記の文字列を Python ソースコードにコンパイルします。最も簡単な実装方法は以下の通りです:

    def _execute():
        _buffer = []
        _buffer.append('\n<html>\n  こんにちは、')
        _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 関数内でテンプレートを処理しています。この関数は、グローバル名前空間内のすべての有効な変数を使用できます。この関数は、複数の string を含むリストを作成し、それらを結合して返します。明らかに、ローカル変数を見つける方がグローバル変数を見つけるよりも速いです。同時に、他のコードの最適化もこの段階で行われます。例えば:

    _buffer.append('こんにちは')

    _append_buffer = _buffer.append
    # 繰り返し使用するために高速化
    _append_buffer('こんにちは')

{ { ... } } 内の式は抽出され、string リストに追加されます。 tornado テンプレートモジュールでは、{ { ... } } に記述された式には制限がなく、if および for コードブロックは正確に Python コードに変換されます。

具体的なコード実装を見てみましょう#

テンプレートエンジンの具体的な実装を見てみましょう。 Template クラス内でコア変数を宣言し、Template オブジェクトを作成すると、作成したテンプレート文字列をコンパイルできます。その後、コンパイル結果に基づいてレンダリングを行うことができます。私たちは作成したテンプレート文字列を一度だけコンパイルし、そのコンパイル結果をキャッシュできます。以下は Template クラスの簡略化されたバージョンのコンストラクタです:

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

上記のコード内の compile 関数は、文字列を実行可能なコードにコンパイルします。私たちは後で exec 関数を呼び出して生成したコードを実行できます。さて、parse_template 関数の実装を見てみましょう。まず、作成したテンプレート文字列を独立したノードに変換し、後で Python コードを生成する準備をします。このプロセスでは、_parse 関数が必要です。まずはそれを脇に置いておき、後でこの関数を見てみましょう。次に、テンプレートファイルからデータを読み取るための補助関数をいくつか作成する必要があります。では、私たちのカスタムテンプレートからデータを読み取るための _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 関数内で、まず _TemplateReader オブジェクトを作成します:

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

次に、作成した _TemplateReader オブジェクトを _parse 関数に渡してノードリストを生成します。ここで生成されるすべてのノードはテンプレートファイルの子ノードです。次に、_CodeWriter オブジェクトを作成し、file_node オブジェクトが生成した 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)))

特殊なマーカーのコードブロックを処理する前に、静的な部分をノードリストに追加します。

    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 では、取得したコードブロックを Python 構文に従ってフォーマットする必要があります。

さて、前述のテンプレートエンジンのレンダリング部分を見てみましょう。 Template オブジェクト内で generate メソッドを実装することで、テンプレートから解析された Python コードを呼び出します。

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

与えられたグローバル名前空間内で、 exec 関数はコンパイルされたコードオブジェクトを実行します。その後、私たちはグローバルに _execute 関数を呼び出すことができます。

最後に#

上記の一連の操作を経て、私たちはテンプレートを自由にコンパイルし、対応する結果を得ることができます。実際、tornado テンプレートエンジンには、私たちがまだ議論していない多くの機能がありますが、私たちはその最も基本的な動作メカニズムを理解しました。この基礎の上に、興味のある部分を研究することができます。例えば:

  • テンプレートの継承
  • テンプレートの包含
  • その他の論理制御文、例えば elseeliftry など
  • 空白制御
  • 特殊文字のエスケープ
  • その他の未説明のテンプレート指令(訳注:tornado 公式ドキュメント を参照してください)
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。