Flaskの便利な使い方まとめ

flaskはpythonでウェブアプリケーションを作るためのライブラリである。ここでは、flaskを使ううえでの基本的な方法をまとめる。

#インストール

~~~shell
pip install flask
~~~

#導入

まず最初に、最も簡単な"Hello World"を返すAPIを作る。

~~~app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def root():
    return "Hello World!"

if __name__ == "__main__":
    app.run()
~~~

- 関数`@app.route(url)`でデコレートすると、そのurlにアクセスすると、その関数が実行されるようになる。

~~~unix
python app.py
~~~

上で作成した`app.py`を実行すると、ウェブサーバーが起動する。ブラウザで`http://localhost:5000/`にアクセスすると、画面に`Hello World!`が表示されるようになる。

#開発時の設定
~~~app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def root():
    return "Hello World!"

if __name__ == "__main__":
    app.run(debug=True,host='localhost',port=5000)
~~~
- `main`を上のように変える。
- `debug=True`を渡すと（デバッグモード）、サーバーが起動している時に、`app.py`や後述するhtmlファイルなどを変更した時に、自動的にロードされる。`debug`の初期値は`False`。
- `host`・`port`は自由に決めれる。`port`の初期値は5000。

#外部からアクセスできるようにする。
~~~app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def root():
    return "Hello World!"

if __name__ == "__main__":
    app.run(debug=False,host='0.0.0.0',port=5000)
~~~
- `main`を上のように変える。
- `host=localhost`の時にはローカルからしかアクセスできない。外部からアクセスできるようにするには、`host="0.0.0.0"`。
- セキュリティの理由から`debug=False`にする。

#ブラウザも同時に起動する

~~~app.py
import threading,webbrowser
from flask import Flask
app = Flask(__name__)

@app.route('/')
def root():
    return "Hello World!"

if __name__ == "__main__":
    threading.Timer(1.0, lambda: webbrowser.open('http://localhost:5000') ).start()
    app.run(debug=False)
~~~
- `main`でブラウザーを開くコマンドを追加する。
- `app.py`を実行すると同時にブラウザーが立ち上がり、ページが表示される。
- `app.run()`に`debug=True`を渡すと、コードを変更するたびに`main`が再実行されるため、その度にブラウザーが追加で開いてしまう。それを防ぐため`debug=False`を渡している. 

デバッグモード(`debug=True`)でブラウザーを自動で開くようにしたい時には、`main`は

~~~python
if __name__ == "__main__":
    app.run(debug=True)
~~~
にして、

