从 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学习资料》免费赠送??!(安全链接,放心点击)
若有侵权,请联系删除