简书上为首发,拒绝抄袭,原文地址 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!'
当传递进来的 template
是 Hello {{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.'
至此,我们就知道 bottle
中 template
的实现了,大体流程图如下
简书不支持 mermaid,先贴上截图
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.*)|(.*))'
分析如下
- 以0个或者多个
\s
(空字符) 开头,后面接着一个%
,后面在接着 0个或者多个\s
字符 - 第一个大括号
?:(n)
为非捕获括号,表示匹配任何其后紧接指定字符串 n 的字符串,所以这个大组并没有编号,当我们后面匹配时m.groups()[0]
并不是这个大括号 - 再对大括号里面的四个括号进行分析。四个括号之间使用
|
隔开,说明只要一行字符串中,有包含其中一个字段即可匹配- 第一个括号,即第一组,匹配 python 关键字
if|elif|else|try|except|finally|for|while|with|def|class
- 第二个括号,即第二组,匹配
include.*
- 第三个括号,即第三组,匹配
end.*
- 第四个括号,即第四组,匹配
(.*)
- 第一个括号,即第一组,匹配 python 关键字
做个简单的测试
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>
网友评论