~~~shell
#!/bin/sh
( sleep 2; open http://localhost:5000/ ) &
python app.py
~~~
のようなシェルスクリプトを実行するような方法がある。

#htmlファイルを返す

~~~app.py
from flask import Flask,render_template
app = Flask(__name__)

@app.route('/')
def main():
    return render_template('index.html')

if __name__ == "__main__":
    app.run()
~~~

~~~/templates/index.html
<html>
<body>
  Hello World!
</body>
</html>
~~~

- htmlファイルを返す時には`render_template(file)`を用いる。
- htmlファイル(ここでは`index.html`)は`app.py`と同じ場所に`templates`というディレクトリを作り、その中に配置する。
- `http://localhost:5000/`にブラウザからアクセスすると`index.html`がレンダリングされ、この場合は"Hellow World!"が表示される。

#CSS・JavaScriptファイル
- CSSファイルやJavaScriptファイルはディレクトリ`./static`に配置する。画像もここに配置する。
- htmlファイルからは、それぞれ

~~~html
<head>
  <script src="/static/script.js"></script>
  <link rel="stylesheet" href="/static/style.css">
</head>
~~~

のように呼び出す。

#GETリクエスト

- ここでは`/get`のアドレスにGETリクエストを受けて、`{"message":"Hello World!"}`というjsonオブジェクトを返すapiを考える。

~~~app.py
from flask import Flask,request,jsonify,render_template
app = Flask(__name__)

@app.route('/')
def index_html():
    return render_template('index.html')

@app.route('/get',methods=['GET'])
def get():
    return jsonify({'message':'Hello world!'})

if __name__ == '__main__':
    app.run()
~~~
- デコレータ`@app.route(url,methods)`でリクエストを受け入れる`url`と`methods`を指定。
- `jsonify()`はオブジェクトを文字列になおし、適切なレスポンスを生成する関数である。

~~~/templates/index.html
<html>
<body>
  <button onclick='get();'>get</button>

  <script>
    function get(){
      fetch('/get').then(
        response => response.json()
      ).then(
        json => { document.body.innerHTML += JSON.stringify(json); }
      ).catch(
        e => console.error(e)
      );
    }
  </script>
</body>
</html>
~~~
- ボタンを押すと、返ってくるJSONを文字列として表示される。

fetchが成功したい時に何か処理を行いたい時には、次のようにすれば良い。

~~~javascript
fetch('/get').then(
  response => (
    if (response.ok){
        //成功時に行ないたい処理
    } else {
        throw new Error()
    }
  ).catch(
    e => console.error(e)
  );
~~~


#POSTリクエスト

- ここでは`/post`のアドレスにPOSTリクエストを受けて、同じJSONをそのまま返すapiを考える。

~~~app.py
from flask import Flask,request,jsonify,render_template
app = Flask(__name__)

@app.route('/')
def index_html():
    return render_template('index.html')

@app.route('/post',methods=['POST'])
def post(): #送られてきたjsonをそのまま返す。
    req = request.json
    return jsonify(req)

if __name__ == '__main__':
    app.run()
~~~
- POSTリクエストで送られてきたJSONは`request.json`で得られる。

~~~/templates/index.html
<html>
<body>
<button onclick='post();'>post</button>

<script>
function post(){
  fetch('/post', {
    method: "POST",
    headers: { "Content-Type": "application/json"},
    body: JSON.stringify({"message": "Hello World!!!"})
  }).then(
    response => response.json()
  ).then(
    json => { document.body.innerHTML += JSON.stringify(json); }
  ).catch(
    e => console.error(e)
  );
}
</script>
</body>
</html>
~~~
- ここでは、ボタンを押すと`{'message':'Hello World!!!'}`のJSONをデータとして持つPOSTリクエストを送る。同じJSONが返ってくるのでそれを表示する。

#画像データを返すAPI

サーバーはデータを受信し、matplotlibでグラフを作成し、その画像データを返すようなAPIを考える。ここでは画像データをdataURLに変換してデータを返す。dataURLは画像データをurlとして表現したものである。

~~~app.py
from flask import Flask,render_template,jsonify,request

import matplotlib
matplotlib.use('agg')
from matplotlib import pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvasAgg
import io,base64

app = Flask(__name__)

@app.route('/')
def main():
    return render_template('index.html')

@app.route('/image',methods=['POST'])
def image():
    data = request.json
    x = data['x']
    y = data['y']

    #図を作成
    plt.figure(figsize=(3,3),dpi=100)
    plt.plot(x,y,'k*-',markersize=10)

    #図をdataURLに変更
    canvas = FigureCanvasAgg(plt.gcf())
    png_output = io.BytesIO()
    canvas.print_png(png_output)
    img_data = base64.b64encode(png_output.getvalue()).decode('utf-8')
    dataurl = 'data:image/png;base64,'+img_data

    return jsonify({'img_data':dataurl})

if __name__ == "__main__":
    app.run()
~~~

- まず、サーバー上での図の描画になるので、下のコマンドを`matplotlib`関連のライブラリのimportの一番最初に行うことに注意する。

~~~python
import matplotlib
matplotlib.use('agg')
~~~

- `image()`ではmatplotlibでデータからグラフを作成し、それをdataURLに変換し、それを返している。

~~~/templates/index.html
<html>
<body>

<button onclick="image();">img</button>
<div id='img'></div>

<script>

function image(){
  fetch('/image', {
    method: "POST",
    headers: { "Content-Type": "application/json"},
    body: JSON.stringify({"x":[0,1,2,3,4,5], "y":[5,2,4,0,3,8]})
  }).then(
    response => response.json()
  ).then(
    json => { document.getElementById('img').innerHTML = `<img src="${json['img_data']}">`; }
  ).catch(
    e => console.error(e)
  );
}

</script>

</body>
</html>
~~~

- imgボタンをクリックすると、データ`x=[0,1,2,3,4,5]`、`y=[5,2,4,0,3,8]`をサーバーに送る。
- レスポンスが返ってきたら、dataURLをimgタグのsrcに入れれば画像が表示される。


#クロスオリジンリクエスト（COR）：別のドメインからのアクセス

デフォルトでは別のドメインからのアクセスは制限されている。これを許可するためには`flask-cors`のパッケージを使う。

~~~shell
pip install flask-cors
~~~


~~~app.py
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route('/')
def root():
    return "Hello World!"

if __name__ == "__main__":
    app.run()
~~~
- 単に`CORS`を`app`に適用するだけでOK。

#ユーザー認証（ダイジェスト認証）

認証のために`flask-httpauth`のパッケージを用いる。

~~~shell
pip install flask-httpauth
~~~

~~~app.py
from flask import Flask
from flask_httpauth import HTTPDigestAuth

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret key here'
auth = HTTPDigestAuth()

users = {
    'Michael': 'pw_m',
    'Smith': 'pw_s'
}

@auth.get_password
def get_pw(username):
    if username in users:
        return users.get(username)
    return None

@app.route('/')
@auth.login_required
def root():
    return "Hello World!"

if __name__ == "__main__":
    app.run()
~~~
- ユーザー名を与えられた時に、そのユーザーが存在するならば、そのユーザーのパスワードを返し、ユーザーが存在しないならば、`None`を返す関数を作り(`get_pw`)、それを`@auth.get_password`でデコレートする。
- ここではユーザは'Michael'と'Smith'二人存在し、パスワードはそれぞれ'pw_m'、'pw_s'であるとする。
- 認証が必要な関数を`@auth.login_required`でデコレートする。
- アクセスすると認証画面が最初に表れ、正しいユーザー名・パスワードを入力すると、"Hello World"が表示される。