続・玩式草子 ―戯れせんとや生まれけん―

第8回ウィスキー・ボトル・ブルース(2番)

新元号「令和」が発表され、⁠平成」も残すところ一週間余りとなりました。筆者にとって平成の30年間は、社会人として働き出した時代であると共に、LinuxやOSSに出会って、それらと共に成長してきた時代だったように思います。

1991年、平成3年に最初のバージョンがリリースされたLinuxは、平成最後の年にメジャーバージョンが5.0にまで到達しました。Intel 80386CPUを積んだPC互換機の上でしか動かなかったLinuxが、平成の末にはスーパーコンピュータからIoT機器までサポートし、文字通りネット社会の基盤を支えるまでに成長しました。

もちろん「令和」の時代になってもLinuxの成長は続いていきます。⁠令和」の時代にLinuxがどう発展していくのか、Linusさんに代表されるカーネル開発者たちが、どのようなアイデアや機能をカーネルに持ち込むのか、興味が尽きることはありません。

さて、感慨に耽(ふけ)るのはこれくらいにしておいて、今回はBottleがブラウザ等のクライアントとデータをやりとりするあたりの話を扱います。

HTTPメソッドとルーティング

前回紹介したように、Bottleでは"@route"という指定(修飾)で、Pythonの関数とURIを結びつけます。

一方HTTPでは、クライアントがURIにアクセスする際の手段として、"GET"や"POST"といったメソッドを定めており、"GET"ならばサーバからデータを提供する、"POST"ならばサーバがデータを受け取って何らかの処理をする、等、サーバ側の動作を指定できるようになっています。

そのため、Bottleの"@route"の指定も、引数にHTTPの「メソッド」を指定して、⁠URI + メソッド」をPythonの関数に結びつけられるようになっており、1つのURIをGETメソッドでアクセスする場合とPOSTメソッドでアクセスする場合で異なる処理ができるように考慮されています。この機能を使えば、以下のようなスクリプトが書けます。

 1  #!/usr/bin/python
 2  
 3  from bottle import route,run,request
 4  
 5  @route('/login')
 6  def login():
 7      return '''
 8          <form action="/login" method="post">
 9              username: <input name="username" type="text" /> <br>
10              password: <input name="password" type="password" />
11              <input value="login" type="submit" />
12          </form>
13      '''
14  
15  @route('/login', method='POST')
16  def do_login():
17      username = request.forms.get('username')
18      password = request.forms.get('password')
19      return '''
20          username: {} <br>
21          password: {}
22      '''.format(username, password)
23  
24  run(host='localhost', port=8080, reloader=True, debug=True)

このスクリプトを実行し、ブラウザから"http://localhost:8080/login"にアクセスすると(GETメソッド⁠⁠、5行目の"@route('/login')"で修飾した6-13行目までのlogin()関数が実行され、ブラウザにはユーザ名とパスワードの入力を促すフォーム画面が表示されます。

図1 GETメソッドでURIにアクセス
図1 GETメソッドでURIにアクセス

この画面を作っているのはPython特有の三連クォート(''')で囲まれた8~15行目までの<form>タグで、ページのソースを確認すると、この部分がそのまま送られていることがわかります。

図2 入力画面のソースコード
図2 入力画面のソースコード

画面に沿ってusernameとpasswordを入力し"login"ボタンを押すと、今度は<form>タグの指示により、"http://localhost:8080/login"をPOSTメソッドでアクセスします。リクエストを受け取ったBottleはHTTPのメソッドを見分けて、15行目の"@route('/login', method='POST')"で修飾したdo_login()関数を実行し、request.forms.get()メソッド経由で受け取ったusernameとpasswordを20、21行目の文字列に埋めこんで返します。

図3 POSTメソッドで送ったデータを表示
図3 POSTメソッドで送ったデータを表示

Bottleでは、上記例のようにmethod引数無しに"@route(...)"を使うと、HTTPで最もよく使われるGETメソッドと見做します。手元ではGETとPOSTしか試したことはないものの、BottleはGETPOSTPUTDELETEPATCHの各メソッドを見わけることができ、データのサマリのみを入手するHEADメソッドも、GETメソッドでコンテンツ本体を転送しないようにする形で対応しているそうです。

また、上記例では"@route(...)"修飾子のmethod引数としてメソッドを指定しましたが、

  from bottle import get, post

としてgetやpostモジュールをインポートしておけば、"@route('/login', method='POST')"の代わりに

  @get('/login')
  def login:
  ....

  @post('/login')
  def do_login:
  ....

のようにメソッドで修飾したルーティングを書くことも可能で、こちらの方が読み易いかもしれません。

requestオブジェクト

前節のスクリプトの17、18行目のように、クライアントから<form>タグやURIの引数として送られてくるデータ(リクエスト・パラメータ)は、Bottleではrequestオブジェクトを経由して受け取ります。

requestオブジェクトを利用するには、先のスクリプトの3行目のように、"from bottle import ..."の行でrequestモジュールをimportしておきます。

このモジュールをimportすれば、前節のスクリプトのように<form>タグ経由でPOSTメソッドを使って送られてくるパラメータを、<input>タグなどのname属性をキーワードに、request.forms.get(name)として取り出すことができます。

一方、GETメソッドでURLパラメータとして送られてくる値を取り出すには、同じrequestオブジェクトのrequest.query.get(name)を使います。

Bottleでは、これらリクエスト時に送られてくるパラメータは、Pythonの辞書型を拡張したFormsDictと呼ばれるクラスに収められ、辞書型のように扱うことも、メソッドを使って"request.forms.get(name)"のように取り出すことも可能です。

Bottleがどのようにリクエスト・パラメータを扱うか確認するために、こんなスクリプトを書いてみました。

 1  #!/usr/bin/python
 2  
 3  from bottle import route,run,request, template
 4  
 5  @route('/check')
 6  @route('/check', method='POST')
 7  def key_check():
 8      print("request.query:",end="")
 9      print(vars(request.query))
10  
11      print("request.forms:",end="")
12      print(vars(request.forms))
13  
14      print("request.params:",end="")
15      print(vars(request.params))
16  
17      try:
18          len(request.forms.get('r_name'))
19          r_name = request.forms.get('r_name')
20          h_name = request.forms.get('h_name')
21          return template('Hello {{name}} a.k.a. {{nick}}', name=r_name, nick=h_name)
22      except:
23          return '''
24      <form action="/check" method="post">
25          real name:   <input name="r_name" type="text" /> <br>
26          handle name: <input name="h_name" type="text" />
27          <input value="send" type="submit" />
28      </form>
29      '''
30  
31  run(host='localhost', port=8080, reloader=True, debug=True)

このスクリプトでは"http://localhost:8080/check"というURIを、5行目でGETメソッド、6行目でPOSTメソッドを指定して、両メソッドとも7行目のkey_check()という関数に結びつけています。

8行目から15行目は、requestオブジェクトにどういうデータが収められているかを観察するためのprint文で、これらprint文の出力は、ブラウザではなくスクリプトを起動したコンソールに出力されます。

17行目から29行目は、POSTメソッドでデータが送られてきた場合はその内容を表示し(18~21行目⁠⁠、GETメソッドでリクエストされた場合はPOSTメソッド用の<form>タグを返す(23~28行目⁠⁠、という処理です。

このスクリプトを起動して、ブラウザから"http://localhost:8080/check"にアクセスすると、ブラウザ側には入力を促す<form>タグの画面が、スクリプトを起動したコンソールには空っぽのrequestオブジェクトの中身が、それぞれ表示されます。

$ python sample_02.py
Bottle v0.13-dev server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.

request.query:{'dict': {}}
request.forms:{'dict': {}, 'recode_unicode': True}
request.params:{'dict': {}}
127.0.0.1 - - [21/Apr/2019 12:30:36] "GET /check HTTP/1.1" 200 222
図4 GETメソッドでアクセス
図4 GETメソッドでアクセス

<form>タグに何かテキストを入力して送信すると、request.formsとrequest.paramsに辞書型のデータが収められ、ブラウザには21行目の"Hello"メッセージが表示されます。

request.query:{'dict': {}}
request.forms:{'dict': {'r_name': ['kojima'], 'h_name': ['isle']}, 'recode_unicode': True}
request.params:{'dict': {'r_name': ['kojima'], 'h_name': ['isle']}}
127.0.0.1 - - [21/Apr/2019 12:33:49] "POST /check HTTP/1.1" 200 24
図5 POSTメソッドでデータを送信
図5 POSTメソッドでデータを送信

一方、URLパラメータを追加して、"http://localhost:8080/check?r_name=kojima&h_name=isle" のようにアクセスしてみると、request.queryとrequest.paramsに辞書型のデータが収まります。

request.query:{'dict': {'r_name': ['kojima'], 'h_name': ['isle']}}
request.forms:{'dict': {}, 'recode_unicode': True}
request.params:{'dict': {'r_name': ['kojima'], 'h_name': ['isle']}}
127.0.0.1 - - [21/Apr/2019 12:37:25] "GET /check?r_name=kojima&h_name=isle HTTP/1.1" 200 222
図6 GETメソッドでURLパラメータを送信
図6 GETメソッドでURLパラメータを送信

このように、BottleではURLパラメータやPOSTメソッドで送られたパラメータは、request.paramsオブジェクトに収められると共に、メソッドに応じてrequest.queryオブジェクトかrequest.formsオブジェクトに収められ、スクリプト内では"request.query.get(name)"といったメソッドやPythonの辞書型のデータとしてname属性をキーに"request.query['name']" といった形で利用できます。

なお、上記スクリプトでは <form>タグに日本語を入れると文字化けしてしまいます。これは、HTTPの仕様上、UTF-8な文字列も単なるバイト列と見做して、Latin-1な文字コードで表示してしまうためで、日本語のようなマルチバイト文字を扱う場合は、"request.query.getunicode(r_name)"や"request.forms.getunicode(h_name)"のように、getメソッドの代わりにgetunicodeメソッドを使う必要があります。


Bottleの作者、Marcel Hellkampさんが、なぜこのコードを"Bottle"と名付けたかは、ドキュメント類をあれこれ調べたものの、よくわかりませんでした。

しかしながら、Bottleのホームページのロゴなどを見ると、化学実験のような丸底フラスコが印象的で、みんながあれこれコードを書くための器、くらいのつもりで"Bottle"と名付けたような気がします。

一方、Bottle同様、Python上で開発された"Flask"というmicro Webフレームワークも存在し、こちらは"Bottle(瓶)"に対する"Flask(フラスコ)"という言葉遊びで命名されたそうです。

呑兵衛の筆者には、"ボトル"とか"フラスコ"と言われると、⁠スキットル」と呼ばれる、ズボンの尻ポケットに突っ込んでおくような、携帯タイプのウィスキー瓶を連想してしまいます(苦笑⁠⁠。

更なる連想で、今回のタイトルに借用した「ウィスキー・ボトル・ブルース」は、シンガー・ソングライターのイルカさんの2枚目のアルバム「小さな空」に入っている楽曲で、場末の飲み屋の裏口に廃棄されたウィスキー瓶たちが、ワイワイガヤガヤと自分たちの境遇を語る楽しい歌です。興味を持たれた方はぜひ聞いてみてください。

おすすめ記事

記事・ニュース一覧