从 Python 函数构建 HTML 组件
from IPython.core.interactiveshell import InteractiveShell InteractiveShell.ast_node_interactivity = "all"
这篇文章也可以称为“或者如何在 Python 中进行 React”或“HTML 作为状态的函数”。
大多数人使用像 jinja2 这样的模板库来渲染 HTML。我认为这可能是在生产中实现这一点的最佳方法。然而,对于非常简单/内部/概念验证的应用程序,我想直接从 Python 函数生成 HTML 以避免需要额外的文件。
我尝试使用 f 字符串来做到这一点,但它很快就会变得混乱。我最近发现了一种使用 lxml 渲染 HTML 的好方法。一个很好的副作用是,整体架构类似于 React,其中函数变成了 UI 组件。同时,它允许轻松地仅渲染单个组件。当与 HTMX 一起使用时,这会特别有用。
一个基本组件,渲染字符串
lxml 已经附带了一个类和一些实用程序来生成 HTML 元素并将它们序列化为字符串。
这将生成以下 HTML(在现实场景中,您可以删除 
from lxml.html import HtmlElement
from lxml.html import tostring
from lxml.html.builder import E as e
def s(tree: HtmlElement) -> str:
    """
    Serialize LXML tree to unicode string. Using DOCTYPE html.
    """
    return tostring(tree, encoding="unicode", doctype="<!DOCTYPE html>", pretty_print=True)
def head(title: str):
    return e.head(
        e.meta(charset="utf-8"),
        e.meta(name="viewport", content="width=device-width, initial-scale=1"),
        e.title(title),
    )
tree = head("Hello")
print(s(tree))
<!DOCTYPE html> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Hello</title></head>
我们现在有了一个从 Python 对象生成的简单但有效的 HTML。
将 Python 对象转换为 HTML
通常,您将拥有某种状态或上下文,并根据该上下文呈现 HTML。我们可以使用任何 Python 对象来生成 HTML。在这里,我们将元素列表转换为 
from lxml.html import HtmlElement
from lxml.html import tostring
from lxml.html.builder import E as e
def s(tree: HtmlElement) -> str:
    """
    Serialize LXML tree to unicode string. Using DOCTYPE html.
    """
    return tostring(tree, encoding="unicode", doctype="<!DOCTYPE html>", pretty_print=True)
def list_items(items: list[str]):
    return e.ul(*[e.li(item) for item in items])
tree = list_items(["foo", "bar", "baz"])
print(s(tree))
<!DOCTYPE html> <ul> <li>foo</li> <li>bar</li> <li>baz</li> </ul>
创建我们的第一个视图
现在我们可以使用 
import asyncio
import random
import uvicorn
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from lxml.html import HtmlElement
from lxml.html import tostring
from lxml.html.builder import E as e
app = FastAPI()
def s(tree: HtmlElement) -> str:
    """
    Serialize LXML tree to unicode string. Using DOCTYPE html.
    """
    return tostring(tree, encoding="unicode", doctype="<!DOCTYPE html>")
def head(title: str):
    return e.head(
        e.meta(charset="utf-8"),
        e.meta(name="viewport", content="width=device-width, initial-scale=1"),
        e.title(title),
    )
def list_items(items: list[str]):
    return e.ul(*[e.li(item) for item in items])
def index(items: list[str]):
    return e.html(
        # generate <head> element by calling a python function
        head("Home"),
        e.body(
            e.h1("Hello, world!"),
            list_items(items),
        ),
    )
@app.get("/", response_class=HTMLResponse)
def get():
    items = [str(random.randint(0, 100)) for _ in range(10)]
    tree = index(items)
    html = s(tree)
    return html
# if __name__ == "__main__":
#     # run app with uvicorn
#     uvicorn.run(
#         f'{__file__.split("/")[-1].replace(".py", "")}:app',
#         host="127.0.0.1",
#         port=8000,
#         reload=True,
#         workers=1,
#     )
if __name__ == "__main__":
    config = uvicorn.Config(app)
    server = uvicorn.Server(config)
    await server.serve()
INFO: Started server process [10016] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: 127.0.0.1:55395 - "GET / HTTP/1.1" 200 OK INFO: 127.0.0.1:55395 - "GET /favicon.ico HTTP/1.1" 404 Not Found INFO: Shutting down INFO: Waiting for application shutdown. INFO: Application shutdown complete. INFO: Finished server process [10016]
安装 FastAPI, uvicorn 和 lxml 后,您可以运行您的应用程序(将 
它看起来是这样的:
添加更多实用程序
lxml 附带了一些向元素添加属性的函数,但我决定编写自己的函数以获得更好的人体工程学效果。
# handle some Python / HTML keywords.
def replace_attr_name(name: str) -> str:
    if name == "_class":
        return "class"
    elif name == "_for":
        return "for"
    return name
def ATTR(**kwargs):
    # Use str() to convert values to string. This way we can set boolean
    # attributes using True instead of "true".
    return {replace_attr_name(k): str(v) for k, v in kwargs.items()}
有了这些函数,我们现在可以构建如下元素:
e.html(
    ATTR(lang="en"),
    head("Hello"),
    e.body(
	    # we use `class` because `class` is a Python keyword
        e.main(ATTR(id="main", _class="container")),
    ),
)
添加更多组件和状态
我们已准备好所有基本部件。我们可以开始构建更多组件并将它们组合在一起。在此示例中,我将生成一个 
import random
# import MappingProxyType for "frozen dict"
from types import MappingProxyType
import uvicorn
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from lxml.html import HtmlElement
from lxml.html import tostring
from lxml.html.builder import E as e
app = FastAPI()
# Type alias. State can be a dict or a MappingProxyType.
State = dict | MappingProxyType
def replace_attr_name(name: str) -> str:
    if name == "_class":
        return "class"
    elif name == "_for":
        return "for"
    return name
def ATTR(**kwargs):
    # Use str() to convert values to string. This way we can set boolean
    # attributes using True instead of "true".
    return {replace_attr_name(k): str(v) for k, v in kwargs.items()}
def s(tree: HtmlElement) -> str:
    """
    Serialize LXML tree to unicode string. Using DOCTYPE html.
    """
    return tostring(tree, encoding="unicode", doctype="<!DOCTYPE html>")
def base(*children: HtmlElement, state: State):
    return e.html(
        ATTR(lang="en"),
        head(state),
        e.body(
            e.main(ATTR(id="main", _class="container"), *children),
        ),
    )
def head(state: State):
    return e.head(
        e.meta(charset="utf-8"),
        e.title(state.get("title", "Home")),
        e.meta(name="viewport", content="width=device-width, initial-scale=1"),
        e.meta(name="description", content="Welcome."),
        e.meta(name="author", content="@polyrand"),
        e.link(
            rel="stylesheet",
            href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css",
        ),
    )
def login_form(state: State):
    return e.article(
        ATTR(**{"aria-label": "log-in form"}),
        e.p(
            e.strong(ATTR(style="color: red"), "Wrong credentials!")
            if state.get("error")
            else f"{state.get('user', 'You')} will receive an email with a link to log in."
        ),
        e.form(
            e.label("Email", _for="email"),
            e.input(
                ATTR(
                    placeholder="Your email",
                    type="email",
                    name="email",
                    required=True,
                )
            ),
            e.button("Log In"),
            action="/login",
            method="post",
        ),
    )
def view_index(state: State):
    return base(
        e.section(
            e.h1("Page built using lxml"),
            e.p("This is some text."),
        ),
        list_items(state),
        login_form(state),
        state=state,
    )
def list_items(state: State):
    return e.ul(*[e.li(item) for item in state["items"]])
@app.get("/", response_class=HTMLResponse)
def idx(error: bool = False):
    items = [str(random.randint(0, 100)) for _ in range(4)]
    state = {
        "title": "Some title",
        "items": items,
        "user": "@polyrand",
    }
    if error:
        state["error"] = True
    tree = view_index(MappingProxyType(state))
    html = s(tree)
    return html
# if __name__ == "__main__":
#     uvicorn.run(
#         f'{__file__.split("/")[-1].replace(".py", "")}:app',
#         host="127.0.0.1",
#         port=8000,
#         reload=True,
#         workers=1,
#     )
if __name__ == "__main__":
    config = uvicorn.Config(app)
    server = uvicorn.Server(config)
    await server.serve()
我们来看一些部分。
return e.article( ATTR(**{“aria-label”: “log-in form”}), e.p( e.strong(ATTR(style=“color: red”), “Wrong credentials!”) if state.get(“error”) else f"{state.get(‘user’, ‘You’)} will receive an email with a link to log in." ),
这里我们在元素上设置属性 
return base( e.section( e.h1(“Page built using lxml”), e.p(“This is some text.”), ), list_items(state), login_form(state), state=state, )
在这里,我们渲染基本模板并传递一些子对象。请注意每个元素都是一个 Python 函数( 
tree = view_index(MappingProxyType(state)) html = s(tree)
我们使用此代码来呈现 HTML 字符串。最好的部分是我们可以使用以下代码只渲染登录表单:
tree = login_form(MappingProxyType(state)) html = s(tree)
现在我们可以返回部分 HTML 块。
这是页面现在的样子。每次刷新时数字都会改变:
如果我们添加 
转义
构建 HTML 时,将用户生成的数据传递到模板时应小心。您可以使用 MarkupSafe 转义所需的 HTML 值。您可以修改 
架构
此时,您可以使用不同的方法来构建 Python-HTML 组件。例如,您将所有组件函数放在一个类中。然后,该类可以将 
或者您可能希望每个函数显式列出所有必需的参数。尽管这可能会变成“Prop Drilling”,正如 React 世界中所说的那样。
与 Jinja2 的性能比较
我运行了一个简单的基准测试benchmark,它根据 Python 列表生成 HTML 列表。使用 
结果如下:
fallback Jinja 16.4 μs ± 51.7 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Jinja recreate template 353 μs ± 4.41 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
LXML 180 μs ± 744 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
LXML cached builder 22.2 μs ± 220 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
概括:
| 技术 | 平均执行时间(微秒) | 
|---|---|
| Jinja2 | 16.4 | 
| Jinja2 recreate | 353 | 
| LXML | 180 | 
| LXML cached | 22.2 | 
---------------------------END---------------------------
题外话

感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。
 
??CSDN大礼包??:全网最全《Python学习资料》免费赠送??!(安全链接,放心点击)
一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。

二、Python兼职渠道推荐*
学的同时助你创收,每天花1-2小时兼职,轻松稿定生活费.
 
三、最新Python学习笔记
当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。

四、实战案例
纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

??CSDN大礼包??:全网最全《Python学习资料》免费赠送??!(安全链接,放心点击)
若有侵权,请联系删除