Python 现代开发学习笔记

(十三)从数据渲染页面并保留文本含义

1. 模板提供结构,值进入指定位置

第五章读写的是整段文本;页面模板在固定 HTML 结构里保留占位位置,服务器拿到本次用户与书籍后再填入。

# 项目文件:web_project/src/webbook/render.py
# 模板只提供固定结构;进入HTML文本和属性的值先转义,JSON显示交给textContent。
from html import escape
from pathlib import Path
from string import Template

TEMPLATES = Path(__file__).resolve().parent / "templates"


def render_page(template_name, **values):
    template = Template((TEMPLATES / template_name).read_text(encoding="utf-8"))
    return template.substitute(values)


def hidden_csrf(session):
    return f'<input type="hidden" name="csrf_token" value="{escape(session.csrf, quote=True)}">'


def render_login(session, message="", username=""):
    return render_page(
        "login.html",
        csrf=hidden_csrf(session),
        message=escape(message),
        username=escape(username, quote=True),
    )

string.Template 识别 $名称 这样的占位符,substitute 用传入值替换。模板路径按模块位置取得,启动目录改变后仍能找到文件。

escape 处理进入 HTML 文本或带引号属性的值。hidden_csrf 生成隐藏控件,render_login 填入说明与账号,错误响应不把密码再填回页面。

实际浏览器中的登录页面,账号和密码由表单提交,令牌以隐藏字段与本次会话关联

2. 用户文本不能变成页面指令

# 项目文件:demos/escaping.py
# HTML文本先转义;JSON仍是数据,前端用textContent显示其中的书名。
import json
from html import escape

title = '<img src=x onerror="alert(1)">'
print("HTML文本", escape(title, quote=True))
payload = json.dumps({"title": title}, ensure_ascii=False)
print("JSON恢复原文", json.loads(payload)["title"] == title)

书名为 <img src=x onerror="alert(1)"> 时,读者保存的是一段书名文本。如果原样拼入 HTML,浏览器可能把它解释为标签及事件;转义以后以 &lt;img...&gt; 进入 HTML,页面显示原书名,而不会创建对应图像节点。

转义按输出位置进行。这里处理 HTML 文本和双引号属性,数据库中的书名仍保存原文本;JSON 序列化后也能恢复原书名,前端再用 textContent 显示。不能因为“已转成 JSON”就把它拼进 innerHTML。HTML 转义工具

同一书名原样拼入HTML会形成节点,转义后显示文字,JSON数据通过textContent显示也不创建图像标签

3. 当前用户与列表分别填入页面

# 续写同一文件:web_project/src/webbook/render.py
# 根据登录状态选择表单和操作按钮;把用户文本转义后再放入 HTML。
def render_home(session, books, message="", values=None):
    values = values or {}
    if session.user_id is None:
        auth = '<a class="button" href="/login">登录书架</a>'
        create = '<p class="empty">登录后添加记录,列表只展示当前账号的书籍。</p>'
    else:
        auth = (
            f"<span>当前账号:{escape(session.username)}</span>"
            f'<form method="post" action="/logout">{hidden_csrf(session)}<button class="quiet">退出登录</button></form>'
        )
        create = render_page(
            "book_form.html",
            csrf=hidden_csrf(session),
            title=escape(str(values.get("title", "")), quote=True),
            tags=escape(str(values.get("tags", "")), quote=True),
            checked="checked" if values.get("finished") == "1" else "",
        )
    rows = []
    for book in books:
        tags = "".join(
            f'<span class="tag">{escape(tag)}</span>' for tag in book["tags"]
        )
        action = (
            '<span class="done">已读</span>'
            if book["finished"]
            else f'<button class="finish quiet" data-id="{book["id"]}">标记已读</button>'
        )
        rows.append(
            f'<li data-book-id="{book["id"]}"><div><strong>{escape(book["title"])}</strong><div class="tags">{tags}</div></div>{action}</li>'
        )
    return render_page(
        "home.html",
        auth=auth,
        create=create,
        rows="".join(rows) or '<li class="empty">还没有记录</li>',
        message=escape(message),
        csrf=escape(session.csrf, quote=True),
        count=str(len(books)),
        finished=str(sum(book["finished"] for book in books)),
    )

服务器先生成账号区、表单区和书籍列表。代码里的固定 HTML 片段由程序提供,书名、标签、用户名和错误信息在进入对应位置前单独转义。

<!-- 项目文件:web_project/src/webbook/templates/home.html -->
<!-- 固定页面结构接收渲染后的片段;用户输入在render.py中按HTML位置转义。 -->
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="csrf-token" content="$csrf">
  <title>阅读书架 · Web 基础</title>
  <link rel="stylesheet" href="/assets/app.css">
  <script src="/assets/app.js" defer></script>
</head>
<body>
  <main>
    <header><div><p class="eyebrow">PYTHON 3.12 / WEB 练习</p><h1>阅读书架</h1><p>保存一本书,再观察请求与记录的变化。</p></div><nav>$auth</nav></header>
    <div class="metrics"><span>当前书籍 <strong>$count</strong></span><span>已读 <strong>$finished</strong></span><span>存储 <strong>SQLite</strong></span></div>
    <p class="message" role="status">$message</p>
    <div class="columns">
      <section><h2>添加书籍</h2><p class="sub">表单提交后,服务器保存记录并跳转回书架。</p>$create</section>
      <section><h2>我的书籍</h2><ul id="books">$rows</ul></section>
    </div>
    <section class="api"><h2>同一份记录的 JSON</h2><p class="sub">按钮请求 /api/books,页面脚本把响应作为文本显示。</p><button id="load-api" type="button">读取 API</button><span id="api-status" role="status"></span><pre id="api-output">点击按钮后显示响应。</pre></section>
  </main>
</body>
</html>

link 加载 app.css 负责字号、颜色与布局,script 加载 app.js 负责浏览器中的操作;defer 让脚本等页面结构解析完成后执行。meta 保存当前页面令牌,列表里的 data-id 保存按钮对应的书籍编号。

浏览器收到的是本次渲染后的 HTML;Python 的循环已经在服务器完成,不会把 Python 解释器带到页面里。

笔记正文与全部题解 · Ctrl / ⌘ K