BottleからJinja2を使ってみる

f:id:monozukuri-bu:20190226203217j:plain

Kibaraです。
主な仕事は神社仏閣を巡りおみくじを引くことです。

ということで、今回はJinja2を学んでいきたいと思います。

こちらを参考にさせていただいております。
Jinja2 Docs

環境

軽量なWebフレームワークであるBottleからJinja2を利用します。
Pythonの環境は以下の通りです。

>python --version
Python 3.7.1

インストール

bottle.pyは1ファイルなので、どこかから入手するか、
condaの場合はconda-forgeを利用します。

# インストール
> conda install -c conda-forge bottle
> conda install jinja2

# バージョン確認
>conda list bottle
bottle                    0.12.16                    py_0    conda-forge
>conda list jinja2
jinja2                    2.10                     py37_0

Jinja2とは

テンプレートエンジンです。
Webフレームワーク等からKey-Valueな形でデータを受け取り、
htmlに展開するような機能を持ちます。

例えば以下のように、htmlの中にPythonっぽいコードが書けます。

<html>
  <head>
    <title>{% title %}</title>
  </head>
  <body>
    <ul>
      {% for user in users %}
        <li><a href="{{ user.url }}">{{ user.username }}</a></li>
      {% endfor %}
    </ul>
  </body>
</html>

上の例では、htmlのみだとユーザーの数だけリストを書かなければなりませんが、
usersでリスト型のデータを渡すことで、動的に生成することができます。

また、変数の中身が変われば、表示されるhtmlの内容も変わります。
ベースとなるデザインをhtml/cssで記述し、
メインのコンテンツはテンプレートエンジンで動的に生成する、
といったことが可能になります。

Jinja2の基本文法

基本的な書き方を見ておきます。

変数

{{ 変数名 }}

分岐

{% if 条件式 %}
 ~~~
{% elif 条件式 %}
 ~~~
{% endif %}

繰り返し

{% for 変数 in リスト %}
 ~~~
{% endfor %}

ブロック

{% block ブロック名 %}
 ~~~
{% endblock %}

コメント

