jinja2 和 python-docx-template 光速入门

有根据模板去渲染和打印 word 的需求,因此研究一个。本来是想用 HTML 渲染再打印的,但发现用 word 更方便。

python-docx-template 是 python 的一个 docx 模板引擎库,它依赖 python-docx 去读写 docx 文件内容,依赖jinja2 进行模板渲染。它就像 Java 中的poi-tl(恰巧我最后一个需求刚用到它)。

这里也说了——它依赖 jinja2 模板引擎,所以需要学一下 jinja2 的语法。

安装

1
pip install docxtpl

jinja2

jinja2 是一个文本模板引擎,它一般上是用来生成 html 的,但并不和 html 绑定,因此它不像 vue 那样,模板语法也是合法 html,而是像 poi-tl 那样(原谅我没学过其他的模板引擎),模板和内容解耦。

jinja2 这么使用(虽然我不会这么直接用它啦):

1
2
3
from jinja2 import Template
tpl = Template("""Hello, {{name}}""")
print(tpl.render({'name': 'Yuuki'})) # Hello, Yuuki

接下来,快速把所有玩意儿都过一遍!

首先,有三种基础语法,是容易理解的:

  • {{ ... }}:插值表达式
  • {% ... %}:控制结构
  • ``:注释

插值

jinja2 支持插值任意合法 Python 表达式(有一定限制):

1
2
3
4
5
6
{{ name }}
{{ user.name }}
{{ user["name"] }}
{{ items[0] }}

{{ first_name.strip() + ' ' + last_name.strip() }}

注意——user.name等价于user['name'],jinja 在这方面不关心索引访问和 getattr 访问(当然,肯定是按照一个优先级顺序去访问的),就像 js 一样,这个很方便——你传 dataclass 还是传 dict,一样的用。

测试器

上面说 jinja2 支持任意 python 表达式?不全是,比如这里提出一个示例——jinja2 不支持 python 的 is 操作符(实际上我们在实践中,is 完全是用来判 None 的,所以在模板引擎中这个行为基本无意义)。jinja2 重新定义 is——is 的右边是所谓的测试器,测试器是可以自己定义的,这里不表。示例见下:

1
{{ name is string and '123' }} {# 结果是 123 如果为真,否则渲染 False #}

jinja2 的 is 同样有两种形式——is 和 is not。

下面是常用测试器:

name desc
none is None
string 类型是 str
number 类型是 int 或 float
sequence 序列(非字符串)
sameas(value) 对应 python 的 is
equalto(value) 对应 ==,jinja2 有==,但如果配置不容忍 undefined,==遇上未定义会报错,这里不会
defined 是否已定义(变量是否存在)
就这些足够用啦,还需要咋样?

过滤器

jinja2 支持所谓的 过滤器 Filter| 是管道,就像 unix 的管道,这里的意思是为 name 使用 trim 过滤器,然后再使用 capitalize 过滤器。过滤器实际上是 python 中定义的函数,我们自己也可以定义过滤器,但这里不表。

1
My name is {{ name|trim|capitalize }} {# 也可以认为对应 kt 代码: name.let(trim).let(capitalize) #}

内置的过滤器有:

name desc
default(value) 默认值,注意用法:{{ name \| default('Anonymous') }},它行为是一个柯里化函数
escape / safe safe 表示转义 html,默认似乎是不转义的(可配置),escape 表示转义
join(delimiter) 字面意思,用 delimiter join 一个集合
truncate(length) 把太长的字符串截断,尾部显示 ...
过火了,剩下的有需求看文档,本来就不该在模板中塞太多业务逻辑,仅供显示用

jinja2 支持 宏 Macros,宏就像函数,你给它参数,它给你结果。宏必须先定义后调用:

1
2
3
4
5
{% macro input(name, type='text') -%}
<input type="{{ type }}" name="{{ name }}">
{%- endmacro %}

{{ input('username') }}

控制结构

对于一个模板引擎,只需要两种控制结构,if 和 for。

条件逻辑

同 python 的,注意需要一个 endif 块表示结尾,for 也是如此。

1
2
3
4
5
6
7
{% if name is defined and name is string %}
Hello, {{ name }}
{% elif guest %}
Hello, Guest!
{% else %}
who the fk r u?
{% endif %}

循环

循环和 python 的区别在于,循环中有一个内置的 loop 变量,每轮循环时做更新:

  • loop.index:下标,以 1 开头
  • loop.index0:下标,以 0 开头
  • loop.first, loop.last:是否是第一项,是否是最后一项
  • loop.revindex:剩余项数,为 length - loop.index0
1
2
3
{% for item in items %}
{{ loop.index }}: {{ item }}
{% endfor %}

这里的 item 也是可以被解构的,但这里不表。

变量定义

有两种变量定义——set 和 with,都是容易理解的:set 的作用域是整个模板,除非它定义在 macro 中,with 的作用域是 with 块内。注意 set 能使用 python 的解包语法,而 with 则更加……传统,但方便。

1
2
3
4
5
6
7
8
9
{% set x, y = 1, 2 %}
{{ x }} {{ y }}
{% set x = x + 100 %}
{{ x }}

{% with a = [1,2,3]|length, b = 'hello' %}
length of [1, 2, 3]: {{ a }}
{{ b }}
{% endwith %}

怎么说呢?jinja2 学完这些感觉就足够啦!回到 docxtpl。

docxtpl

先来个 HelloWorld:

1
2
3
4
5
6
from docxtpl import DocxTemplate

doc = DocxTemplate("my_word_template.docx")
context = { 'company_name' : "World company" }
doc.render(context)
doc.save("generated_doc.docx")

一些扩展,一些坑

docxtpl 对 jinja2 做了一些更严格的限制和扩展。

关于限制:jinja2 的 tag 内容,必须处在同一个段落的同一个 run 中,即 {{ name }} 这整个内容必须处在同一个 run 中,且不能换行。

run,指的是 word 中为同一个格式的内容的文字的集合,就像 html 和 markdown 中的各种标签。这是可以想见的——必须要“批处理”以减少重复和浪费。而像{{ name }}这样的标签如果跨多个 run,docxtpl 就会迷糊——我究竟用前面的 run 还是后面的 run 呢?

这里有一个重要的坑:像这样 `


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!