从 Python 函数构建 HTML 组件

从 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(在现实场景中,您可以删除 pretty_print=True 参数):

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。在这里,我们将元素列表转换为 <ul> 元素。

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>


创建我们的第一个视图

现在我们可以使用 <head> 元素创建一个索引视图,该元素分隔在不同的函数中,以及一个从 Python 对象生成的列表。在这里,我正在创建一个 FastAPI 应用程序来呈现内容。

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 后,您可以运行您的应用程序(将 file.py 更改为您的 Python 脚本的名称):

它看起来是这样的:

添加更多实用程序

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")),
    ),
)

添加更多组件和状态

我们已准备好所有基本部件。我们可以开始构建更多组件并将它们组合在一起。在此示例中,我将生成一个 state 字典并将其传递给 1 2 ,而不是传递所有元素参数。我还将向 <head> 添加 picocss 以进行样式设置。我将展示所有代码以及一些注释,然后我们将查看特定部分:

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." ),

这里我们在元素上设置属性 aria-label="log-in form" 。然后,我们将根据状态渲染文本(参见下面的屏幕截图)。

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 函数( list_itemslogin )。

tree = view_index(MappingProxyType(state)) html = s(tree)

我们使用此代码来呈现 HTML 字符串。最好的部分是我们可以使用以下代码只渲染登录表单:

tree = login_form(MappingProxyType(state)) html = s(tree)

现在我们可以返回部分 HTML 块。

这是页面现在的样子。每次刷新时数字都会改变:

如果我们添加 /?error=1 作为 URL 参数,状态字典将包含 "error": True ,它应该显示不同的消息 3 :

转义

构建 HTML 时,将用户生成的数据传递到模板时应小心。您可以使用 MarkupSafe 转义所需的 HTML 值。您可以修改 lxml.html.builder.E 类来转义所有字符串值 4 。 Jinja2 does not escape by default 默认情况下不会转义。

架构

此时,您可以使用不同的方法来构建 Python-HTML 组件。例如,您将所有组件函数放在一个类中。然后,该类可以将 state 字典作为属性保存。这样,您就不必传递它。这允许将所有 UI 函数保留在单独的命名空间中,同时仍然能够将所有代码保留在单个文件 5 中。我使用这种方法构建了相同的应用程序;这是源代码here。

或者您可能希望每个函数显式列出所有必需的参数。尽管这可能会变成“Prop Drilling”,正如 React 世界中所说的那样。

与 Jinja2 的性能比较

我运行了一个简单的基准测试benchmark,它根据 Python 列表生成 HTML 列表。使用 jinja2 比使用 LXML 更快,尽管与应用程序的其他部分相比,性能差异可能并不那么重要。由于 jinja2 会在第一次解析模板后对其进行缓存,因此我还对一个函数进行了基准测试,该函数在每次调用时都会重新创建模板(这就是 LXML 方法所做的)。然后我还创建了一个(使用起来不太方便)函数,它使用 LXML 生成元素,但它会在首次创建后缓存每个生成的元素。

结果如下:

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

jinja2 无疑更快。


---------------------------END---------------------------

题外话

在这里插入图片描述

感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。

??CSDN大礼包??:全网最全《Python学习资料》免费赠送??!(安全链接,放心点击)

一、Python所有方向的学习路线

Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。

img

二、Python兼职渠道推荐*

学的同时助你创收,每天花1-2小时兼职,轻松稿定生活费.
在这里插入图片描述

三、最新Python学习笔记

当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。

img

四、实战案例

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

img

??CSDN大礼包??:全网最全《Python学习资料》免费赠送??!(安全链接,放心点击)

若有侵权,请联系删除