{# コメント #}

これでプログラムの基本原則である、順次、分岐、反復を習得しました。
完璧ですね。もう敵はいない。

動かす

では、サクッと動かしてみます。

bottleでサーバを起動

早速テンプレートを…と言いたいところですが、
まずはbottleでサーバーを立ち上げます。

from bottle import Bottle, run
from paste import httpserver as web

app = Bottle()

@app.route('/')
def top():
    return "top"

run(app=app, host='127.0.0.1', port=8080, server='paste')

f:id:monozukuri-bu:20190226203807p:plain

起動できました。

bottleはデフォルトでシングルスレッドですが、
serverにpasteを指定してマルチスレッドで動作させています。
今回の場合は特に意味はありません。(紹介のため)

シンプルなテンプレート

ベースとなるHTMLファイルと、テンプレートを記述するファイルを作成しましょう。
templateディレクトリを作成し、index.htmlとindex.j2を作成します。

f:id:monozukuri-bu:20190226204103p:plain

まずはindex.htmlから記述します。

<!DOCTYPE html>
<html>
<head>
  <title>{{ title }}</title>
</head>
<body>
    <p>{{ content }}</p>
</body>
</html>

titlecontentという変数を定義しました。
テンプレートを通じてこの変数に値を与え、展開して表示させてあげます。

{% extends "index.html" %}

現時点ではテンプレートはこれだけです。
後で追記していきます。

最後に、bottleからデータを渡してあげましょう。

@app.route('/')
def top():
    context = {}
    context["title"] = "Jinja2Test"
    context["content"] = "top"
    return template('index.j2', **context)

テンプレートへ渡すデータを辞書型で作成し、
テンプレートファイル名とデータを指定してレスポンスしています。

ではレッツ実行。

f:id:monozukuri-bu:20190226204139p:plain

titlecontentに指定した値が展開されていますね。

分岐と反復

Pythonから引数を与えて、HTMLに値を展開することができましたので、
次は分岐と反復を使ってコーディングしてみます。

Pythonからusersにリスト型の値を入れてテンプレートへ渡します。

@app.route('/')
def top():
    context = {}
    context["title"] = "User List"
    context["users"] = ["Ayame","Itsuki","Utsugi"]
    return template('index.j2', **context)

値を受け取るHTML側は以下のようになります。

<body>
    <h1>{{ title }}</h1>

    {% if users|length == 0 %}
        <p>no user</p>
    {% else %}
        <ul>
            {% for user_name in users %}
                <li>{{ user_name }}</li>
            {% endfor %}
        </ul>
    {% endif %}
</body>

usersがあればユーザーの数だけリストで表示し、
無ければno userと表示します。

users|lengthとすることで、usersの要素数をカウントしています。
リスト型の値に対してできる操作は色々とあり、以下が参考になります。
List of Builtin Filters

ブロック

これまでの例ではHTMLファイルの中にコードを書いており、
テンプレートエンジンのメリットをあまり享受できていませんでした。

ブロックを使ってHTMLのひな形と実装を分離しましょう。
まずはhtmlファイルからロジック部分を削除し、contentという名前のブロックを作成します。

<body>
    <h1>{{ title }}</h1>

    {% block content %}
    {% endblock %}
</body>

次に、テンプレートファイルにもcontentという名前のブロックを作成し、
その内部に先ほどのロジック部分を記述します。

{% extends "index.html" %}

{% block content %}
    {% if users|count == 0 %}
        <p>no user</p>
    {% else %}
        <ul>
            {% for user_name in users %}
                <li>{{ user_name }}</li>
            {% endfor %}
        </ul>
    {% endif %}
{% endblock %}

Pythonの実装には変更ありません。
実行してみると、先ほどと同じ結果になっていると思います。

f:id:monozukuri-bu:20190226204139p:plain

これで、HTMLのひな形とロジックの分離ができました。

同じひな形に異なるロジックを挿入する

冒頭で書いたように、ベースとなるデザインをHTML/CSSで記述し、
コンテンツをテンプレートで動的に生成するような使い方ができます。

index.htmlを使って、別のテンプレートのcontentを描画させてみます。

まず、Bottleでアクセス先の指定と引数を作っておきます。
期末試験の結果をテーブル表示させてみましょう。

@app.route('/other')
def other():
    context = {}
    context["title"] = "期末試験の結果"
    context["headers"] = ["氏名","国語","数学","理科","社会","英語"]
    context["results"] = [
        ["青木", 90, 80, 92, 88, 86],
        ["佐藤", 46, 22, 18, 55, 14],
        ["高木", 76, 62, 58, 69, 55]]
    return template('other.j2', **context)

テンプレート側です。
other.j2を作り、index.htmlを使うよう指定しました。
また、今回は簡単のためにStyle直書きですが、良い子はマネしないでくださいね。

{% extends "index.html" %}

{% block content %}
    <table style="border: solid 2px black;border-collapse: collapse;">
        {% for header in headers %}
            <th style="border: solid 1px black;">{{ header }}</th>
        {% endfor %}

        {% for res in results %}
            <tr>
                {% for data in res %}
                    <td style="border: solid 1px black;">{{ data }}</td>
                {% endfor %}
            </tr>
        {% endfor %}
    </table>
{% endblock %}

それでは、/otherにアクセスしてみます。

f:id:monozukuri-bu:20190226204320p:plain

ばっちりテーブルを表示できています。

htmlファイルの骨組みはindex.htmlの1ファイルのみですが、
テンプレートを変更することで、内容を変化させることができました。

デザインを変える

index.htmlを修正することで、両方のページに修正を加えることができます。

たとえば、<h1>の要素を赤色にするStyleを定義すれば、
User List期末試験の結果も赤色になります。

まとめ

今回はテンプレートエンジンであるJinja2をBottleから使ってみました。
今回の例ではあまりメリットが伝わらなかったかもしれませんが、
htmlファイルを1つ修正すれば全ページのデザインが修正できると思うと、
大規模な構成において真価を発揮することがわかります。

PythonでWeb開発にもチャレンジしたいですね。
以上です。