こんにちは。今回のテーマは『python製フレームワークBottleで簡単なWebアプリを作る(その1)』です。この記事はBottleのチュートリアルや解説をする記事ではありません。簡単なwebアプリを作りながらプログラミングの面白さや、案外簡単にwebアプリって作れるんだなってことを伝えられればという思いで書きました。もしプログラミングに興味があるけど、何を作って良いか分からない方は一緒に作ってみませんか?今の所、全3回で完結する予定です。
【目次】
Bottleってなに?
今回つくるもの
環境構築
ディレクトリ構成
まずはHello world
テンプレートを使ってみよう
登録画面の作成
次回予告
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等の他のライブラリも必要になってきますが、必要に応じてインストールしていきます。
ディレクトリ構成
ディレクトリ構成は以下の通りです。
. ├── apps.py ├── models.py ├── routes.py ├── static │ ├── css │ │ ├── bootstrap.min.css │ │ └── common.css │ ├── font │ │ ├── css │ │ │ ├── open-iconic-bootstrap.css │ │ │ ├── open-iconic-bootstrap.less │ │ │ ├── open-iconic-bootstrap.min.css │ │ │ ├── open-iconic-bootstrap.scss │ │ │ ├── open-iconic-bootstrap.styl │ │ │ ├── open-iconic-foundation.css │ │ │ ├── open-iconic-foundation.less │ │ │ ├── open-iconic-foundation.min.css │ │ │ ├── open-iconic-foundation.scss │ │ │ ├── open-iconic-foundation.styl │ │ │ ├── open-iconic.css │ │ │ ├── open-iconic.less │ │ │ ├── open-iconic.min.css │ │ │ ├── open-iconic.scss │ │ │ └── open-iconic.styl │ │ └── fonts │ │ ├── open-iconic.eot │ │ ├── open-iconic.otf │ │ ├── open-iconic.svg │ │ ├── open-iconic.ttf │ │ └── open-iconic.woff │ └── js │ ├── bootstrap.min.js │ └── common.js ├── utils │ └── util.py └── views ├── add.html ├── complete.html ├── confirm.html └── list.html
いろいろな流儀がありますので、もっとシンプルな方が好ましい場合は適宜変更してみて下さい。特にapp.pyとroutes.pyを分けている部分に違和感がある方も多いかも知れません。このファイルを1つする書き方も多いです。今回はアプリの設定とルーティングという意味合いで分けています。尚、fontはopen-iconicのフォントアイコンを使用するためのディレクトリですので、今の所気にしなくてOKです。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を呼び出せるようになりました。
では登録画面を作っていきましょう。localhost:8080/addにアクセスされたらフォームが並んだ入力画面を返すようにします。先程作成したadd.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"> <title>書籍情報の{{kind}}</title> </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"> <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><span class="badge badge-danger">必須</span>書名</label> <input class="form-control" type="text" name="name" value="{% if form['name'] %}{{form['name']}}{% endif %}"/> </div> <div class="form-group"> <label><span class="badge badge-secondary">任意</span>巻数</label> <input class="form-control" type="text" name="volume" value="{% if form['volume'] %}{{form['volume']}}{% endif %}"/> </div> <div class="form-group"> <label><span class="badge badge-danger">必須</span>著者</label> <input class="form-control" type="text" name="author" value="{% if form['author'] %}{{form['author']}}{% endif %}"/> </div> <div class="form-group"> <label><span class="badge badge-danger">必須</span>出版社</label> <input class="form-control" type="text" name="publisher" value="{% if form['publisher'] %}{{form['publisher']}}{% endif %}"/> </div> <div class="form-group"> <label><span class="badge badge-secondary">任意</span>メモ・感想</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-secondary" value="キャンセル"/></a> <input id="submit" type="submit" class="btn btn-info" value="登録"/> </div> {% if error %} {% for e in error %} <p class="text-danger">{{e}}</p> {% endfor %} {% endif %} </div> </div> </div> </form> </main> </BODY>
また、確認画面用のテンプレートを用意します。views/confirm.htmlファイルを以下の内容で生成します。
<!DOCTYPE html> <HEAD> <link href="/static/css/bootstrap.min.css" type="text/css" rel="stylesheet"> <title>確認</title> </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"> <form action="" 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-primary" name="next" value="regist">登録</button> <button class="btn" name="next" value="back">戻る</button> </div> </form> </main> </BODY>
Jinja2では{% %}で囲まれた場所にpythonのロジックを入れ、分岐や繰り返しを入れることが出来ます。
次にroutes.pyを次のように変更します。
from bottle import Bottle, route, run, jinja2_template as template, static_file, request,redirect app = Bottle() app.install(LoggingPlugin(app.config)) @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関連の記事ばかりでプログラミングに関する記事はあまり書かないので、解説が分かりづらかったかも知れません。時間を見つけて修正したいとは考えています。Bottleの使い方はネット上でいろいろな方が使い方を紹介しているので、この記事も1つの参考として何かの役に立てば幸いです。
Sponsored Link