このサイトについて

みんなのPython Webアプリ編 - 標準モジュールを使ったテンプレートエンジン

みんなのPython Webアプリ編 - 標準モジュールを使ったテンプレートエンジン

サポート用Facebookグループを作りました。PythonやIoT,人工知能の学習に役立つ情報を平日毎日配信しています。拙著やPython,IoTなどについて興味や質問のある方をはじめとして,どなたでも気軽にご参加いただけるアットホームなグループです。

標準モジュールを使ったテンプレートエンジン

Pythonの標準モジュールには、簡単な機能を持ったテンプレートエンジンが内蔵されています。stringモジュールに含まれているTemplateクラスです。ここでは、そのテンプレートエンジンの使い方を簡単に解説しましょう。

ところで、Pythonには文字列テンプレートと呼ばれる機能があります。文字列の中に%s、%dといった記号を埋め込んでおくと、動的に文字列の置換を行うことができます。その他に、文字列テンプレートでは、%(key)sのように辞書のキーを指定して置換を行うこともできます。Templateクラスは、この機能をより高度にしたクラスです。

Templateクラスの使い方

クラスですので、テンプレートの機能を使うためにはインスタンスオブジェクトを生成する必要があります。Templateクラスでは、インスタンスを生成するときに文字列を引数として取ります。以下のようにして、テンプレートのもととなる文字列を与えてインスタンスオブジェクトを作ります。以下の例では、ファイルに書かれている文字列を引数として渡し、Templateクラスのインスタンスオブジェクトを作っています。

:::python
t=Template(open('./tmpl.txt').read())

「tmpl.txt」というファイルには、以下のような文字列が定義されているとします。${〜}という部分が置換される文字列になります。

:::html
<html><body>
<h1> ${title} </h1>
<p>${body}</p>
</body></html>

Templateクラスのインスタンスであるtを使い、置換結果の文字列を得るにはsubstitute()というメソッドを呼びます。このとき、メソッドの引数に置換用のパターンに埋め込みたい文字列を渡します。渡し方は2種類あります。1つは、置換したい文字列を納めた辞書を渡す方法です。もう1つは、キーワード引数として文字列を渡す方法です。

テンプレート上では、titleとbodyという2種類のキーが渡ってくることが期待されています。WebアプリケーションなどからTemplateクラスを使うときには、辞書やキーワード引数の形で、データを受け渡すことになります。以下の2つの例は、どちらも同じ結果を返します。

