🔦

Jinja2を使ったテンプレート記法メモる

2024/08/02に公開

概要

いろいろなフレームワークで使われているJinja2テンプレート、使い方は難しくはないもののその場しのぎで利用している気がしてきたためフレームワークに依存しない使い方についてまとめる。

組み込めるフレームワークの例

個人的に使いそうな範囲だと

  • Flask
  • Django
  • FastAPI

FlaskとDjangoはJinjaのページに記載されていた[1]。Integrationということだから、追加で設定しなくても使えるということなのだろうと思う。

準備

インストールのコマンド

pip install Jinja2

コード

テンプレート

### template.jinja
<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">
    {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
    {% endfor %}
    </ul>
    <h1>My Webpage</h1>
    {{ a_variable }}
    {# a comment #}
</body>
</html>

ステートメント {% ... %}
印刷 {{ ... }}
コメント(出力されない) {# ... #}

イテラブルな navigation から引っ張った要素を item としてリンク先を item.href 、表示文字列を item.caption で表現している。
このテンプレートを使った出力がどのようになるかを確認する。

Jinja2で処理した結果を出力するコード(CLI)

テンプレートにJinja2でpythonデータを埋め込んだ文字列を出力するpythonコード:

### render_template.py
import sys
from jinja2 import Template


def read_template_from_stdin():
    return sys.stdin.read()

def read_template_from_file(filename):
    try:
        with open(filename, 'r', encoding='utf-8') as file:
            return file.read()
    except FileNotFoundError:
        print(f"Error: File '{filename}' not found.")
        sys.exit(1)

if len(sys.argv) > 1:
    # コマンドライン引数からテンプレート文字列を取得
    template_string = read_template_from_file(sys.argv[1])
else:
    template_string = read_template_from_stdin()

# データ
class NavItem:
    def __init__(self, link:str, caption:str):
        self.href = link
        self.caption = caption
nav_list = [
    NavItem(link="aaa.html", caption="caption A"),
    NavItem(link="bbb.html", caption="caption B"),
    NavItem(link="ccc.html", caption="caption C"),
]
my_desire = 'I want some money!'

# データとして使用する辞書を定義
context = {
    "navigation": nav_list,
    "a_variable": my_desire
}

# テンプレートを作成
template = Template(template_string)

# テンプレートをレンダリング
output = template.render(context)

# 結果を出力
print(output)

このpythonコードにJinja2テンプレートファイル名または文字列を渡して実行する。
この実行をレンダリングというらしい?
コードの説明:
2つの関数read_template_from_xxx()は引数からJinja2テンプレートの文字列を取り出す。
作業中パイプ入力からも取り出したくなったので若干趣旨から外れた実装が含まれてしまっている。
引数が存在したらファイルが指定されたと見做し、なかったらパイプと判断。
パイプ出力をせずに引数無しで実行するとstdinの入力待ちになるが、このときにテンプレート文字列を入力してからCtrl+Dして実行後にテンプレート文字列を入力することもできる。何も入力せずにCtrl+Dするとレンダリングせずに終了する(Ctrl+Dで標準入力を確定することを知らなかった)。
NavItemクラスはテンプレートに渡すデータ用に用意したクラスで、リンク先URLとアンカーテキストに相当する属性を持たせている。この要素からなるリストを辞書contextの値として使うことでテンプレートにデータを渡せるようにする。contextのkeyはテンプレート内で利用しているプレースホルダーと一致させる。一般的な?開発においてはコアプログラムが先に作られる場合もあるのでテンプレートのプレースホルダーに合わせるのかプログラムのデータに合わせるのかはケースバイケース。

コードの実行

ファイルを用意した後に

cat template.jinja | python render_template.py

あるいは

python render_template.py template.jinja

とすると、Jinja2によって処理された文字列が表示される。

処理結果

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">
    
        <li><a href="aaa.html">caption A</a></li>
    
        <li><a href="bbb.html">caption B</a></li>
    
        <li><a href="ccc.html">caption C</a></li>
    
    </ul>
    <h1>My Webpage</h1>
    I want some money!
    
</body>
</html>

{# ... #}の部分はレンダリング結果には出力されない。
目的は達しているものの、空行をどうにかできないものか?

空行を削る

できるらしい。
forステートメント部分を

    {% for item in navigation -%}
        <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
    {% endfor %}

のように-%}で閉じると下記のような出力が得られた。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">
    <li><a href="aaa.html">caption A</a></li>
    <li><a href="bbb.html">caption B</a></li>
    <li><a href="ccc.html">caption C</a></li>
    
    </ul>
    <h1>My Webpage</h1>
    I want some money!
    
</body>
</html>

https://jinja.palletsprojects.com/en/3.1.x/templates/#whitespace-control

Jinja2を使ったhtmlコード共通化

前項までで基本的な使い方には困らないかと思ったものの、いざアプリで使うときにhtmlファイルを用意するときにいつも考えるコードの共通化について解決策があるのかもしれないと思い公式ページを彷徨っていたところ、継承という言葉を見かけた。これを使えば実現できるのか、試してみることにした。

親テンプレート

共通化したいコードは親テンプレートに記述する。
なお以降Jinjaテンプレートコードの拡張子はhtmlにする。
base.html:

<!DOCTYPE html>
<html lang="en">
<head>
    {% block head -%}
    <link rel="stylesheet" href="style.css" />
    <title>{% block title %}{% endblock %} - My Webpage</title>
    {% endblock %}
</head>
<body>
    <div id="content">{% block content %}{% endblock %}
    </div>
    <div id="footer">
        {% block footer -%}
        &copy; Copyright 2008 by <a href="http://domain.invalid/">you</a>.
        {%- endblock %}
    </div>
</body>
</html>

このテンプレートには4つのblockが用意されている。

  • block head
  • block title
  • block content
  • block footer

head block はlinkタグとtitleタグを持ち、さらに title block を内包している。派生先のテンプレートで選択的に利用可能なコードになる。選択的に利用可能とは派生先で同名ブロックを用意して super() を呼び出さないことで上書きすることが可能であるため。また当然ではあるが補足として、派生先で基本コードを使わないようにした場合、派生先で用意した title block は無視される。

content block は派生先テンプレートのメインコンテンツを挿入するプレースホルダーとなる。

footer block は派生先テンプレートに同名ブロックを使わなければ、または同名ブロック内でsuper()を呼び出せば挿入される。

子テンプレート

child.html:

{# ベースの指定 -#}
{% extends "base.html" %}
{# ページタイトルの設定 -#}
{% block title %}<!-- specific >>>> -->Index<!-- <<<< specific -->{% endblock %}
{% block head -%}
    {# ベースコードのheadブロックを挿入 -#}
    <!-- super() code >>>> -->
    {{ super() -}}
    <!-- <<<< super() code -->
    {# 固有のヘッダ -#}
    <!-- specific code >>>> -->
    <style type="text/css">
        .important { color: #336699; }
    </style>
    <!-- <<<< specific code -->
{%- endblock -%}
{# 固有のメインコンテンツ -#}
{% block content -%}
    <!-- specific code >>>> -->
    <h1>Index</h1>
    <p class="important">
      Welcome to my awesome homepage.
    </p>
    <p>{{ a_variable }}</p>
    <!-- <<<< specific code -->
{%- endblock %}
{#-
{% block footer -%}
{{ super() }}
{% endblock %}
-#}

コメント >>>> <<<< を追加することで処理結果にどのように反映されるのかを把握できるようにする

レンダリングスクリプト

render_template2.py:

import os
import sys

from jinja2 import Environment, FileSystemLoader

if len(sys.argv) == 1:
    basename = os.path.basename(__file__)
    print(f'usage:{basename} <template_child_file>')
    sys.exit(1)

# コマンドライン引数に指定されたファイル名からテンプレートローダーを作成
template_file_path = sys.argv[1]
template_dir = os.path.dirname(os.path.abspath(template_file_path))
loader = FileSystemLoader(template_dir)

# テンプレートに埋め込むデータ
my_desire = 'I want some money! to buy my favorite pizza...'

# データとして使用する辞書を定義
context = {
    "a_variable": my_desire
}

env = Environment(loader=loader)

# テンプレートを作成
template = env.get_template(template_file_path)

# テンプレートをレンダリング
output = template.render(context)

# 結果を出力
print(output)

実行

実行コマンド:

python render_template2.py child.html

実行結果:

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- super() code >>>> -->
    <link rel="stylesheet" href="style.css" />
    <title><!-- specific >>>> -->Index<!-- <<<< specific --> - My Webpage</title>
    <!-- <<<< super() code -->
    <!-- specific code >>>> -->
    <style type="text/css">
        .important { color: #336699; }
    </style>
    <!-- <<<< specific code -->
</head>
<body>
    <div id="content">
        <!-- specific code >>>> -->
    <h1>Index</h1>
    <p class="important">
      Welcome to my awesome homepage.
    </p>
    <p>I want some money! to buy my favorite pizza...</p>
    <!-- <<<< specific code -->
    </div>
    <div id="footer">
        &copy; Copyright 2008 by <a href="http://domain.invalid/">you</a>.
    </div>
</body>
</html>

派生テンプレートに記述したメインコンテンツ出力部分のインデントが気になる。けど解決策なさそうな気もするしあったとしても見る人誰よ?って考えると調べる労力と見合わなそうなので放置。

https://jinja.palletsprojects.com/en/3.1.x/templates/#template-inheritance

https://github.com/rifumi/learning_jinja2

脚注
  1. https://jinja.palletsprojects.com/en/3.1.x/integration/ ↩︎

Discussion