PythonのテンプレートエンジンJinja2を使ってみた

Jinja2はPythonDjangoウェブフレームワークに強く影響を受けて開発された、テンプレートエンジンらしいです。テキストベースのファイルであればなんでも出力することができるのでHTMLに加え、CSVXMLレンダリングすることができますね。やろうと思えばできますが、JSONYAMLももちろんレンダリングできます。

環境

以下の環境で試しに使ってみます。

bash-3.2$ python --version
Python 3.8.6

bash-3.2$ pip list
Package    Version
---------- -------
Jinja2     2.11.2
MarkupSafe 1.1.1
pip        20.2.3
setuptools 50.3.0

サンプル

Jinja2の基本的な使い方は Loader クラスでテンプレートファイルの読み込み方を決めて、その他の設定とともに Environment クラスをインスタンス化します。んで、 Template クラスを生成してもらって必要な引数を render メソッドに渡してレンダリング結果を文字列として受け取る、という流れです。

from jinja2 import Environment, FileSystemLoader

# 1. テンプレートファイルの場所と、レンダリング時のtrim設定を行う。
#    Environmentクラスはjinja2の中心的なクラスで、以下のように
#    設定を元にTemplateインスタンスを生成する役割を担う。
env = Environment(
    loader=FileSystemLoader('src/first_example/templates'),
    trim_blocks=True
)

# 2. テンプレートファイルを取得しレンダリングする。
#    レンダリング結果はファイルに出力する。
template = env.get_template('message1.jinja')
result = template.render(name='BookStore')
with open('src/first_example/output/message1.txt', 'w') as f:
    f.write(result)

このときに使われたテンプレートファイルは以下になります。かなり単純な内容ですが。

src/first_example/output/message1.txt.

Hello, {{ name }} !

テンプレートでは {{ name }} の箇所が render メソッドの引数に応じて書き換わるという動きになります。 render メソッドは名前付き引数を任意の数受け取るのでテンプレートの {{ variable name }} に合わせて呼び出すことになるかと。

テンプレートファイル

jinja2 template designer

テンプレートファイルでは主に以下の3つの要素を使うことができます。StatementsとExpressionsだけ覚えておけば基本もの足りる印象でした。

  • {% …​ %} …​ Statements

  • {{ …​ }} …​ Expressions

  • {# …​ #} …​ Comments

上記の要素を使ってテンプレートを書くとこうなります。これはHTMLファイルのレンダリングですね。ステートメントではfor文を定義し、リストで渡されるであろう値をレンダリングしてます。

<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<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>

以下のようにif文もテンプレート内に記述できます。 username が定義されているか、されていないかを判断してレンダリングする内容を切り替えます。

{% if username is defined %}
    username is : {{ username }}
{% else %}
    username not defined
{% endif %}

パイプ処理を行うように、フィルタリングも行うことができます。フィルタリングは以下のように | を使って関数をつなげていきます。処理の順番は一番左に書かれた変数を左から順に関数に適用して、最終的な結果の文字列をレンダリングします。以下はHTMLの文字列をエスケープ処理し、各単語の戦闘を大文字に変換するフィルタリングになります。

{{ name|striptags|title }}

フィルタリングで使用できるビルトインの関数はここに載ってます。

jinja2 builtin filters

他にもいろんな機能があるみたいなんですが、今のところ触ってみたのはここまでですね。

Loader

Loader はテンプレートファイルをロードする役割を持つもので Environment の引数の一つです。いくつか種類があり、以下のものを試しに使ってみました。サンプルコードは unittest モジュールを使って単体テスト形式で書いてます。

  • FileSystemLoader

  • PackageLoader

  • DictLoader

  • FunctionLoader

  • PrefixLoader

# Loaderはテンプレートを読み込む役割を持ちます。
# jinja2.BaseLoaderを継承したクラスがLoaderとして扱われ、標準で用意されているものもあれば独自に作ることもできます。

import unittest

import jinja2


class ExamplesJinja2Loaders(unittest.TestCase):

    def test_FileSystemLoader(self):
        # ファイルシステムからテンプレートを読み込みます。
        loader = jinja2.FileSystemLoader('mypackage/views')
        template = jinja2.Environment(loader=loader).get_template(name='template1.jinja')
        self.assertEqual(template.render(message='Hello'), 'Hello')

    def test_PackageLoader(self):
        # パッケージからテンプレートを読み込みます。
        loader = jinja2.PackageLoader(package_name='mypackage', package_path='views')
        template = jinja2.Environment(loader=loader).get_template(name='template1.jinja')
        self.assertEqual(template.render(message='Hello'), 'Hello')

    def test_DictLoader(self):
        # dictによりテンプレートファイルのソースを読み込みます。
        # testなどで使用する想定らしいです。
        loader = jinja2.DictLoader({'message_template': '{{ message }}'})
        template = jinja2.Environment(loader=loader).get_template('message_template')
        self.assertEqual(template.render(message='Hello'), 'Hello')

    def test_FunctionLoader(self):
        # 関数によりテンプレートを読み込みます。
        # 関数にはテンプレート名が渡されるので、その名前をもとにテンプレート文字列を返します。
        # その他にも(source, filename, uptodatefunc)のタプル値を返すこともできます。
        def load_template(name):
            if name == 'template1.jinja':
                with open('mypackage/views/template1.jinja', 'r') as f:
                    return f.read()
            else:
                return None

        loader = jinja2.FunctionLoader(load_template)
        template = jinja2.Environment(loader=loader).get_template(name='template1.jinja')
        self.assertEqual(template.render(message='Hello'), 'Hello')

    def test_PrefixLoader(self):
        # テンプレート名を指定する際のプレフィックスで、Loaderを変更できます。
        # 例えば複数のFileSystemLoaderを組み合わせれば異なるディレクトリからテンプレートを読み込めます。
        loader = jinja2.PrefixLoader({
            'app1': jinja2.FileSystemLoader('mypackage/views'),
            'app2': jinja2.FileSystemLoader('templates')
        })
        template = jinja2.Environment(loader=loader).get_template(name='app2/template1.jinja')
        self.assertEqual(template.render(message='Hello'), 'Hello')


if __name__ == '__main__':
    unittest.main()

ちょっとしたツールを作るのであればFileLoaderで十分かなと。テンプレートファイルが複数のディレクトリに分散するのであればPrefixLoaderを使うと便利そうですね。

おわりに

試しに使ってみましたが、なにかテキスト形式のものを生成するには使いやすそうだなと思いました。Ansibleでも使われているみたいですし、Python界隈では割とポピュラーなモジュールなのかな?