:::python
t.substitute({'title':'The title', 'body':'This is body''})
t.substitute(title='The title', body='This is body''})

Templateクラスを活用してブックマーク管理Webアプリを作る

テンプレートエンジンを使うとWebアプリケーションの実装がどのように変わるのかを実感するために、Webアプリケーションを作ってみましょう。今回は、ブックマーク管理を登録するためのWebアプリケーションを作ってみます。フォームを使って、WebサイトなどのURLとタイトルを登録し、ブックマークを管理するためのWebアプリケーションを作ります。

ブックマークの要素を入力するUIとしてフォームを利用します。Webアプリケーションでフォームからの入力を受けて、データベースにデータを登録します。また、1つのプログラムで、フォームの表示とデータの登録を行うようなWebアプリケーションを作ることにします。 今回のWebアプリケーションでは、ちょっと凝った処理を実装してみましょう。フォームの入力に不足があったとき、エラーの表示をして、データを再入力するように指示をする処理を実装してみます。このとき、前に入力した文字列があったらフォームに再表示するようにします。たとえば、タイトルのみが入力されていてURLが入力されていなかった場合。エラーの表示とともに、直前に入力したタイトルだけがあらかじめ入力された状態でフォームを表示するわけです。そうすることによって、入力の二度手間を防ぎ、使いやすいWebアプリケーションを作ることができます。

図03 フォームの入力に不足があった場合

図03 フォームの入力に不足があった場合

テンプレートファイルを作る

まずは、Webアプリケーションの出力として利用するHTMLを、独立したテンプレートファイルとして設置します。テンプレートファイルには、状況によって内容が変化する場所に特別な文字列を埋め込みます。このテンプレートをひな形として、Webアプリケーションの出力となるHTMLを組み立てます。

今回は、標準モジュールに組み込まれているTemplateクラスを使います。Templateクラスでは、${title}というような特別な記法を使って、動的に変化する文字列を埋め込むようになっています。今回作るWebアプリケーションでは、次の2つの要素が動的に変化します。

  • フォームに埋め込まれる文字列
  • エラーメッセージ

フォームに埋め込まれる文字列は、inputエレメントのvalueアトリビュートに埋め込みます。テンプレートのvalueアトリビュートに、置換用の文字列を埋め込んでテンプレートを作ることになります。エラーメッセージはHTMLの内部に文字列として表示します。pエレメントなどで囲んで、エラーを表示したい場所に置換用の文字列を埋め込むことになります。

以下がテンプレートとして利用するファイルの内容です。「bookmarkform.html」というファイル名で、cgi-binフォルダの下に設置してください。もちろん、ファイルの文字コードはUTF-8にしておきます。

HTMLに紛れて${〜}という置換用文字列が4つ設置されています。この部分が置き換わって、Webアプリケーションの出力用文字列が作られます。

bookmarkform.html

:::html
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<h1>簡易ブックマーク</h1>
<p>${message}</p>
<form method="post" action="templatebbs.py">
  タイトル : <input type="text" name="title" size="40" value="${title}" /><br />
  URL : <input type="text" name="url" size="40" value="${url}" /><br />
  <input type="hidden" name="post" value="1" />
  <input type="submit" />
</form>
${bookmarks}
</body>
</html>

Webアプリケーションのロジックを作る

次に、実際の処理を行うWebアプリケーションのプログラムを作ります。今回のプログラムには、以下のような機能を持たせます。

  • データを登録するフォームを表示する
  • フォームから正しいデータが送られた場合、データを登録する
  • データに不足があった場合、フォームを再表示する

データの登録には、データベース(SQLite3)を使います。プログラム中では、DBAPIを使ってSQLを発行し、データの登録を行います。 以下がWebアプリケーションの本体となるプログラムです。cgi-binフォルダに「templatebbs.py」というファイル名で設置してください。文字コードはUTF-8にしてください。

プログラムがスッキリとしていて、見通しが良くなっていることが分かると思います。P.87で作成したhttphandlerモジュールのRequestクラス、Responseクラスを使うことによって、Webアプリケーションで実行する典型的な処理が抽象化され、プログラムが簡潔に書けるようになりました。その上今回は、テンプレートエンジンを使うことで、HTMLがプログラムから一掃されました。入力が正しいかどうかを検証し、正しかったらデータを登録する、というプログラムで行っている処理の内容が、かなり分かりやすくなっているはずです。

templatebbs.py

:::python
#!/usr/bin/env python
# coding: utf-8
import sqlite3
from string import Template
from os import path
from httphandler import Request, Response, get_htmltemplate import cgitb; cgitb.enable()
con=sqlite3.connect('./bookmark.dat') cur=con.cursor()
try:
    cur.execute("""CREATE TABLE bookmark ( title text, url text);""")
except:
    pass
req=Request()
f=req.form
value_dic={'message':'', 'title':'', 'url':'','bookmarks':''}

if f.has_key('post'):                                                (1)
    if not f.getvalue('title', '') or not f.getvalue('url', ''):     (2)
        value_dic['message']=u'タイトルとURLは必須項目です'              (3)
        value_dic['title']=unicode(f.getvalue(
                  'title', ''), 'utf-8', 'ignore')
        value_dic['url']=f.getvalue('url', '')
    else: cur.execute(
              """INSERT INTO bookmark(title, url) VALUES(?, ?)""",
              (f.getvalue('title', ''), f.getvalue('url', '')))
        con.commit()

res=Response()                                                       (4)
f=open(path.join(path.dirname(__file__), 'bookmarkform.html'))
t=Template(unicode(f.read(), 'utf-8', 'ignore'))

body=t.substitute(value_dic)                                         (5)
res.set_body(body)
print res

さて、プログラムの内容について簡単に見てみましょう。まず、プログラムの冒頭では必要なモジュールをインポートしています。その後、try〜exceptを使ってデータベースにデータを記録するためのテーブルを作っています。

その後は、フォームから送られたデータの処理が続いています。フォームにはWebブラウザ上には見えないようにpostというキーが埋め込まれています。このため、フォームからデータが送られた場合には、必ず「post」というキーが送られてくるようになっています。このキーがあるかどうかを判別して、ブックマークを登録するかどうかを判断しています(1)。

フォームからデータが送られた場合は、すべての項目が入力されているかどうかを調べます。もし入力に不足がない場合には、データベースにデータを登録します(2)。

入力項目に不足がある場合は、エラー表示をしながら直前に入力された項目をフォームに表示するようなデータを作ります(3)。エラー、フォームの入力として埋め込む文字列は、value_dicという辞書に登録します。辞書のキーとなるのは、テンプレートに埋め込んである置換用の文字列に出てきた文字列です。

最後に、出力の準備をします。まずはResponseクラスのインスタンスを作ります。その後、ファイルからテンプレートを読み、Templateクラスのインスタンスを作ります(4)。その後、Templateクラスのインスタンスのsubstitute()メソッドを使ってテンプレートの変換を行います(5)。このとき、テンプレートに埋め込みたいデータを辞書の形式で渡します。

今回のプログラムでは、表示をコントロールする部分と、処理を行う部分が完全に分離しています。表示上の修正を行いたい場合はテンプレートを直します。処理の内容を直したい場合にはプログラムを直します。このように、テンプレートエンジンを使うと、目的によって修正部分の切り分けがしやすくなるのです。

Webアプリケーションの場合、出力となるHTMLの修正を頻繁に行います。WebブラウザにHTMLを表示しながら、文字の大きさや位置、表示方法などを修正して、より分かりやすい出力を作る、というようなことをよく行います。そのような場合、出力のひな形がテンプレートファイルとして独立していた方が作業がはかどります。テンプレートファイルを直すだけで、文字の大きさやデザインなどの変更を行うことができるわけです。

ブックマークリストを埋め込む

ところで、このWebアプリには足りない機能があります。ブックマークは登録できるものの、登録したブックマークを表示することができません。このままではとても不便なので、登録済みのブックマークを埋め込む機能を追加しましょう。登録済みのブックマークは、フォームの下に表示するようにします。

図04 スクリプト変更後の画面

図04 スクリプト変更後の画面

テンプレートの最後の方に${bookmarks}という置換用の文字列があります。この文字列はまだ使われていませんので利用することにしましょう。

Templateクラスを使ったテンプレートエンジンで実現できるのは、文字列の置換だけです。つまり、テンプレート上に置き換えて表示したい文字列は、プログラム側で作り、テンプレートエンジンに渡してやる必要があるわけです。

登録済みのブックマークはデータベースに入っています。DBAPIを使ってSQLite3にSQL(SELECT文)を送信すれば、登録済みのブックマーク一覧を取得できます。ブックマーク一覧を取得したら、それを元に表示用の文字列を組み立てます。

以下が、登録済みブックマークの一覧を表示するためのコードです。先ほどのtemplatebbs.pyに以下のコードを追加してください。「res=Response()」としてレスポンスオブジェクトを作っている行の直前に追加します。

templatebbs.pyへの追加部分

:::python
listbody=''
cur.execute("SELECT title, url FROM bookmark")
for item in cur.fetchall():
    listbody+="""<dt>%s</dt><dd>%s</dd>¥n"""%(item)
listbody="""<ul>¥n%s</ul>"""%listbody
value_dic['bookmarks']=listbody

せっかくPythonのコードからHTMLを完全に追い出したのに、またHTMLが入り込んでしまいました。このようになってしまったそもそもの問題は、Templateクラスが単純な置換を行う機能しか持っていない、ということにあります。もっと複雑なWebアプリケーションを作るには、より高度な機能を持ったテンプレートエンジンがあった方が便利そうです。

テンプレートエンジンに求められる機能と分類

簡単なテンプレートエンジンを使ってWebアプリケーションを開発してみることで、テンプレートエンジンの有用性を確かめることができました。文字列の置換機能だけでも、ある程度のことは可能ですが、より高度な処理を行おうとすると、プログラムの中でHTML文字列を組み立てる必要が出てきます。

普通の文章のように一次元的な構造を持つテキストと異なり、HTMLは「構造」を持っています。HTMLでは、繰り返しによって要素を表現することがよくあります。今回追加したブックマークリストなどがその例です。また、Webアプリケーションのように比較的高度な出力を求められる場面では、条件分岐のような機能があると便利でしょう。

このように、Webアプリケーションで利用するテンプレートエンジンには、プログラミング言語に似た高度な機能が求められることがあります。

Pythonには、Webアプリケーション作成に活用できる既成のテンプレートエンジンが多くあります。実際、そのようなテンプレートエンジンでは、文字列置換以上の高度な機能を実装しています。置換用の文字列を埋め込む場合と同じように、特別なパターンをテンプレートに埋め込み、そのような機能を利用するようになっています。

以下では、典型的なテンプレートエンジンに実装されている主な機能について解説します。そうすることで、テンプレートエンジンに求められる機能について探ってみましょう。

ループ

既存のテンプレートエンジンはほとんど、テンプレート上のある部分を繰り返し表示する機能を持っています。Webアプリケーションの出力となるHTMLでは、似たような内容を繰り返して利用することがよくあります。複数の要素をリスト表示したい場面もあるでしょう。また、UIとなるフォームでは、メニューやラジオボタンなど、同じようなタグを複数回繰り返して表示する場面が多くあります。

表示する要素の内容が状況によって変わる場合は、なんらかのプログラミング的な方法を使って取り扱った方が効率がよくなります。このような処理をするために、テンプレートエンジンの持つループ機能を利用すると便利です。テンプレートエンジンを利用するプログラムの側では、ループの元となるシーケンスやイテレータを作ります。ループやイテレータを渡し、テンプレートエンジン側で繰り返し処理をします。

似たような要素をループを使って記述することで、テンプレートの記述自体がシンプルになる、という利点もあります。

条件分岐

Webアプリケーションでは、条件によって表示の内容や方法を切り替えたいことがよくあります。たとえば、エラーがある場合にのみエラーを表示する、ある値を超えた数値は目立つように表示する、というようなことをしたい場合に、Pythonのif文のような条件分岐を使えると便利です。実際、多くのテンプレートエンジンではこの条件分岐の機能が用意されています。

プログラム側では、条件の判断に必要なデータだけを作ってテンプレートに渡すことになります。テンプレート側には条件が記述してあって、場合によって切り分けをして表示を切り替えます。

テンプレートの部品化

たとえばブログのサイドバーと呼ばれる部分のように、Webアプリケーションでは複数の場所で共通して利用される部品を活用することがあります。このように、共通して利用する部品を複数のテンプレートにバラバラに記述しておくのでは、開発の効率が落ちてしまいます。ある部品を修正したいときには、複数に散らばっているすべての部品を修正しなければならないからです。

Webアプリケーションでこのように共通した部分を実装するときに、テンプレートの一部を部品化できると便利です。既存のテンプレートエンジンでも、比較的高機能なものにはこの機能が実装されています。また、部品を持っている外部のテンプレートを呼び出すために、多くのテンプレートエンジンでは外部のテンプレートを読み込む機能を備えています。

図05 一部を部品化して他のテンプレートで利用できる機能を持つテンプレート

図05 一部を部品化して他のテンプレートで利用できる機能を持つテンプレート

テンプレートの部品化は、Pythonのプログラミングでモジュールを作るのに似ています。Pythonでは、よく行う処理を関数としてまとめておき、いろいろなプログラムから再利用できるようにモジュールを作ります。テンプレートの部品化は、目的や手法がモジュール作成によく似ています。

マスターテンプレートとスロット

より高機能なテンプレートエンジンには、Webアプリケーションの出力となるHTMLの配置を決める機能が備わっていることがあります。最近のWebアプリケーションでは、画面のレイアウトをブロックに分割して配置を行います。ブロックの中には、場合によってはいつも決まった内容を表示することもありますし、場合によって表示内容が変化することもあります。

たとえば、ブログの画面を思い出してください。一般的なブログの画面には、ヘッダ、本文、サイドバーという3つのブロックがあります。このうち、ヘッダの表示内容はいつも変わりません。本文の表示内容は状況によって変わります。トップページであれば、近々のエントリ数個を表示します。エントリ単体の表示であれば、該当エントリの本文だけを表示します。また、サイドバーの表示も状況によって変わります。

このようにブロックに分割したレイアウトを効率よく扱うために、特殊な機能を備えているテンプレートエンジンがあります。全体的なレイアウトを定義したテンプレート(マスターテンプレート)を用意しておきます。実際に表示を行うテンプレートでは、変化するブロックだけを継承して定義します。このようにすることで、ブロックレイアウトで変化する部分を効率よく扱うことができます。

このように、テンプレートの全体のうち、変化する部分だけを定義して継承する機能のことをスロットと呼ぶことがあります。

図06 スロット

図06 スロット

マスターテンプレートの機能は、Pythonの抽象クラスによく似ています。抽象クラスには、クラスの振る舞いのうち大まかな部分や、最小限度必要な部分のみを定義します。より専門的な処理を行うメソッドなどは、抽象クラスを継承したクラスに実装します。抽象クラスを継承したクラスに実装したメソッドが、テンプレートエンジンのスロットに相当する機能になります。

コードブロックの埋め込み

テンプレートエンジンによっては、テンプレート内にPythonのコードそのものを埋め込むことができるものがあります。実際、テンプレートの中で簡単な処理が実行できると便利な場面があります。テンプレートの中だけで利用する変数を定義したり、前段階で簡単な文字列処理をして、結果を表示に利用する、といった目的でコードを書くことが多いでしょうか。

テンプレートエンジンで処理をするテンプレートには、特殊な記法を使ってPythonのコードを埋め込みます。文字列の置換、ループや条件分岐などでは、Pythonの式を埋め込みます。Pythonの式とは、簡単に解説すると「改行を使わないで書けるコード」のことです。変数、関数呼び出しなどが式に相当します。

コードブロックでは、Pythonの文が記述できます。Pythonの文を簡単に解説すると「改行を組み合わせて記述するコード」のことです。変数の定義、for文を使ったループ、if文を使った条件分岐などが文に相当します。

テンプレートの中にPythonのプログラムが書けることが分かると、なんでもテンプレートの内部で実行したくなるかもしれません。しかし、あまり複雑な処理をテンプレート内で実行しようとすると、Webアプリケーションのプログラムとの役割分担が曖昧になってしまいます。プログラムの内部からHTMLの文字列を追い出すことでプログラムがスッキリしました。テンプレートの内部にPythonのコードが氾濫するようになると、今度はテンプレートの見通しが悪くなってしまいます。

テンプレートエンジンの中には、文を使うような複雑なコードはテンプレート内部に書くべきではない、と主張してこの機能を実装していないものもあります。

テンプレートエンジンの分類

Pythonには、Webアプリケーション開発に利用できるテンプレートエンジンが数多くあります。それぞれが持っている機能はとてもよく似ています。また、特殊なパターンや記法を使って、動的に生成した文字列を埋め込むという基本的な考え方も同じです。

ただし、テンプレートエンジンによって記法として採用しているパターンが異なります。また、パターンの埋め込み方には大まかに分けて2つの手法があります。

1つは、場所を問わず、テンプレート専用の記法でパターンを埋め込むタイプです。ここでは埋め込み型と呼びましょう。HTMLのような文字列に、特定のパターンを持った文字列を埋め込みます。

以下は、埋め込み型のテンプレートであるDjangoの記法のサンプルです。liエレメントの要素を繰り返し、複数表示しています。{%〜%}や{{〜}}という文字列で囲み、処理の内容を指示していることが分かります。独自の記法で 囲まれた部分が、出力時に動的に置き換わります。

Djangoテンプレートの例

:::html
<ul>
{% for choice in choices %}
  <li> {{ choice }} </li>
{% endfor %}
</ul>

もう1つのタイプは、HTMLのタグ(エレメント)の内部にテンプレート独自のパターンを埋め込むタイプです。こちらのタイプをXML型と呼びましょう。

エレメントの中にテンプレート用の文字列を埋め込むため、でき上がったテンプレートをHTMLやXMLとして評価したときに正しく(valid)なります。テンプレートの仕様自体、端正な出力をするように意図されて作られているのです。半面、テンプレートの記法に制限が多くなります。

以下は、XML型テンプレートGenshiので書いたサンプルです。py:〜というアトリビュートの形で、テンプレート独自のパターンが埋め込まれているのが分かります。py:から始まるアトリビュートは、出力時には消されます。なお、py:contentというアトリビュートは特殊な働きをします。このアトリビュートに記載されている内容が、エレメントの内部(この場合は

  • タグで囲まれた部分)に埋め込まれるのです。

    Genshiテンプレートの例

    :::html
    <ul>
    <li py:for="choice in choices"
        py:content="choice">
      A choice here
    </li>
    </ul>
    

    みんなのIoT

    プログラミングの知識がまったくなくてもPythonを学べる新著「みんなのIoT」を書きました。IoTやAIを交えて,Pythonを楽しく学べる書籍です。ぜひお読みください。

  • 2014-09-03 15:00