讓咱們一塊兒來構建一個模板引擎(一)

假設咱們要生成下面這樣的 html 字符串:html

<div>
    <p>welcome, Tom</p>
    <ul>
        <li>age: 20</li>
        <li>weight: 100</li>
        <li>height: 170</li>
    </ul>
</div>

要求姓名以及 <ul></ul> 中的內容是根據變量動態生成的,也就是這樣的:python

<div>
    <p>welcome, {name}</p>
    <ul>
        {info}
    </ul>
</div>

沒接觸過模板的同窗可能會想到使用字符串格式化的方式來實現:git

HTML = '''
<div>
    <p>welcome, {name}</p>
    <ul>
        {info}
    </ul>
</div>
'''


def gen_html(person):
    name = person['name']
    info_list = [
        '<li>{0}: {1}</li>'.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 的更經常使用的辦法是使用模板:github

HTML = '''
<div>
    <p>welcome, {{ person['name'] }}</p>
    <ul>
        {% for item, value in person['info'].items() %}
        <li>{{ item }}: {{ value }}</li>
        {% endfor %}
    </ul>
</div>
'''


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

本系列文章要講的就是如何從零開始實現一個這樣的模板引擎( Template )。正則表達式

使用技術

咱們將使用將模板編譯爲 python 代碼的方式來解析和渲染模板。 好比上面的模板將被編譯爲以下 python 代碼:app

def render_function():
    result = []

    result.extend([
        '<div>\n',
        '<p>welcome, '
        str(person['name']),
        '</p>\n',
        '<ul>\n'
    ])
    for item, value in person['info'].items():
        result.extend([
            '<li>',
            str(item),
            ': ',
            str(value),
            '</li>\n'
        ])
    result.extend([
        '</ul>\n'
        '</div>\n'
    ])
    return ''.join(result)

而後經過 exec 執行生成的代碼,以後再執行 render_function() 就能夠獲得咱們須要的 html 字符串了:ui

namespace = {'person': person}
exec(code, namespace)
render_function = namespace['render_function']
html = render_function()

模板引擎的核心技術就是這些了,下面讓咱們一步一步的實現它吧。spa

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)

forwardbackward 方法能夠用來控制縮進前進或後退一步,好比在生成 if 語句的時候:code

if age > 13:      # 生成完這一行之後,須要切換縮進了 ``forward()``
    ...
    ...           # 退出 if 語句主體的時候,一樣須要切換一次縮進 ``backward()``
...

Template

這個模板引擎的核心部分就是一個 Template 類,用法:

# 實例化一個 Template 對象
template = Template('''
<h1>hello, {{ name }}</h1>
{% for skill in skills %}
    <p>you are good at {{ skill }}.</p>
{% 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(['<h1>', name, '</h1>'])
        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('<h1>{{ title }}</h1>')
['{{ title }}']
>>>

知道了如何匹配變量語法,下面咱們要把變量跟其餘的模板字符串分割開來,這裏仍是用的 re:

>> re_variable = re.compile(r'(\{\{ .*? \}\})')
>>> re_variable.split('<h1>{{ title }}</h1>')
['<h1>', '{{ title }}', '</h1>']

這裏的正則之因此加了個分組是由於咱們同時還須要用到模板裏的變量。 分割開來之後咱們就能夠對每一項進行解析了。支持 {{ 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)

  • '"{}"'.format(token): 這種雖然是把 token 當成了字符串,可是會有轉義的問題,當 token 中包含 " 時生成的代碼爲 ""hello""

下面先來看一下新的 template1c.py 生成了什麼樣的代碼:

>>> from template1c import Template
>>> template = Template('<h1>{{ title }}</h1>')
>>> template.code_builder
def __func_name():
    __result = []
    __result.extend(['<h1>',str(title),'</h1>'])
    return "".join(__result)

沒問題,跟預期的是同樣的。再來看一下 render 的效果:

>>> template.render({'title': 'Python'})
'<h1>Python</h1>'

不知道你有沒有發現,其實 {{ variable }} 不僅支持變量,還支持表達式和運算符:

>>> Template('{{ 1 + 2 }}').render()
'3'
>>> Template('{{ items[0] }}').render({'items': [1, 2, 3]})
'1'
>>> Template('{{ func() }}').render({'func': list})
'[]'

這個既能夠說是個 BUG 也能夠說是個特性?, 看模板引擎是否打算支持這些功能了, 咱們在這裏是打算支持這些功能 ;)。

既然支持了 {{ }} 那麼支持註釋也就很是好實現了。

註釋

打算支持的註釋模板語法是 {# comments #} ,有了上面實現 {{ variable }} 的經驗,實現註釋是相似的代碼 (能夠直接在 github 上下載 template1d.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_comment = re.compile(r'\{# .*? #\}')
        self.re_tokens = re.compile(r'''(
            (?:\{\{ .*? \}\})
            |(?:\{\# .*? \#\})
        )''', re.X)

        # 生成 def __func_name():
        # ...

    def _parse_text(self):
        tokens = self.re_tokens.split(self.raw_text)

        for token in tokens:
            if self.re_variable.match(token):
                # ...
            # 註釋 {# ... #}
            elif self.re_comment.match(token):
                continue
            else:
                # ...

效果:

>>> from template1d import Template
>>> template = Template('<h1>{{ title }} {# comment #}</h1>')
>>> template.code_builder
def __func_name():
    __result = []
    __result.extend(['<h1>',str(title),' ','</h1>'])
    return "".join(__result)

>>> template.render({'title': 'Python'})
'<h1>Python </h1>'

至此,咱們的模板引擎已經支持了變量和註釋功能。 那麼如何實現支持 if 語句和 for 循環的標籤語法呢:

{% if user.is_admin %}
    admin, {{ user.name }}
{% elif user.is_staff %}
    staff
{% else %}
    others
{% endif %}

{% for name in names %}
    {{ name }}
{% endfor %}

我將在 第二篇文章 中向你詳細的講解。敬請期待。

相關文章
相關標籤/搜索