资讯动态

FastAPI模板渲染利器:Jinja2过滤器完全指南

发布时间:2026/9/10 6:22:35 来源:尧图企业网站定制
用FastAPI做模板渲染的人迟早会遇到这么个场景接口数据取出来了可真要把它塞进HTML页面时才发现后端算好的东西跟页面要展示的东西之间还差了一大截格式化工作。时间戳要显示成“2025-06-18 14:30”文章摘要要从正文里截阅读量过万要显示成“1.2万”标签列表要用顿号串起来。这些脏活累活在Jinja2模板里都有一个统一的出口——过滤器。这篇系列第14篇就把这个模板语法里的过滤器讲透内置的怎么用、FastAPI项目里怎么注册自定义过滤器、哪些场景不该用过滤器以及我在实际项目里踩过的坑。无论你是刚开始用FastAPI还是已经在生产环境跑了一段时间这篇都值得从头扫一遍很多细节容易被忽略但对排查问题却非常关键。1. 过滤器是模板引擎的“格式化工坊”1.1 一条竖线背后的设计逻辑Jinja2中的过滤器语法很简单就三种形态{{ 变量|过滤器名称 }} {{ 变量|过滤器名称(参数) }} {{ 变量|过滤器1|过滤器2|过滤器3 }}一条竖线|把它左边的值“传”给右边的过滤器函数函数处理完的结果再交给下一个过滤器或直接渲染。这个设计很容易理解原始数据是原材料过滤器是一道道加工工序竖线是传送带。模板里想展示什么形态就挂上对应的工序。为什么要这么设计因为Web页面要展示的数据几乎从来不等于后端查出来的原始值。我从数据库里取出一个字段create_time是datetime对象页面却要显示成“2025-06-18 14:30”列表页里文章正文是一大段HTML卡片上只需要60字的纯文本摘要用户填的昵称首字母不够整齐想统一转成大写。这些变换如果全部放在视图函数里做视图会被塞满格式化代码模板又变成一坨光秃秃的变量如果在模板里挨个写if/else判断维护起来会更痛苦。过滤器把“取值”和“展示”彻底分开视图层只管给原始数据展示层自己去加工。1.2 过滤器、模板方法调用与全局函数到底怎么选有人可能会问模板里也能直接调用对象的Python方法比如{{ username.upper() }} {{ tags.count() }}这看起来和过滤器几乎一样为什么还要额外学一套过滤器语法我的经验是能写过滤器就不写方法调用。原因有三。第一对象方法受类型限制username.upper()只对字符串有效换成别的类型要么报错要么得先做类型判断而过滤器对任意输入更宽容很多内置过滤器本身就做了类型兜底。第二过滤器能链式组合一个竖线一个竖线接下去方法调用嵌套起来可读性差很多。第三过滤器可以被覆盖和注册你可以注册一个同名过滤器改变全局行为比如模板里所有|length都输出成“共N项”这种替换能力方法调用给不了。至于全局函数比如range()、namespace()它的使用场景和过滤器完全不同。全局函数通常是“凭空生成一个值”过滤器是“把一个已有的值变成另一个形态”。如果你发现自己想在模板里做一段比较复杂的数据加工例如先对列表筛选再排序再取前三条优先考虑在视图层算好直接把结果传给模板而不是在模板里叠一堆过滤器。模板终究是展示层不是业务逻辑层。2. 内置过滤器分类速查与实战示例2.1 文本处理大小写、替换、截断与去标签文本处理是过滤器最家常的应用。Jinja2内置了upper、lower、title、capitalize、trim、replace、truncate、striptags、wordwrap等一连串文本过滤器。直接看用例{{ name|upper }} {# 转大写 #} {{ name|lower }} {# 转小写 #} {{ name|title }} {# 每个单词首字母大写 #} {{ name|capitalize }} {# 整句首字母大写 #} {{ title|trim }} {# 去掉首尾空白 #} {{ 我-是-关键词|replace(-, ) }} {# 替换字符 #}实际项目里最常用的是truncate和striptags我单独拉出来说。truncate用来截断字符串常用写法是{{ article.content|truncate(80, killwordsTrue, end…) }}它的三个核心参数第一个是最大长度默认255第二个killwords决定是否在单词中间硬切第三个end是截断后追加的字符串默认是英文省略号。这里有个细节truncate在计算长度时把end的长度也算进去也就是说如果你设end…最终展示的字符数会比80略少一点。对中英文混排的页面不要过度依赖truncate的精确截断真正严格按字数控制标题的地方我更建议后端先算好摘要再传过来。striptags会剥掉字符串里所有HTML标签。文章列表页需要纯文本摘要时先striptags再truncate是非常经典的一条管道{{ article.content|striptags|truncate(80, end…) }}这个组合我在博客、管理系统、移动端接口的标题生成上都用过效果稳定。要注意的是striptags只是去标签不负责过滤XSS如果要展示用户提交的含标签内容还得配合转义一起处理。2.2 数值处理round与filesizeformat数值类过滤器在统计页面和后台管理页面上特别常用。内置的有int、float、round、abs、filesizeformat。round过滤器负责处理小数精度常配合method参数使用{{ 3.1415926|round(2) }} {# 3.14 #} {{ 3.1415926|round(2, methodceil) }} {# 3.15 #} {{ 3.1415926|round(2, methodfloor) }} {# 3.14 #}method默认是common即四舍五入可选ceil向上取整和floor向下取整。做分页、金额计算时这两个取值方式很有用比如计算订单页数时我习惯用total // page_size|round(0, methodceil)确保最后一页也能被算进去。当然遇到真正需要严谨计算的场景我更推荐在后端用math.ceil算好再传给模板模板里的round更适合做展示层的“差不多就行”。filesizeformat会把字节数显示成友好格式{{ 1048576|filesizeformat }} {# 1.0 MB #} {{ 1024|filesizeformat }} {# 1.0 KB #}做文件上传列表、对象存储管理页时这个过滤器能省掉一长串换算逻辑。2.3 列表、字典与集合排序、分组、拼接与切片列表类过滤器处理的对象是数组常见的包括first、last、length、sum、sort、reverse、unique、join、batch、slice。几个典型用例{{ users|first }} {{ users|last }} {{ users|length }} {{ [1, 2, 3, 4]|sum }} {{ users|map(attributename)|join(, ) }} {{ tags|sort }} {{ tags|reverse|first }}join过滤器非常实用它会把列表元素拼成一个字符串{{ tags|join(、) }} {{ users|join(, attributename) }}第二个例子是把每个user对象的name字段取出来再拼接免去了先map再join的两层写法。sort过滤器还可以指定排序字段和方向。按发布时间倒序展示文章列表{% for article in articles|sort(attributepublished_at, reverseTrue) %} ... {% endfor %}groupby按字段分组适合做“按分类展示文章”“按日期归档”这类页面{% for group in articles|groupby(category) %} h2{{ group.grouper }}/h2 {% for article in group.list %} p{{ article.title }}/p {% endfor %} {% endfor %}batch和slice是按数量分块常用于栅格布局比如每行三列卡片{% for row in products|batch(3) %} div classrow {% for product in row %} div classcol{{ product.name }}/div {% endfor %} /div {% endfor %}2.4 缺省值、转义与安全default、safe与tojsondefault可能是整个Jinja2里使用频率最高的过滤器之一几乎所有列表页都会用到。作用很简单当变量不存在或者值为None时输出你指定的默认值。{{ user.nickname|default(游客) }}但这里有一个几乎人人都会踩的坑当变量的值是空字符串、0、False这类“假值”时默认的default并不会替换它。比如用户把昵称清空后存了空字符串页面依然显示空白而不是“游客”。想要让空字符串也走默认值必须加booleanTrue{{ user.nickname|default(游客, booleanTrue) }}booleanTrue会把左边的值先按布尔判断只有真值才保留否则直接用默认值。做面向用户的展示层时我基本上都会带上booleanTrue除非业务上明确区分“没填”和“填了但为空”。转义和安全类是过滤器里最需要谨慎对待的部分。Jinja2默认会在输出HTML时自动转义因此{{ name }}里如果包含script会被转成lt;scriptgt;。escape过滤器就是手动强制转义但绝大多数情况不需要手动做因为默认已经开了。和它相反的是safesafe会告诉模板引擎“这个字符串可以按原始HTML输出不要转义”。safe是把双刃剑。模板里展示后端生成的富文本时确实需要safediv{{ article.content_html|safe }}/div但如果是未经深思熟虑就把用户提交的HTML直接safeXSS就会找上门。在后面的踩坑章节我会专门展开这里先记住一个原则safe只能用在你完全信任的数据上。tojson过滤器是另一个常用的安全类工具它能把Python对象序列化成JSON字符串并且自动做HTML转义适合在模板中直接把后端数据传给前端JavaScriptscript const appData {{ app_data|tojson }}; /script不用tojson的话你可能会手写json.dumps然后担心特殊字符把页面搞坏用tojson就稳很多。2.5 链式组合的读法过滤器真正的威力来自组合。一条管道从左往右读先执行的在前、后执行的在后{{ article.summary|default(暂无摘要)|striptags|truncate(60, end…) }}这条管道的意思是取summary如果为空就用“暂无摘要”去掉可能存在的HTML标签再截断成60字。整行代码读下来处理流程一目了然。我建议每个过滤器只做一件事这样链条再长也不会混乱。一旦发现某条链上要写超过三四个过滤器或者中途需要if判断就该考虑在后端提前处理好了。3. FastAPI里把过滤器真正用起来3.1 最小可运行的模板渲染环境先搭一个能在FastAPI里跑通Jinja2的最小结构。工程里我一般是这样组织project/ ├── main.py └── templates/ └── index.htmlmain.py内容from fastapi import FastAPI, Request from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse app FastAPI() templates Jinja2Templates(directorytemplates) app.get(/, response_classHTMLResponse) async def index(request: Request): context { username: ada, score: 87.345, tags: [FastAPI, Jinja2, Templates], } return templates.TemplateResponse( requestrequest, nameindex.html, contextcontext, )这段代码里有两个容易搞混的细节。第一FastAPI新版本推荐把request作为TemplateResponse的第一个参数传入context参数里就不用再写request了旧版本常见写法是把request塞进context字典形如templates.TemplateResponse(index.html, {request: request, ...})。两种写法在对应版本都能工作但如果你照着网上旧教程抄到新版本里会看到类型签名相关的报错或警告。第二response_classHTMLResponse是为了让接口文档能识别响应类型不写也能返回HTML但写了更清晰。3.2 模板里的过滤器实际渲染templates/index.html!DOCTYPE html html head title{{ username|title }}/title /head body h1Hello, {{ username|title }}/h1 p分数{{ %.2f|format(score) }}/p p标签{{ tags|join(、) }}/p /body /html在模板里{{ }}之间的内容就是最终会被渲染的表达式。过滤器在这里正常工作Jinja2引擎会在服务端把所有管道计算完毕、生成一段纯HTML字符串后返回给浏览器。你在浏览器里看到的是处理后的结果而不会看到|title这种语法残留。这也意味着如果过滤器写错了浏览器不一定立刻报错可能只是显示了格式不对的内容这时候要回到模板代码本身排查。3.3 request在模板环境中的位置使用Jinja2Templates渲染时Starlette会把request注入到模板上下文里所以模板中可以直接访问request.method、request.url、request.client.host等属性。过滤器同样可以处理这些值比如显示当前请求路径或者用replace把URL里的参数拼成更友好的展示。不过这里要提醒一句模板里的request自动注入并非Jinja2原本的特性而是Starlette的Jinja2Templates做的封装。理解这一点对排查问题很有帮助比如当你脱离FastAPI单独用Jinja2时发现模板里根本没有request这个变量不用惊讶。4. 自定义过滤器注册与调用4.1 哪些场景必须自定义过滤器内置过滤器虽然多但真实的业务永远比通用功能刁钻。我项目里比较典型的自定义过滤器场景包括时间显示成“刚刚 / 5分钟前 / 昨天 / 2025-06-18”阅读量、浏览量从12345显示成“1.2万”手机号、身份证号脱敏比如138****8888把Markdown文本渲染成纯文本摘要把枚举值转成中文状态status|status_label给数字加千分位1234567显示成1,234,567这些逻辑简单、重复使用、只影响展示形态非常适合做成过滤器。如果哪天多个模板都要用同一个格式化规则直接注册一个过滤器比在每个视图函数里复制粘贴代码要优雅得多。我这里特别强调“展示形态”这四个字——如果某个逻辑涉及状态修改、数据写入或者计算结果会作为后续业务的判断依据那就不该用过滤器老老实实放后端。4.2 在FastAPI中注册自定义过滤器的具体写法注册过滤器的本质就是往Jinja2环境对象的filters字典里塞一个函数。FastAPI里我们拿到Jinja2Templates实例后通过templates.env就能访问到那个Jinja2 Environment。第一种写法直接给env.filters赋值def format_views(value): if value is None: return 0 value int(value) if value 10000: return f{value / 10000:.1f}万 return str(value) templates.env.filters[views] format_views第二种写法用Jinja2 Environment自带的filter装饰器templates.env.filter def time_ago(value, nowNone): if value is None: return if now is None: now datetime.utcnow() delta now - value if delta.days 30: return f{delta.days // 30}个月前 if delta.days 1: return f{delta.days}天前 if delta.seconds 3600: return f{delta.seconds // 3600}小时前 if delta.seconds 60: return f{delta.seconds // 60}分钟前 return 刚刚第二种写法的函数名会直接成为模板中的过滤器名函数名得起得干净利落。我更喜欢把自定义过滤器集中放在单独的modules/filters.py里统一管理而不是散落在各个路由文件里# modules/filters.py def format_views(value): ... def time_ago(value): ... def register_template_filters(env): env.filters[views] format_views env.filters[time_ago] time_ago return env然后在main.py里先创建Jinja2Templates再注册templates Jinja2Templates(directorytemplates) register_template_filters(templates.env)这种拆分的好处是项目里的过滤器越来越多时不会污染主路由文件测试也好写。模板里直接这样用p{{ article.views|views }}/p p{{ article.published_at|time_ago }}/p4.3 带参数的过滤器与链式调用自定义过滤器还能接收额外参数。函数签名里第一个参数永远是被管道传入的变量后面的参数来自|filter(param1, param2)调用。举个例子手机号脱敏过滤器def mask_phone(value, start3, end7, mask_char*): if not value: return value str(value) return value[:start] mask_char * (end - start) value[end:]模板里的使用方式{{ user.phone|mask_phone(3, 7) }} {{ user.id_card|mask_phone(3, 12) }}参数还可以带默认值这样部分场景可以不传参数。把自定义过滤器和内置过滤器串起来也很常见比如

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价