一年前、pythonでウェブサーバを作ろうとしたことがあったんだけれど、道半ばで挫折した。確かcgiとかいう機能を使ったり、pyスクリプトとテンプレートを分けたり、中途半端に非同期処理を組み込んだりした挙句、日本語表示がうまくいかなくてイヤになってヤメちゃったんだよね。そのリトライをするぞ。あれから一年、趣味とはいえきちんとクラスを覚え、ちっちゃなものではあるがゲームを作り、Skype Botを作り、DialogFrameなんつー俺俺フレームワークまで作ったのだ。今ならばいける! かもしれない!

というわけで「1日1python」のお時間です。今回は前回のhttp.serverではなく、Bottleモジュールを使ってウェブサーバを作る。

先回のhttp.serverをもう一度触り直してみたのだけれど、「pythonを使ってページを表示するにはどうしてもURLが/cgi-bin/になっちまう。もっと自由にURLを決めたい」「htmlテンプレートの中でpython側の変数とか処理をかけない」なんつー不満点があったのよな。でもBottleがすべて解決してくれた。

 

0 とりあえずこれ書いとく

#!/usr/bin/env python
# coding: utf-8
from bottle import route, run, template, request
from bottle import error, redirect, HTTPResponse, static_file

# これは一番下に
run(host='localhost', port=8000, debug=True, reloader=True)
以降のスクリプトは全部この中に書く。なお localhost を 0.0.0.0 にすると、サーバが外からも見えるようになるようだぜ。俺はそうやって、スマホからチェックをしていた。

 

1 一番シンプルなやつ

@route('/1/')
def page_01(name):
    return '<h1>YO!</h1>'

http://localhost:8000/1/ でアクセスできる。

 

2 URLにワイルドカードを使う

@route('/2/:name')
def page_02(name):
    return f'<h1>YO {name}!</h1>'

http://localhost:8000/2/Wada とかでアクセスできる。

 

3 テンプレートを使う

python側がこう。テンプレート側へ変数を渡すにはこういう書き方をする。

@route('/3/')
def page_03():
    a = 'LALALA'
    b = {'0':'AAA', '1':'BBB'}
    return template('template_03', a=a, b=b)

テンプレートはpythonスクリプトと同階層にviewsというディレクトリを作ってその中に置く。以下にpythonから受け取った変数の表示方法とかテンプレート内pythonスクリプトの書き方とかを記載する。

# template_03.tpl

■ pythonスクリプトから受け取った変数の表示
ふつーの変数: {{a}}
ディクショナリとか: {{b['0']}}

■ for文
<ul>
% for i in range(5)
    <li>{{i}}</li>
% end
</ul>

■ if文
% if b['1'] == 'BBB':
    <p>...</p>
% else:
    <p>...</p>
% end

■ いちいち % を行頭につけるのが面倒なアナタには
<%
    num = len(b)
    if num in [1]:
        pass
    elif num in [2]:
        pass
    end
%>

4 GETクエリを使う

@route('/4/')
def page_04():
    # get一覧を見たいならこれ
    print(request.query.__dict__)

    # クエリの取り出し方
    get_content = request.query.a

    return template('template_04', get_content=get_content)

テンプレート側は普通のgetフォームなので割愛。

5 POSTクエリを使う

@route('/5/')
@route('/5/', method='POST')
def page_05():
    # post一覧を見たいならこれ。
    print(request.params.__dict__)

    # 取り出し方。
    post_content = request.forms.get('name')

    return template('template_05', post_content=post_content)

テンプレート側は普通のpostフォームなので割愛。POSTのほうは、route()デコレータ表記をふたつ書かないといけないのに注意。

6 リダイレクトする

@route('/6/')
def page_06():
    return redirect('/5/')

7 ステータスコードでハンドリングする

えっとぶっちゃけ俺はステータスコードなるものは404くらいしかしらんので404の例を。存在しないURLにアクセスしたら、/5/にリダイレクトするようにしてみた。

@error(404)
def error_404(error):
    r = HTTPResponse(status=302)
    r.set_header('Location', '/5/')
    return r

