python製フレームワークBottleで簡単なWebアプリを作る(その1)

こんにちは。今回のテーマは『python製フレームワークBottleで簡単なWebアプリを作る(その1)』です。この記事はBottleのチュートリアルや解説をする記事ではありません。簡単なwebアプリを作りながらプログラミングの面白さや、案外簡単にwebアプリって作れるんだなってことを伝えられればという思いで書きました。もしプログラミングに興味があるけど、何を作って良いか分からない方は一緒に作ってみませんか?今の所、全3回で完結する予定です。

(2018-12-15)加筆修正しました。また、ソース全体が見渡せるようにGitHubにソースコードをUPしました。


Bottleってなに?

Bottleはpythonで作られたWebフレームワークです。フレームワークにも関わらず1ファイルで構成されるというシンプルさが魅力的です。そのシンプルさ故にフルスタックフレームワークと比較すると機能的には限定的であり、機能の半分がルーティング機能という感じです。具体的にはセッションやORMの機能はBottle単体ではないために、他のライブラリやプラグインを導入して補う必要があります。

そのシンプルさから学習コストはフルスタックフレームワークであるDjangoと比べると低く、スピーディに動くモノを作りたいと言うときには便利だと思います。用途としては小規模なサイトや実験的なウェブアプリという感じでしょうか。大規模な商用サイトを構築する際には選択肢に入ってこないと思います。(少なくとも現時点では)

尚、Bottleよりも少し後にFlaskというBottleによく似たフレームワークがエイプリルフールのネタとして開発されましたが、今やBottleのお株を奪いそうな勢いで浸透していますね。

今回つくって見るもの

今回は簡単な読書記録アプリを作りながらBottleの使い方を紹介したいと思います。百聞は一見にしかず、多分画像を見ればおおよその機能の予想はつくと思います。

書籍登録画面:書籍情報の登録

登録確認画面:登録前の確認を行う

登録リスト:登録された書籍情報の一覧を表示

環境構築

筆者の環境はArch Linuxで作業をしました。pythonおよび各ライブラリのバージョンは以下の通りです。

  • python: 3.6.5
  • bottle: 0.12.13
  • Jinja2: 2.10
  • SQLAlchemy: 1.28

Bottleのインストール

以下のコマンドでBottleをインストールします。

pip install bottle

環境によってはsudoコマンドが必要かも知れません。

Jinja2のインストール

今回はテンプレートとしてJinja2を使います。Jinja2はDjangoテンプレートの記載方法を踏襲しながら機能拡張したテンプレートでpythonのテンプレートとしては有名です。Bottleは独自のテンプレートを有していますが、筆者の趣味でJinja2を使います。

以下のコマンドでBottleをインストールします。

pip install jinja2

余談ですが、Jinjaという名前は”Templete(テンプレート)”と”Temple(寺院)”を掛け、日本語に転じて名付けたらしいのですが、神社は”shrine”と習った筆者からすると「誤訳では?」と勘ぐってしまいます。ま、昨今、寺院と神社の区別がつかない日本人も多いですしね。

この後、SQLAlchemy等の他のライブラリも必要になってきますが、必要に応じてインストールしていきます。

ディレクトリ構成

ディレクトリ構成は以下の通りです。(2018-12-15修正しました。)

.
├── apps.py
├── models.py
├── routes.py
├── static
│   ├── css
│   │   ├── bootstrap.min.css
│   │   └── common.css
│   └── js
│       ├── bootstrap.min.js
│       └── common.js
├── utils
│   └── util.py
└── views
    ├── add.html
    ├── base.html
    ├── confirm.html
    └── list.html

いろいろな流儀がありますので、もっとシンプルな方が好ましい場合は適宜変更してみて下さい。特にapp.pyとroutes.pyを分けている部分に違和感がある方も多いかも知れません。このファイルを1つする書き方も多いです。今回はアプリの設定とルーティングという意味合いで分けています。尚、fontはopen-iconicのフォントアイコンを使用するためのディレクトリですので、今の所気にしなくてOKです。アイコンフォントはFont Awasoneに変更しcdnを読み込む方式に変更しました。jsも今回は使う予定ないのですが、一応使うときのために置いてあります。

