(点击
上方蓝字
,快速关注我们)
来源:伯乐在线专栏作者 - mozillazg
如有好文章投稿,请点击 → 这里了解详情
如需转载,发送「转载」二字查看说明
假设我们要生成下面这样的 html 字符串:
welcome, Tom
-
age: 20
-
weight: 100
-
height: 170
要求姓名以及 中的内容是根据变量动态生成的,也就是这样的:
没接触过模板的同学可能会想到使用字符串格式化的方式来实现:
HTML
=
'''
'''
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'
,
'
])
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 中包含 " 和 ' 的情况。 下面是几种有问题的写法: