PythonのWebフレームワークBottle使ってみる

仕事では基本的に社内で作った Flaskをベースというか、テンプレートとしたwerkzeugを使った myojin を使っていて、これも公開されているので、そのまま使ってもよいのですが、せっかくなのでということでBottleを試してみた。

Bottleの自分的ポイントは

  1. Python3も普通に対応
  2. 最初のweb.pyを思い出される感じの1ファイルで動くっていうコンセプトが好き
  3. テンプレートエンジンをmakoに入れ替えるのが簡単

くらいでしょうか。

ということで、Bottle + makoで簡単なクローラーアプリでも作ってみようかと思っていますが、とりあえず簡単な使い方部分だけ。

セットアップ

インストールは1ファイルなのでダウンロードしてもよいのですが、めんどくさいしpipで入れます。(1ファイルの意味ないとか言われそうですが) Gentooを使っていると、最近であればPython3.3がすでに入っているはずなので、eselectで3.3を選んだ上で、mkvirtualenvします。virtualenv,virtualenvwrapperはemergeで入れてしまうのでよいんではないかと。私はそうしてます。

$ eselect python set --python3 python3.3
$ mkvirtualenv --python /usr/bin/python3 narou
$ pip install ipython bottle

という感じで、ipythonとbottleをインストールします。0.13系の開発が進んでいるようですが、とりあえず、pipで入れられるstableの0.11系の最新、0.11.6を入れます。

Hello World

とりあえず動作確認ということで、チュートリアル通りにHello Worldを動かしてみます。これ以降、基本的にチュートリアルの抜粋というか、焼き直しです。細かいところはチュートリアルとか公式ドキュメントがしっかりと書かれているので英語でよければ確実にそちらを読む方がよいです。

とりあえず、Hello worldのソースはこんな感じ。まあ適当に、hello.pyという名前で保存します。

from bottle import route, run, template

@route('/hello/<name>')
def index(name):
    return template('<b>Hello {{name}}</b>!', name=name)

run(host='0.0.0.0', port=8080)

唯一、hostのところは、コード書いているのがさくらVPS上なので0.0.0.0にして外部からのアクセスを受けるように変えています。

で、スクリプトを起動します。うまくいけばエラーなく開発用のhttpのサーバが立ち上がるはずです。

$ python hello.py
Bottole v0.11.6 server starting up (using WSGIRefServer())...

になって、さくっとブラウザで、http://[server_ip]:8080/hello/mat2uken とすると、Hello mat2uken!と見えれば成功です。簡単ですね。

とりあえずデバッグフラグとオートリロードを有効にする

チュートリアルとか見てると最後のほうに出てくるのですが、先に有効にしておくと楽なのが、デバッグフラグとオートリロード、最近のフレームワークで対応されている機能ですが、Bottleにもちゃんとあります。werkzeugみたいにデバッガ起動してエラーページ上で触れたりまではできませんが。

import bottle
bottle.debug(True)

と最初に設定しておくことで、エラーページにtracebackが出るようになるので、とりあえず開発時は有効にしておくことがオススメ。ただ、いくつか無効になるものもあるので、debug=Falseの場合に動作が変わってしまってうわーんみたいなケースもあるっぽい?

オートリロードのほうは、run関数の引数でreloader=Trueとすると有効になります。開発時はどうせ立ち上げっぱなしにするので、そのまま有効にしとくのでよいと思います。

run(host='0.0.0.0', port=8080, reloader=True)

makoと組み合わせる

単純に個人的な趣味でmakoのほうがテンプレートとして慣れているので、makoを使うように変更します。といっても、templateになっているところをmako_templateに変えるだけです。

とりあえず、makoをいれます。

$ pip install mako

さっきのソースを少しだけ変えます。fromのことろでmako_templateをtemplateとしてimportします。あと、変数展開は${}とかになるので、そこを書き換えます。

import bottle
bottle.debug(True)

from bottle import route, run
from bottle import mako_template as template

@route('/hello/<name>')
def index(name):
    return template('<b>Hello ${name}</b>!', name=name)

run(host='0.0.0.0', port=8080, reloader=True)

これで、ブラウザでアクセスすれば、同様の動きをするはずです。

ルーティングでのパラメータ取得

すでにやってるけれど、ルーティングの指定のところで、<>で囲むとwildcard指定でdynamic routeという仕組みで引数として受け取れます。なにもしないとただ指定した箇所の文字列を引数に渡されるだけですが、Filterをつかうと簡単な値チェックや制約を与えることもできます。

@route('/hello/<id:int>')
これだと、idはintに変換されて渡されます。

@route('/hello/<name:re:[a-z]+>')
正規表現も使えます。これ便利ですよね。性能的にはどれくらい影響あるのかなとかはちょっと気になりますが。

@route('/static/<p:path>')
こうすると、pに/が含まれていてもこれ以降のすべての文字列にマッチするようです。これ以下を自力でdispatchしたい場合などに便利なのかな。

myojinも同様の仕組みで、ついでにモデルを指定してSelectの発行もついでにやってモデルを引数として渡される便利機能があるのですが、そこまではやらないようです。でも、自分でFilterは作れるっぽいので簡単に同じような機能が実現できそうですね。

GET,POSTなどのメソッドの取り扱い

こちらも割とシンプルで、@post,@getなどのデコレータを使うか、@route(‘/hoge’, method=’post’)みたいな表記でメソッドを指定します。個人的にはただの好みですが、ちょっと冗長でもrouteにmethodを渡す方が好きです。受け取りはrequest.paramsも使えるようですが、request.query or request.formsで名前を指定して値を取得する感じです(GETでもPOSTでも同じ動きなんてことはめったにないのでアクセスする変数使い分ける方がなんかしっくりきます)。もう少しまともにvalidationとかしたいならwtformsとか組み合わせるほうがいいかもしれないですね。FormsDict.decode()をつかって、wtformsに渡せば使えるっぽいことが書かれています。

import bottle
bottle.debug(True)

from bottle import route, run
from bottle import mako_template as template

from bottle import request

@route('/hello', method='GET')
def hello():
    name = request.query.get('name')
    return template('<b>Hello ${name}</b>!', name=name)

run(host='0.0.0.0', port=8080, reloader=True)

という感じにして、http://[server_ip]:8080/hello?name=aaa という感じでアクセスすれば、上のサンプルで、/hello/aaaというような形で指定した場合と同じ表示のHello, aaaみたいな表示が出れば成功です。

ちなみに、request.jsonというのもあって、application/jsonでPOSTされると、ここにparseされたjsonが格納されるようです。

staticファイルの配信

最終的にはたぶんnginxを前に置いてstaticファイルはそちらから配信するとはおもうんですが、開発時用に開発サーバからもstaticファイルは配信してほしいところです。というわけで、その辺の設定。

from bottle import static_file
@route('/static/<filename:path>')
def send_static(filename):
    return static_file(filename, root='./static')

という感じでルーティングを追加します。これでファイルそのまま送ってくれます。こういう用途でも、Filterのpathは有効ですね。そのまま渡すと、セキュリティ的などうなの的な話がありそうですが、まあ開発用ということで。他にも画像ディレクトリとかなら、mimetypeでimage/xxxみたいなのも渡すとよりよさそうだったりもするようです。download=[filename]という形で引数を渡すと強制的にダウンロードさせることもできそう。Content-Dispositionがきっと設定されているはず。確認してないけど。

リダイレクト

リダイレクトするときの一番簡単な方法は、redirectを呼ぶだけ。301にするか、302にするかみたいなところは、redirect(‘/hello’, 301)という風に数字を渡せばそのステータスコードが返されるっぽい。ちなみに、同様の関数として、abortもある。4xxを返すときはこれを呼べばエラーページを出せる。

といいつつも、redirectとabortはショートカットの関数でこれを呼んだ時点で、HTTPErrorとかがraiseされるような動きをしているっぽくて、そういうことをせずに自力で適当にResponseオブジェクトを作って返してやることで同様の動作をさせることもできる。冗長なので、わざわざ使うケースは少なそうだけども。

from bottle import response
@route('/r2')
def r2_test():

  response.status = 302
  redirect_url = '{0}://{1}/hello?name=hoge'.format(
                  request.urlparts.scheme, request.urlparts.netloc)
  response.set_header('Location', redirect_url)
  return response

こんなかんじでしょうか。LocationはFQDNで指定しないといけないので、URL作るのが若干面倒ですね。簡単なユーティリティ作れば使い回せるのでそんなに苦でも無いとは思いますが。

さいごに

最低限、チュートリアルを見ながらざっくり進めていっただけですが、こんなもんでしょうか。あとは、プラグイン周りでSQLiteの説明もありましたが、そのへんは割愛。

その辺用にプラグインはもうすこしちゃんと学びたいところ。SQLAlchemyはいくつも例がすでにあるようだし、LevelDBとか使ってみようかな...

とりあえず、Bottle+makoで最低限の開発ができるようになるまではこれくらいで。