まずはHello world

まずは好みのURLにアクセスした際にHTMLを返す簡単なルーティングを体験しましょう。
apps.pyに以下のように記載します。

import bottle
import routes

app = routes.app

if __name__ == '__main__':
    bottle.run(app=app,port=8080, reloader=True, debug=True)

portには未使用のポートを指定して下さい。今回は8080としました。reloader=Trueとしておくと、プログラムを修正した際に起動し直す必要なく、自動で再起動してくれます。debug=Trueとしておくとエラーページにtracebackが表示されるようになるので、開発中は便利です。

routes.pyに以下の記載します。

from bottle import route

@app.route('/add')
def add():
    return("<h3>Hello World</h3>")

この状態でapps.pyがあるディレクトリに移動して以下のコマンドを実行します。

$ python apps.py

開発用のサーバーが立ち上がりました。この状態でwebブラウザでlocalhost:8080/addにアクセスしてみましょう。

テンプレートを使ってみよう

出力する度にreturnでHTMLの文字列を生成していたら大変です。テンプレートファイルを読み込んで表示出来るようにしましょう。viewsディレクトリ以下にadd.htmlを以下の内容で生成します。

<!DOCTYPE html>
<head>
    <title>Hello</title>
</head>
<body>
    <h1>{{title}}</h1>
</body>

ではこのlocalhost:8080/addにアクセスがあったらadd.htmlの内容を表示するようにroutes.pyを編集します。

from bottle import route, jinja2_template as template

@app.route('/add')
def add():
    return template('add.html', title="テンプレートのテストだよ")

ではlocalhost:8080/addにアクセスしてみましょう。今回はtitleという変数に”テンプレートのテストだよ”という文字列を渡してテンプレート側の{{title}}部分を置換しました。jinja2では{{}}で囲まれた変数はpythonの変数として扱うことが出来ます。その他にも分岐や繰り返しが使えるなど動的にHTMLを生成することができます。

登録画面の作成

CSSやjavascriptファイル用のディレクトリ作成

staticディレクトリ以下にcssとjavascript用のディレクトリを作成します。プロジェクトディレクトリ下で以下コマンドを実行します。

$ mkdir -p static/css
$ mkdir -p static/js

今回はbootstrap 4を使用するためstatic/cssにbootstrap.min.cssを配置します。

次にroutes.pyに以下のように編集します。

from bottle import route, jinja2_template as template, static_file

@app.get('/static/<filePath:path>')def index(filePath):
    return static_file(filePath, root='./static')

@app.route('/add')
def add():
    return template('add.html', title="テンプレートのテストだよ")

これでテンプレートファイルviews/add.html中でファイルパスを指定してcssやjsを呼び出せるようになりました。しかし、どのテンプレートにも共通で使用するcssやJSの読込処理を書くのはスマートではないので、ベースとなるテンプレートbase.htmlを作成します。

<!DOCTYPE html>
<HEAD>
    <link href="/static/css/bootstrap.min.css" type="text/css" rel="stylesheet">
    <link href="/static/css/common.css" type="text/css" rel="stylesheet">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css">
    {% block title %}
    {% endblock %}
</HEAD>
<BODY>
    <nav class="navbar navbar-dark bg-dark">
        <span class="navbar-brand">READING RECORD</span>
    </nav>
    <main class="bd-content py-5 pl-3" role="main">
        {% block content %}
        {% endblock %}
    </main>
</BODY>

各テンプレートはこのbase.htmlを継承して{% block title %}と{% block content %}の部分に固有の表示が組み込まれることになります。

では登録画面を作っていきましょう。localhost:8080/addにアクセスされたらフォームが並んだ入力画面を返すようにします。先程作成したadd.htmlを以下のように修正します。base.htmlを継承しているので、base.htmlで書いた部分は各必要がありません。{% block ○○ %}と{% endblock %}で囲まれた部分が各テンプレート固有の内容として反映されます。

{% extends 'base.html' %}
{% block title %}
<title>書籍情報の{{kind}}</title>
{% endblock %}
{% block content %}
<form action="add" method="POST">
    {% if registId %}
        <input type="hidden" value="{{registId}}" name="id"/>
    {% endif %}
    <div class="container">
        <h3>書籍情報の{{kind}}</h3>
        <p>書籍情報を{{kind}}します。</p>
        <div class="row">
            <div class="col-md-6">
                <div class="form-group">
                    <label>書名</label>
                    <input class="form-control" type="text" placeholder="例:燃えよ剣" name="name" value="{% if form['name'] %}{{form['name']}}{% endif %}"/>
                </div>
                <div class="form-group">
                    <label>巻数</label>
                    <input class="form-control" type="text" placeholder="例:上巻" name="volume" value="{% if form['volume'] %}{{form['volume']}}{% endif %}"/>
                </div>
                <div class="form-group">
                    <label>著者</label>
                    <input class="form-control" type="text" placeholder="例:司馬遼太郎" name="author" value="{% if form['author'] %}{{form['author']}}{% endif %}"/>
                </div>
                <div class="form-group">
                    <label>出版社</label>
                    <input class="form-control" type="text" placeholder="例:新潮社" name="publisher" value="{% if form['publisher'] %}{{form['publisher']}}{% endif %}"/>
                </div>
                <div class="form-group">
                    <label>メモ</label>
                    <textarea cols="5" class="form-control" placeholder="" name="memo">{{form['memo']}}</textarea>
                </div>
                <div class="form-group">
                    <a href="/list"><input id="submit" type="button" class="btn btn-dark" value="戻る"/></a>
                    <input id="submit" type="submit" class="btn btn-primary" value="登録"/>
                </div>
                {% if error %}
                    {% for e in error %}
                    <p class="error_message">{{e}}</p>
                    {% endfor %}
                {% endif %}
            </div>
        </div>
    </div>
</form>
{% endblock %}


また、確認画面用のテンプレートを用意します。views/confirm.htmlファイルを以下の内容で生成します。add.htmlと同様にbase.htmlを継承して作っていきます。

{% extends 'base.html' %}

{% block title %}
<title>確認</title>
{% endblock %}

{% block content %}
<form action=>"regist>" method=>"POST>">
    <input type=>"hidden>" value=>"{{form['name']}}>" name=>"name>" />
    <input type=>"hidden>" value=>"{{form['volume']}}>" name=>"volume>" />
    <input type=>"hidden>" value=>"{{form['author']}}>" name=>"author>" />
    <input type=>"hidden>" value=>"{{form['publisher']}}>" name=>"publisher>" />
    <input type=>"hidden>" value=>"{{form['memo']}}>" name=>"memo>" />
    {% if registId %}
        <input type=>"hidden>" value=>"{{registId}}>" name=>"id>" />
    {% endif %}
    <div class=>"container>">
        <h3>登録情報の確認</h3>
        <p>登録内容を確認して下さい</p>
        <div class=>"row>">
            <div class=>"col-md-6>">
                <table class=>"table table-bordered>">
                    <tr>
                        {% for head in headers %}
                        <th>{{head}}</th>
                        {% endfor %}
                    </tr>
                    <tr>
                        {% for data in form.values() %}
                        <td>{{data}}</td>
                        {% endfor %}
                    </tr>
                </table>
            </div>
        </div>
        <button class=>"btn btn-secondary>" name=>"next>" value=>"back>">戻る</button>
        <button class=>"btn btn-primary>" name=>"next>" value=>"regist>">登録</button>
    </div>
</form>
{% endblock %}

Jinja2では{% %}で囲まれた場所にpythonのロジックを入れ、分岐や繰り返しを入れることが出来ます。
次にroutes.pyを次のように変更します。

from bottle import Bottle, route, run, jinja2_template as template, static_file, request,redirect

app = Bottle()

@app.get('/static/<filePath:path>')
def index(filePath):
    return static_file(filePath, root='./static')

