用python寫個模板引擎

来源:https://www.cnblogs.com/q1214367903/archive/2020/04/29/12803993.html
-Advertisement-
Play Games

一.實現思路 本文講解如何使用python實現一個簡單的模板引擎, 支持傳入變數, 使用if判斷和for迴圈語句, 最終能達到下麵這樣的效果: 渲染前的文本: <h1>{{title}}</h1> <p>十以內的奇數:</p> <ul> {% for i in range(10) %} {% if ...


一.實現思路

  本文講解如何使用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': '高等數學'})

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文的模型使用了C++工具箱dlib基於深度學習的最新人臉識別方法,基於戶外臉部數據測試庫Labeled Faces in the Wild 的基準水平來說,達到了99.38%的準確率。 dlib:http://dlib.net/ 數據測試庫Labeled Faces in the Wild:htt ...
  • 1 #include <iostream> 2 3 using namespace std; 4 5 class Matrix 6 { 7 private: 8 int a,b; 9 int arr[100][100]; 10 public: 11 Matrix() 12 { 13 for(int ...
  • 問題:Mac,php7.2,nginx 重啟php-fpm,多次運行killall php-fpm,或者一個一個的kill -9之後,再查看進程ps aux | grep php-fpm,php-fpm進程還在…… 參考網址:http://blog.haohtml.com/archives/1897 ...
  • 場景: 有一個表中的某一列,你需要獲取到這一列的所有值,你怎麼操作? 解決辦法: 有一個model為:Event 方式一: 方式一獲取到的是一個 ,內容是鍵值對構成的,鍵為表的列名,值為對應的每個值。 方式二: 方式二獲取到的也是一個 ,但是內容是元祖形式的查詢列的值。 但是我們想要的是這一列的值呀 ...
  • String 字元串 一個字元串是由一系列字元組成的。其中每個字元等同於一個位元組。這意味著 PHP 只能支持 256 的字元集,因此不支持 Unicode 。 語法 一個字元串可以用 4 種方式表達: 單引號 雙引號 單引號 一個最簡單的字元串是用單引號包起來的。可以再前面加反斜杠(\)來轉義。例如 ...
  • 我覺得我生活在這世上二十多年裡,去過最多的餐廳就是肯德基小時候逢生日必去,現在長大了,肯德基成了我的日常零食下班後從門前路過餓了便會進去點分黃金雞塊或者小吃拼盤早上路過,會買杯咖啡。主要快捷美味且飽腹,而且到處都是總是會路過,現在只要一餓,心心念念便是肯德基的味道 環境介紹 python 3.6 p ...
  • [TOC] 前言 今天在公司無事,就搞了下文件上傳的東東,方便自己的學習,和今後的使用,目前只支持單文件上傳。下麵是代碼部分,也可以訪問我的 "Gitee" 或 "Github" ,不想看代碼的童鞋可以直接 "下載]。。。(算了,討厭百度雲,如果有需要的話留言)代碼使用,使用方法請[點擊" 代碼 使 ...
  • 效果: 批量查詢指定關鍵字 & 指定目錄下PDF文件中的文本,並導出文件路徑和關鍵字所在文本行。 下載: 鏈接: https://pan.baidu.com/s/1sK2OMMgGX26l7PiMPqKXkA 提取碼: 67h4 使用: 雙擊exe,輸入pdf所在目錄,空格,關鍵字 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...