0%

Python实现字符串插值

前言

在 WEB 开发技术中, 字符串插值是一种很常用的操作, 在各种 Web 前端框架中都能看到其身影.

例如 VUE 中, {{ }} 用两个大括号包裹住可以表示一个表达式

1
<p>{{ name }}</p>

简言之, 此时我们有变量

1
name = "naonao"

那么上面的 HTML 模板最终会被渲染为

1
<p>naonao</p>

我们可以实现一个render函数, 像这样

1
2
def render(templates: str, values: Dict[str, str]) -> str:
...

目标

  • 接收一个模板字符串, 以及需要被替换的变量字典, 返回渲染后的结果字符串.
  • 当模板串里的变量不存在于变量字典时, 抛出异常.
  • {{ }} 包裹住的变量需要清除其前后的空白字符, 例如 {{ name }}, {{name}}, {{ name}} 都是等价的

特别的

1
2
{{{name}}}
{{ {name} {age} }}

这些也应当是有效变量, 他们最终会被识别为

1
2
"{name}"
"{name} {age}"

如何实现呢?

解决思路

  1. 先搜索出模板串里的变量
  2. 从变量字典里提取对应变量值并替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from typing import Dict, List, Tuple


def _search(string: str) -> Tuple[str, Tuple[int, int]]:
"""单次搜索模板串

Args:
string: 目标字符串

Return: 模板串, 以及在原始字符串中的起止坐标
当字符串里不存在插值模板时返回空值.
例 -> ('', (None, None))
"""
i = 0
s_list = list(string)
s_len = len(s_list)
_stack = []
_temp = []
_start = None
_end = None

while i < s_len:
# 仅查找第一个模板串
if _start is not None and _end is not None:
break

# 当栈里面的 { 符号数量大于等于 2 时,
# 表示当前指针已经指向"变量"字符串, 所以
# 需要开始记录
if len(_stack) >= 2:
_temp.append(s_list[i])

# 检测连续两个字符是否为 -> {{
if s_list[i] == "{":
try:
_next = s_list[i + 1]
except IndexError:
raise SyntaxError("格式不正确, 缺少 {")
if _next == "{":
_stack.append(s_list[i])
_stack.append(s_list[i + 1])
_temp.append(s_list[i])
_temp.append(s_list[i + 1])
_start = i
i += 2
continue
else:
# 不连续时仅添加
_stack.append(s_list[i])

# 消括号
if s_list[i] == "}":
# 消括号时, 如果辅助栈里剩余 2 个括号, 则
# 表示当前指向的字符与下一个字符一定是 -> }
# 即应当是一个连续的 }}, 否则格式不正确
if len(_stack) == 2:
try:
_next = s_list[i + 1]
except IndexError:
raise SyntaxError("格式不正确, 缺少 }")
# 栈里只剩下两个元素时, 一定要出现两个连续的
# }} 否则格式不正确
if _next != "}":
raise SyntaxError("格式不正确, 缺少 }")
# 清空栈并记录结束坐标
_stack.clear()
i += 2
_temp.append(_next)
_end = i
continue

# 如果栈里存在元素就弹一个出来
if _stack:
_stack.pop()

# 指针 + 1
i += 1

# 辅助栈不为空则说明格式不正确
if _stack:
raise SyntaxError("格式不正确, 缺少 }")

return ("".join(_temp), (_start, _end))

def _var_name(temp_string: str) -> str:
try:
var = temp_string[2:-2]
except IndexError:
raise SyntaxError("格式不正确")
return var.strip()

def _replace(string: str, k1: str, k2: str) -> str:
"""
Args:

string: 原始字符串
k1: 被替换的子串
k2: 替换后的子串

"""
s_list = list(string)
s_len = len(s_list)

k1, k2 = str(k1), str(k2)
k1_list, k2_list = list(k1), list(k2)
k1_len, k2_len = len(k1_list), len(k2_list)

res = list()

i = 0
_temp = 0
while i < s_len:
# i 指针指向的位置匹配到了 k1 的首元素
# 从该位置向后匹配, 检测是否完全匹配
if s_list[i] == k1_list[0]:
for j in range(k1_len):
if k1_list[j] == s_list[i + j]:
_temp += 1
else:
break

if _temp == k1_len:
res.extend(k2_list)
i += (k1_len - 1)
else:
res.append(s_list[i])

i += 1
_temp = 0

return "".join(res)

def render(string: str, values: Dict[str, Tuple[str, int]]) -> str:
"""渲染模板字符串

逻辑概要

string = "my {{ name }} is, {{ age }} !!"

查找到第一个模板串 "{{ name }}", 然后提取左串

l_string = "my {{ name }}"

将左串替换后的结果增加到 s_list, 然后更新 _temp 为右串

_temp = " is, {{ age }} !!"

直至搜索不到模板串, 替换完毕.
"""
s_list = list()
_temp = string
while True:
# 查找模板串里的模板
t_string, (i_start, i_end) = _search(_temp)
if i_start is None or i_end is None:
break

# 取模板字符的具体值
name = _var_name(t_string)
if name not in values:
raise ValueError(f"缺少关键字 {name}")
value = str(values[name])

# 截取左串(第一个搜索到的模板串), 将替换后的结果
# 添加到 s_list, 然后更新 _temp 变量为右串(原始字符串剩余部分)
l_string = _replace(_temp[: i_end], t_string, value)
s_list.append(l_string)
_temp = _temp[i_end:]

s_list.append(_temp)
return "".join(s_list)

验证下效果

1
2
3
4
5
6
7
8
strings = [
"{{ name }} is, {{ age }}",
"my {{ name }} is, {{ age }}",
"{{ name }} is, {{ age }} !!",
"my {{ name }} is, {{ age }} !!",
]
for string in strings:
print(f'|{render(string, {"name": "nao", "age": 16})}|')
1
2
3
4
|nao is, 16|
|my nao is, 16|
|nao is, 16 !!|
|my nao is, 16 !!|

达到预期效果!