CGI を書いていると出力する HTML の中で定型の部分が多くあることに気付きます。例えば <!DOCTYPE html> から </head> までは、どのページでもそう多く変化することはないと思います。
CGI という仕組み上、最終的な出力は殆どが HTML です。そこに着目したのが PHP という言語で HTML の中にコードを埋めるという逆の発想で、これはこれで理に適っている仕組みです。静的なページに近い場合はこれでも良いかなと思いますが、ロジックを組むことが多いのでやっぱり Python で書きたい、でも保守性も考えたいとなったときに活躍するのがテンプレートエンジンです。
pip が使えたら、とりあえずインストールします。
$ pip install jinja2
jinja2.Template
最も基本的な使い方、単純に placeholder として使う場合です。
from jinja2 import Template
t = Template('こんにちは、{{ name }}さん')
print(t.render(name = 'foo'))
print(t.render(name = 'bar'))
実行結果
こんにちは、fooさん
こんにちは、barさん
{{ name }}
のスペースをなくして {{name}}
としても結果は同じですが、公式ドキュメントは全てスペースありで記述されているので統一します。
jinja2.Environment, jinja2.FileSystemLoader
HTML をプログラムから分離するのが今回のゴールなので、テンプレートは別ファイルとして用意します。ファイルからテンプレートを読み込むのに FileSystemLoader を使います。
from jinja2 import Environment, FileSystemLoader
env = Environment(loader = FileSystemLoader('./templates'))
t = env.get_template('base.tpl')
print(t.render(title = 'Sample Page', body = '<p>This is a Jinja2 sample page.</p>'))
templates/base.tpl
<title>{{ title }}</title>
<body>{{ body }} </body>
実行結果
<title>Sample Page</title>
<body><p>This is a Jinja2 sample page.</p></body>
エスケープ処理
鋭い方はもうお気づきだと思いますが、先の例では <p> タグがそのままテンプレートに埋め込まれています。これでは XSS の危険性があるので、エスケープ処理をする必要があります。Jinja2 には自動でエスケープ処理をしてくれる仕組みがあります。
先程のソースの Environment を作成するときに autoescape を指定するだけです。
from jinja2 import Environment, FileSystemLoader
env = Environment(loader = FileSystemLoader('./templates'), autoescape = True)
t = env.get_template('base.tpl')
print(t.render(title = 'Sample Page', body = '<p>This is a Jinja2 sample page.</p>'))
実行結果
<title>Sample Page</title>
<body><p>This is a sample page.</p></body>
select_autoescape() という、テンプレートの拡張子で autoescape をするかどうか決める仕組みもあります。import select_autoescape を加えて Environment の引数に autoescape = select_autoescape(('html.', 'xml'))
とすれば、テンプレートの拡張が html か xml のときだけエスケープが働きます。
テンプレート側で個別にエスケープ
漏れが出たときに XSS のリスクとなるので避けたいところですが、テンプレート側で一つ一つエスケープする方法もあります。
{{ body }}
{{ body|escape }}
{{ body|e }}
<p>This is a sample page.</p>
<p>This is a sample page.</p>
<p>This is a sample page.</p>
基本的に autoescape は有効にしておくべきですが、エスケープしてほしくない箇所があった場合は override して部分的に有効化および無効化できます。
テンプレート側の {% autoescape true %}
と {% endautoescape %}
で挟まれば部分は有効化され、逆に {% autoescape false %}
と {% endautoescape %}
で挟まれた部分は無効化されます。
テンプレートの書式
ここまでは Python 側の話でしたが、ここからはテンプレート側の話です。
Python の # と同じでコメントを記述できます。
for, if, macro/call, filter といった制御文を記述します。例えば for をつかって 1 から 5 までの連続した数値をリストアイテムとして出力する場合は次のようになります。
<ul>
{% for i in range(1, 6) %}
{{ i }}
{% endfor %}
</ul>
出力
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
元々 {% … %} があった部分は空行になって出力されます。空行の抑制は後述します。
変数の出力で使いますが、計算式を入れて計算結果を表示したりもできます。組込みのフィルタも多数用意されているため、値の加工も容易です。
辞書から値を取出すのに . 区切り形式を使えます。Python で dict['key']
とするところを dict.key
で表現できます。
空白と空行の制御
次のテンプレートと出力を基準として trim_blocks, lstrip_block, {%- … %} を指定したときの出力の違いを観察します。
<ul>
{% for i in range(1, 6) %}
<li>{{ i ** 2 }}</li>
{% endfor %}
</ul>
<ul>
<li>1</li>
<li>4</li>
<li>9</li>
<li>16</li>
<li>25</li>
</ul>
trim_blocks = True, lstrip_blocks = False
<ul>
<li>1</li>
<li>4</li>
<li>9</li>
<li>16</li>
<li>25</li>
</ul>
trim_blocks = False, lstrip_blocks = True
<ul>
<li>1</li>
<li>4</li>
<li>9</li>
<li>16</li>
<li>25</li>
</ul>
trim_blocks = True, lstrip_blocks = True
<ul>
<li>1</li>
<li>4</li>
<li>9</li>
<li>16</li>
<li>25</li>
</ul>
{%- … %}
trim_blocks = False, lstrip_blocks = False とした上で、ソースを下記のように変更。
<ul>
{%- for i in range(1, 6) %}
<li>{{ i ** 2 }}</li>
{%- endfor %}
</ul>
<ul>
<li>1</li>
<li>4</li>
<li>9</li>
<li>16</li>
<li>25</li>
</ul>
参考