@app.route('/add', method=['POST','GET'])
def add():
    view = ""
    registId = ""
    form = {}
    kind = "登録"
    # GETされた場合
    if request.method == 'GET':
        # TODO: id指定された場合

        # 表示処理
        return template('add.html'
                , form = form
                , kind=kind
                , registId=registId)

    # POSTされた場合
    if request.method == 'POST':
        # POST値の取得
        form['name'] = request.forms.decode().get('name')
        form['volume'] = request.forms.decode().get('volume')
        form['author'] = request.forms.decode().get('author')
        form['publisher'] = request.forms.decode().get('publisher')
        form['memo'] = request.forms.decode().get('memo')
        registId = ""
        # idが指定されている場合
        if request.forms.decode().get('id') is not None:
            registId = request.forms.decode().get('id')

        # TODO: バリデーション処理
        errorMsg = []

        # 表示処理

        # 確認画面から戻る場合
        if request.forms.get('next') == 'back':
            return template('add.html'
                    , form=form
                    , kind=kind
                    , registId=registId)

        if not errorMsg:
            headers = ['著書名', '巻数', '著作者', '出版社', 'メモ']
            return template('confirm.html'
                    , form=form
                    , headers=headers
                    , registId=registId)
        else:
            return template('add.html'
                    , error=errorMsg
                    , kind=kind
                    , form=form
                    , registId=registId)


簡単に解説を加えると、@app.route('/add', method=['POST','GET'])でエンドポイントへアクセス可能なメソッドを指定しています。ここではGETとPOSTを受けられるようにしました。今回はGETされた場合とPOSTされた場合で表示を切り替えたかったのでBottleのrequestオブジェクトからメソッドを取得して場合分けしています。requests.forms.get()がの使い方が有名ですが、文字化けしてしまうため、decode関数を挟んでいます。POSTでアクセスされた場合は入力チェック(バリデーション)して、問題なければ確認画面用のテンプレートにPOSTされた値を渡して表示します。

localhost:8080/addにアクセスしましょう。値を入力して「登録」を押したら確認画面に遷移すればOKです。

バリデーション処理

ではまだ実装されていないバリデーション処理を追加しましょう。今回は単純に必須入力項目が入力されていない場合にエラーメッセージを出すようにします。登録画面はroutes.pyのaddファンクションでバリデーション処理をTODOで残しておきました。utils/util.pyを以下の内容で生成します。

class Utils():

    @classmethod
    def validate(cls, data):
        errMsg = []
        noInput = 'が未入力です。'
        if not data['name']:
            errMsg.append('書名' + noInput)
        if not data['author']:
            errMsg.append('著者' + noInput)
        if not data['publisher']:
            errMsg.append('出版社' + noInput)
        return errMsg

Utilsクラスのバリデーションを使用できるようroutes.pyに修正を加えます。
import部分にutilクラスのインポートを追加

from utils.util import Utils

先程は処理を記載しなかったバリデーション処理を以下のように修正します。

...
        # バリデーション処理
        errorMsg = []
        errorMsg = Utils.validate(data=form)
        # 表示処理
...


これで必須項目に値が入力されていない場合にエラーメッセージが出るようになりました。

次回予告

さて、ここまでで必須項目の入力チェックを行える確認画面と登録画面が出来ました。しかし、まだ入力した値を確認するだけの状態です。しかし、まだ登録は出来ないし、確認画面から戻る機能もついていません。

次回はSQLAlchemyというORMライブラリを用いてデータベースに値を登録できる機能をつけていきます。

最後に

駆け足で進めてしまった感が拭えないのですが、いかがったでしょうか?普段はLinux関連の記事ばかりでプログラミングに関する記事はあまり書かないので、解説が分かりづらかったかも知れません。

分かりづらかった方のためにソースコードをGitHubにアップしています。宜しかったら参考になさって下さい。

時間を見つけて修正したいとは考えています。Bottleの使い方はネット上でいろいろな方が使い方を紹介しているので、この記事も1つの参考として何かの役に立てば幸いです。

Pythonを用いたウェブアプリの作成に興味がある方はDjango学習帳も参考にしてください。Pythonの代表的なウェブフレームワークDjangoの使い方を実践形式で紹介しています。

【関連記事】
python製フレームワークBottleで簡単なWebアプリを作る(その2)
【python】Bottleフレームワークで簡単なWebアプリを作る(その3)

Sponsored Link


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です