errorを使った場合、6で使用したredirect()は使えないみたい。でも上述の書き方をしたらむりくりリダイレクトできた。HTTPヘッダにステータスコード302(リダイレクト)と飛び先を設定する方式。

8 静的ファイルを使う

pyファイルと同階層に静的ファイルを置くディレクトリ(例ではstatic)を作り、ソコを静的ファイル置き場として登録する。

@route('/static/:file_path')
def static(file_path):
    return static_file(file_path, root='./static')

HTMLから呼び出すときはこんな感じの相対パスで。

<img src="/static/image/python3.jpg">

 

こんなところで俺のリトライは無事成功した。自身の成長を感じられてふんすふんすしつつ改めて先回の奮闘を見ると、いやあpython全然関係ないところで詰まってんなあ。「ネットに情報がねえ」なんて理由で挫折しているが、まあ多分それは、当時もBottleとかDjangoとかそういう単語自体は目に入ってたんだろうけど、馴染みのないモジュールの蓋を開けてみることにまだ抵抗があったのだろう。ちなみにBottleの前にDjangoも試してみたが、あっちはなんかスゲー規模がでかいわディレクトリに色々ファイルが増えやがるわでタイヘンだったので、import一本で済むBottleを今回は採用した。

使用をヤメたhttp.serverについては、使うのがhtmlだけだったら即採用だと思うぜ。なにせターミナルから一行で立ち上がるからな。

$ python -m http.server ポート番号

学習の集大成として、小さなジャンケンゲームを作ったので下に載っけとく。スクリプトと、htmlを載せたところで、あっそうかhtmlを載せるってことはcssも載せないと締まらないのか…クソ長くなっちゃう…ヤだ…ってなったので折りたたんどく。

こんなゲーム。

スクリプトがこれ。janken.py

#!/usr/bin/env python
# coding: utf-8

from bottle import route, run, template, request
from bottle import error, HTTPResponse, static_file
import random


@error(404)
def error_404(error):
    r = HTTPResponse(status=302)
    r.set_header('Location', '/')
    return r


@route('/static/:file_path')
def static(file_path):
    return static_file(file_path, root='./static')


@route('/')
@route('/', method='POST')
def janken():
    req = request.params.__dict__['dict']
    counter = {
        'draw': int(request.forms.get('draw')) if ('draw' in req) else 0,
        'lost': int(request.forms.get('lost')) if ('lost' in req) else 0,
        'won': int(request.forms.get('won')) if ('won' in req) else 0,
    }
    hand = {
        'mine': -1,
        'his': int(request.forms.get('his')) if ('his' in req) else -1,
    }
    result = -1

    if (hand['his'] != -1
            and 0 <= int(hand['his']) <= 2):
        hand['his'] = int(hand['his'])
        hand['mine'] = random.randint(0, 2)
        result = (hand['his'] - hand['mine'] + 3) % 3
        if result in [0]:
            counter['draw'] += 1
        elif result in [1]:
            counter['lost'] += 1
        elif result in [2]:
            counter['won'] += 1

    return template('janken', counter=counter, hand=hand, result=result)

run(host='0.0.0.0', port=8000, debug=True, reloader=True)

テンプレートがこれ。views/janken.tpl

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link rel="stylesheet" type="text/css" href="/static/janken.css" media="all">
        <title>BOTTLE SERVER</title>
    </head>

    <body>
        <div class="outer">
            <form action="/" method="post">

                <div class="inner_header" style="text-align:center;">
                    C'mon, J A N K E N...
                </div>

                <div class="inner_body">
                    <div class="shelf">
                        % if hand['mine'] != -1:
                        <button class="hand" style="background-color:orange;">
                            <%
                            hand_mine_name = ''
                            if hand['mine'] == 0:
                                hand_mine_name = '00_rock.png'
                            elif hand['mine'] == 1:
                                hand_mine_name = '01_scissors.png'
                            elif hand['mine'] == 2:
                                hand_mine_name = '02_paper.png'
                            end
                            %>
                            <img src="/static/{{hand_mine_name}}">
                        </button>
                        % end
                    </div>
                    <div class="shelf">
                        % if result != -1:
                            <%
                            result_class = ''
                            result_label = ''
                            if result == 0:
                                result_class = 'draw'
                                result_label = 'DRAW'
                            elif result == 1:
                                result_class = 'lost'
                                result_label = 'YOU LOST..'
                            elif result == 2:
                                result_class = 'won'
                                result_label = 'YOU WON!!'
                            end
                            %>
                            <span class="{{result_class}}">{{result_label}}</span>
                        % end
                    </div>
                    <div class="shelf">
                        <div class="box_row">
                            <div class="button box_row_left" style="background:none;width:40px;">
                                <span style="color:red;">●</span>{{counter['won']}}
                                <input type="hidden" name="won" value="{{counter['won']}}">
                            </div>
                            <div class="button box_row_left" style="background:none;width:40px;">
                                <span style="color:black;">●</span>{{counter['draw'] }}
                                <input type="hidden" name="draw" value="{{counter['draw']}}">
                            </div>
                            <div class="button box_row_left" style="background:none;width:40px;">
                                <span style="color:blue;">●</span>{{counter['lost'] }}
                                <input type="hidden" name="lost" value="{{counter['lost']}}">
                            </div>
                        </div>
                    </div>
                </div>

                <div class="inner_footer">
                    <div class="box_row">
                        <%
                        tmp = ['','','']
                        if hand['his'] != -1:
                            tmp[hand['his']] = 'background-color:orange;'
                        end
                        %>
                        <div class="button box_row_left">
                            <button class="hand" type="submit" name="his" value="0" style="{{tmp[0]}}">
                                <img src="/static/00_rock.png" alt="01_rock">
                            </button>
                        </div>
                        <div class="button box_row_left">
                            <button class="hand" type="submit" name="his" value="1" style="{{tmp[1]}}">
                                <img src="/static/01_scissors.png" alt="02_scissors">
                            </button>
                        </div>
                        <div class="button box_row_left">
                            <button class="hand" type="submit" name="his" value="2" style="{{tmp[2]}}">
                                <img src="/static/02_paper.png" alt="03_paper">
                            </button>
                        </div>
                    </div>
                </div>

            </form>
        </div>
    </body>
</html>

CSS。static/janken.css

@charset "UTF-8";

/********************
構造わかりやすさのための色つけ。消してOK。
********************/

/*body {
    border:1px solid black;
    background:lime;
}
div.outer {
    background:gray;
}
div.inner_header {
    background:red;
    border:1px solid black;
}
div.inner_body {
    background:green;
}
div.inner_footer {
    background:blue;
}*/

/********************
大まかな構造。
********************/

body {
    width:240px;
    height:320px;
}

div.outer {
    width:225px;
    height:305px;
}

div.inner_header {
    height:60px;
}

div.inner_body {
    height:185px;
}

div.inner_footer {
    height:60px;
}

/********************
細かな構造。
********************/

div.box_row {
    overflow:hidden;
}

div.box_row_left {
    float:left;
}

div.box_row_right {
    float:right;
}

/********************
個別の装飾。
********************/

div.button {
    background:#DBD9B5;
    margin:2px 10px;
    padding:0px 2px;
    text-align:center;
}

button.hand {
    width:50px;
    height:50px;
    padding:0;
    margin:0;
    border:0;
}

span.draw {
    text-shadow:black 1px 1px 0px, black -1px 1px 0px,
                black 1px -1px 0px, black -1px -1px 0px;
}

span.lost {
    text-shadow:blue 1px 1px 0px, blue -1px 1px 0px,
                blue 1px -1px 0px, blue -1px -1px 0px;
}

span.won {
    text-shadow:red 1px 1px 0px, red -1px 1px 0px,
                red 1px -1px 0px, red -1px -1px 0px;
    font-size:140%;
}

div.shelf {
    margin:2px 10px;
    padding:0px 2px;
    text-align:center;
    height:60px;
}

ブログの構造として折りたたみは好きじゃないんだけど、こういうときはあってよかったと思うぜ。

以下の記事からリンクされています