一.實作思路
本文講解如何使用python實作一個簡單的模板引擎, 支持傳入變數, 使用if判斷和for回圈陳述句, 最終能達到下面這樣的效果:
渲染前的文本: <h1>{{title}}</h1> <p>十以內的奇數:</p> <ul> {% for i in range(10) %} {% if i%2==1 %} <li>{{i}}</li> {% end %} {% end %} </ul> 渲染后的文本,假設title="高等數學": <h1>高等數學</h1> <p>十以內的奇數:</p> <ul> <li>1</li> <li>3</li> <li>5</li> <li>7</li> <li>9</li> </ul>
要實作這樣的效果, 第一步就應該將文本中的html代碼和類似{% xxx %}這樣的渲染陳述句分別提取出來, 使用下面的正則運算式可以做到:
re.split(r'(?s)({{.*?}}|{%.*?%}|{#.*?#})', html)
用這個正則運算式處理剛才的文本, 結果如下:

在提取文本之后, 就需要執行內部的邏輯了. python自帶的exec函式可以執行字串格式的代碼:
exec('print("hello world")') # 這條陳述句會輸出hello world
因此, 提取到html的渲染陳述句之后, 可以把它改成python代碼的格式, 然后使用exec函式去運行. 但是, exec函式不能回傳代碼的執行結果, 它只會回傳None. 雖然如此, 我們可以使用下面的方式獲取字串代碼中的變數:
global_namespace = {} code = """ a = 1 def func(): pass """ exec(code, global_namespace) print(global_namespace) # {'a': 1, 'func': <function func at 0x00007fc61e3462a0>, '__builtins__': <module 'builtins' (built-in)>}
因此, 我們只要在code這個字串中定義一個函式, 讓它能夠回傳渲染后的模板, 然后使用剛才的方式把這個函式從字串中提取出來并執行, 就能得到結果了.
基于上面的思路, 我們最終應該把html文本轉化為下面這樣的字串:
# 這個函式不是我們寫的, 是待渲染的html字串轉化過來的 def render(context: dict) -> str: result = [] # 這一部分負責提取所有動態變數的值 title = context['title'] # 對于所有的html代碼或者是變數, 直接放入result串列中 result.extend(['<h1>', str(title), '</h1>\n<p>十以內的奇數:</p>\n<ul>\n']) # 對于模板中的for和if回圈陳述句,則是轉化為原生的python陳述句 for i in range(10): if i % 2 == 1: result.extend(['\n <li>', str(i), '</li>\n ']) result.append('\n</ul>') # 最后,讓函式將result串列聯結為字串回傳就行, 這樣就得到了渲染好的html文本 return ''.join(result)
如何將html文本轉化為上面這樣的代碼, 是這篇文章的關鍵. 上面的代碼是由最開始那個html demo轉化來的, 每一塊我都做了注釋. 如果沒看明白的話, 就多看幾遍, 不然肯定是看不懂下文的.
總的來說, 要渲染一個模板, 思路如下:

二.字串代碼
為了能夠方便地生成python代碼, 我們首先定義一個CodeBuilder類:
class CodeBuilder: INDENT_STEP = 4 def __init__(self, indent_level: int = 0) -> None: self.indent_level = indent_level self.code = [] self.global_namespace = None def start_func(self) -> None: self.add_line('def render(context: dict) -> str:') self.indent() self.add_line('result = []') self.add_line('append_result = result.append') self.add_line('extend_result = result.extend') self.add_line('to_str = str') def end_func(self) -> None: self.add_line("return ''.join(result)") self.dedent() def add_section(self) -> 'CodeBuilder': section = CodeBuilder(self.indent_level) self.code.append(section) return section def __str__(self) -> str: return ''.join(str(line) for line in self.code) def add_line(self, line: str) -> None: self.code.extend([' ' * self.indent_level + line + '\n']) def indent(self) -> None: self.indent_level += self.INDENT_STEP def dedent(self) -> None: self.indent_level -= self.INDENT_STEP def get_globals(self) -> dict: if self.global_namespace is None: self.global_namespace = {} python_source = str(self) exec(python_source, self.global_namespace) return self.global_namespace
這個類作為字串代碼的容器使用, 它的本質是對字串代碼的封裝, 在字串的基礎上增加了以下的功能:
- 代碼縮進
CodeBuilder維護了一個indent_level變數, 當呼叫它的add_line方法寫入新代碼的時候, 它會自動在代碼開頭加上縮進. 另外, 呼叫indent和dedent方法就能方便地增加和減少縮進.
- 生成函式
由于定義這個類的目的就是在字串里面寫一個函式, 而這個函式的開頭和結尾都是固定的, 所以把它直接寫到物件的方法里面. 值得一提的是, 在start_func這個方法中, 我們寫了這樣三行代碼:
append_result = result.append extend_result = result.extend to_str = str
這樣做是為了提高渲染模板的性能, 呼叫我們自己定義的函式, 需要的時間比呼叫result.append或者str等函式的時間少. 首先對于串列的append和extend兩個方法來說, 每呼叫一次, python都需要在串列中的所有方法中找一次, 而直接把它系結到我們自己定義的變數上, 就能避免python重復地去串列的方法中來找. 然后是str函式, 理論上, python查找區域變數的速度比查找內置變數的快, 因此我們使用一個區域變數to_str, python找到它的速度就比找str要快.
上面這段話都是我從網上看到的, 實際測驗了一下, 在python3.7上, 運行append_result需要的時間比直接呼叫result.append少了大約25%, to_str則沒有明顯的優化效果.
- 代碼嵌套
有的時候我們需要在一塊代碼中嵌套另外一塊代碼, 這時候可以呼叫add_section方法, 這個方法會創建一個新的CodeBuilder物件作為內容插入到原CodeBuilder物件里面, 這個和前端的div套div差不多.
這個方法的好處是, 你可以在一個CodeBuilder物件中預先插入一個CodeBuilder物件而不用寫入內容, 相當于先占著位置. 等條件成熟之后, 再回過頭來寫入內容. 這樣就增加了字串代碼的可編輯性.
- 獲取變數
呼叫get_globals方法獲取當前字串代碼內的所有全域變數.
三.Template模板
在字串代碼的容器做好之后, 我們只需要決議html文本, 然后把它轉化為python代碼放到這個容器里面就行了. 因此, 我們定義如下的Template類:
class Template: html_regex = re.compile(r'(?s)({{.*?}}|{%.*?%}|{#.*?#})') valid_name_regex = re.compile(r'[_a-zA-Z][_a-zA-Z0-9]*$') def __init__(self, html: str, context: dict = None) -> None: self.context = context or {} self.code = CodeBuilder() self.all_vars = set() self.loop_vars = set() self.code.start_func() vars_code = self.code.add_section() buffered = [] def flush_output() -> None: if len(buffered) == 1: self.code.add_line(f'append_result({buffered[0]})') elif len(buffered) > 1: self.code.add_line(f'extend_result([{", ".join(buffered)}])') del buffered[:] strings = re.split(self.html_regex, html) for string in strings: if string.startswith('{%'): flush_output() words = string[2:-2].strip().split() ops = words[0] if ops == 'if': if len(words) != 2: self._syntax_error("Don't understand if", string) self.code.add_line(f'if {words[1]}:') self.code.indent() elif ops == 'for': if len(words) != 4 or words[2] != 'in': self._syntax_error("Don't understand for", string) i = words[1] iter_obj = words[3] # 這里被迭代的物件可以是一個變數,也可以是串列,元組或者range之類的東西,因此使用_variable來檢驗 try: self._variable(iter_obj, self.all_vars) except TemplateSyntaxError: pass self._variable(i, self.loop_vars) self.code.add_line(f'for {i} in {iter_obj}:') self.code.indent() elif ops == 'end': if len(words) != 1: self._syntax_error("Don't understand end", string) self.code.dedent() else: self._syntax_error("Don't understand tag", ops) elif string.startswith('{{'): expr = string[2:-2].strip() self._variable(expr, self.all_vars) buffered.append(f'to_str({expr})') else: if string.strip(): # 這里使用repr把換行符什么的改成/n的形式,不然插到code字串中會打亂排版 buffered.append(repr(string)) flush_output() for var_name in self.all_vars - self.loop_vars: vars_code.add_line(f'{var_name} = context["{var_name}"]') self.code.end_func() def _variable(self, name: str, vars_set: set) -> None: # 當決議html程序中出現變數,就呼叫這個函式 # 一方面檢驗變數名是否合法,一方面記下變數名 if not re.match(self.valid_name_regex, name): self._syntax_error('Not a valid name', name) vars_set.add(name) def _syntax_error(self, message: str, thing: str) -> None: raise TemplateSyntaxError(f'{message}: {thing}') # 這個Error類直接繼承Exception就行 def render(self, context=None) -> str: render_context = dict(self.context) if context: render_context.update(context) return self.code.get_globals()['render'](render_context)
首先, 我們實體化了一個CodeBuilder物件作為容器使用. 在這之后, 我們定義了all_vars和loop_vars兩個集合, 并在CodeBuilder生成的函式開頭插了一個子容器. 這樣做的目的是, 最終生成的函式應該在開頭添加類似 var_name = context['var_name']之類的陳述句, 來提取傳入的背景關系變數的值. 但是, html中有哪些需要渲染的變數, 這是在渲染之后才知道的, 所以先在開頭插入一個子容器, 并創建all_vars這個集合, 以便在渲染html之后把這些變數的賦值陳述句插進去. loop_vars則負責存放那些由于for回圈產生的變數, 它們不需要從背景關系中提取.
然后, 我們創建一個bufferd串列. 由于在渲染html的程序中, 變數和html陳述句是不需要直接轉為python陳述句的, 而是應該使用類似 append_result(xxx)這樣的形式添加到代碼中去, 所以這里使用一個bufferd串列儲存變數和html陳述句, 等渲染到for回圈等特殊陳述句時, 再呼叫flush_output一次性把這些東西全寫入CodeBuilder中. 這樣做的好處是, 最后生成的字串代碼可能會少幾行.
萬事具備之后, 使用正則運算式分割html文本, 然后迭代分割結果并處理就行了. 對于不同型別的字串, 使用下面的方式來處理:
- html代碼塊
只要有空格和換行符之外的內容, 就放入緩沖區, 等待統一寫入代碼
- 帶的{{}}的變數
只要變數合法, 就記錄下變數名, 然后和html代碼塊同樣方式處理
- if條件判斷 & for回圈
這兩個處理方法差不多, 首先檢查語法有無錯誤, 然后提取引數將其轉化為python陳述句插入, 最后再增加縮進就行了. 其中for陳述句還需要記錄使用的變數
- end陳述句
這條陳述句意味著for回圈或者if判斷結束, 因此減少CodeBuilder的縮進就行
在決議完html文本之后, 清空bufferd的資料, 為字串代碼添加變數提取和函式回傳值, 這樣代碼也就完成了.
四.結束
最后, 實體化Template物件, 呼叫其render方法傳入背景關系, 就能得到渲染的模板了:
t = Template(html) result = t.render({'title': '高等數學'})
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/151724.html
標籤:Python
上一篇:c語言商品管理系統問題
