亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

利用python怎么編寫一個模板引擎

發布時間:2021-01-14 14:26:35 來源:億速云 閱讀:167 作者:Leah 欄目:開發技術

這篇文章給大家介紹利用python怎么編寫一個模板引擎,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

渲染前的文本:
<h2>{{title}}</h2>
<p>十以內的奇數:</p>
<ul>
{% for i in range(10) %}
  {% if i%2==1 %}
    <li>{{i}}</li>
  {% end %}
{% end %}
</ul>


渲染后的文本,假設title="高等數學":
<h2>高等數學</h2>
<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怎么編寫一個模板引擎

  在提取文本之后, 就需要執行內部的邏輯了. 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(['<h2>', str(title), '</h2>\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怎么編寫一個模板引擎

二.字符串代碼

  為了能夠方便地生成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': '高等數學'})

關于利用python怎么編寫一個模板引擎就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

米泉市| 梅州市| 广东省| 台山市| 枣庄市| 祁连县| 汕尾市| 额敏县| 凌源市| 修文县| 长海县| 衡东县| 石泉县| 临西县| 商水县| 沙坪坝区| 临安市| 亚东县| 敦煌市| 沁水县| 丰城市| 宁德市| 巴彦淖尔市| 隆昌县| 洪湖市| 富裕县| 正定县| 遵义县| 常山县| 鲁甸县| 舞钢市| 大连市| 贵港市| 绥中县| 保山市| 明溪县| 乌鲁木齐县| 济宁市| 兰溪市| 武城县| 蒲江县|