とほほのFlask入門

目次

Flaskとは

インストール

下記の環境を想定しています。

OS: Ubuntu 20.04 LTS
Python: 3.8.10
Flask: 2.0.2

下記でインストールします。

$ sudo apt update		# 必要に応じてアップデートする
$ sudo apt -y upgrade		# 必要に応じてアップグレードする
$ sudo apt -y install python3 python3-pip 
$ sudo pip install Flask

チュートリアル

Hello world!

http://~/ にアクセスすると Hello world! を返却するサンプルです。hello.py として作成してください。

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello world!</p>"

プログラム名を環境変数 FLASK_APP に設定し、flask run を実行します。対象ファイルが wsgi.py または app.pyPYTHONPATH で参照可能であれば、FLASK_APP の設定は省略できます。

$ export FLASK_APP=hello
$ flask run

他ホストからの接続を受け付ける場合は -h 0.0.0.0 を、ポートを指定するには -p port を指定します。--reload をつけるとプログラム修正時に自動的にリロードします。

$ flask run -h 0.0.0.0 -p 5000

http://~/ にアクセスして Hello world! が表示されれば成功です。

プログラムに下記を追加して

from flask import Flask

app = Flask(__name__)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

下記の様に実行することもできます。

$ python3 hello.py

デバッグモード

FLASK_ENVdevelopment を指定するとデバッグモードで動きます。デバッグモードでは、プログラムを修正すると自動的にリロードし、エラーが発生した際に、ブラウザにエラーの詳細情報が表示されます。

$ export FLASK_ENV=development
$ flask run

ルーティング指定

@app.route()

@app.route() で、どのURLにどのメソッドでアクセスされると、どのメソッドを呼び出すかを指定します。下記は /users に対して GET メソッドでアクセスされたら users メソッドを呼び出します。

@app.route("/users")
def users():
    ...

下記の様に引数を受け取ることもできます。

@app.route("/users/<user_name>")
def users(user_name):
    ...

引数には型を指定することもできます。型には string(デフォルト), int, float, path, uuid が指定できます。

@app.route("/users/<int:user_id>")
def users(user_id):
    ...

GET 以外のメソッドに対応するには methods を指定します。

@app.route("/users", methods=["GET", "POST"])
def users():
    ...

@app.route() の代わりに app.add_url_rule() を使用することもできます。

app = Flask(__name__)
app.add_url_rule("/users", view_func=users)

URL末尾のスラッシュ

@app.route() で指定するURL末尾にスラッシュをつけると /foo でアクセスしても /foo/ にリダイレクトされて、スラッシュ有りでも無しでもアクセスできますが、つけない場合はスラッシュ無しでしかアクセスできません。

@app.route("/foo/")	# /foo でも /foo/ でもアクセスできる
@app.route("/baa")	# /baa はアクセス可。/baa/ はアクセス不可

データを受け取る

requestオブジェクト

メソッドの中では request オブジェクトを参照できます。リクエストに関する情報が含まれています。

from flask import request

@app.route("/users")
def users():
    print(request.method)

メソッド・パス情報

メソッドやパス情報として下記などを参照できます。

request.method			メソッド(GET)
request.url			URL(http://127.0.0.1:5000/users?uid=U12345)
request.host_url		ホストURL(http://127.0.0.1:5000/)
request.scheme			スキーマ(http)
request.host			ホスト(127.0.0.1:5000")
request.path			パス名(/users)
request.query_string		クエリ文字列(uid=U12345)

リクエストパラメータ

GETリクエストのパラメータは下記で参照できます。

request.args[key]		GETパラメータ(Keyがなければエラー)
request.args.get(key)		GETパラメータ(KeyがなければNone)
request.args.get(key, "...")	GETパラメータがない場合のデフォルト値を指定
request.args.get(key, type=int)	GETメータをint型で受け取る

POSTリクエストのパラメータは下記で参照できます。

request.form[key]		POSTパラメータ(Keyがなければエラー)
request.form.get(key)		POSTパラメータ(KeyがなければNone)
request.form.get(key, "...")	POSTパラメータがない場合のデフォルト値を指定
request.form.get(key, type=int)	POSTメータをint型で受け取る

JSONデータは下記で参照できます。

request.get_json()		ボディのJSON。送信側が Content-Type: application/json で送信する必要あり
request.json			get_json()と同じ

その他のリクエスト情報

クライアント情報として下記などを取得できます。

request.accept_charsets		受付可能なキャラクタセット
request.accept_encodings	受付可能なエンコーディング
request.accept_languages	受付可能な言語
request.accept_mimetypes	受付可能なMIMEタイプ
request.remote_addr		リモートアドレス
request.remote_user		リモートユーザ

リクエスト情報として下記などを取得できます。

request.date			日時
request.content_type		コンテントタイプ
request.referrer		遷移元URL
request.get_data()		ボディ(バイナリ)

下記でヘッダ情報を参照できます。

request.headers.get("Host")	特定ヘッダ(KeyがなければNone)
request.headers["Host"]		特定ヘッダ(Keyがなければエラー)

下記で環境変数を参照できます。

request.environ.get("PATH")	特定の環境変数(Keyが無ければNone)
request.environ["PATH"]		特定の環境変数(Keyが無ければエラー)

その他の情報については下記を参照してください。

参考:https://flask.palletsprojects.com/en/2.0.x/api/#flask.Request

ファイルアップロード

ファイルのアップロードを受け付けてサーバ側に保存するサンプルは下記の様になります。

<form method="POST" action="/upload" enctype="multipart/form-data">
  <input type="file" name="datafile">
  <button>OK</button>
</form>
@app.route("/upload", methods=["POST"])
def upload():
    f = request.files["datafile"]
    f.save("/tmp/datafile")
    return "Received: " + f.filename

参考:https://flask.palletsprojects.com/en/2.0.x/patterns/fileuploads/

データを返却する

レスポンスデータ

メソッドの戻り値は HTML などのボディデータを指定します。

@app.route("/")
def main():
    return "<!DOCTYPE html><html><head>..."

JSONを返却する場合はオブジェクトをそのまま返却することもできます。

@app.route("/")
def main():
    return {"name": "Yamada"}

make_response() を使用すると レスポンスヘッダCookie を返却することが可能になります。

from flask import make_response
@app.route("/")
def main():
    resp = make_response("Return data...")
    return resp

HTTPステータス

2番目の戻り値にHTTPステータスを返却することができます。

return "...", 404
return {...}, 404
return resp, 404

テンプレートファイル

Flask には Jinja2 と呼ばれるテンプレートエンジンが含まれています。テンプレートは 下記の様に templates フォルダに設置する必要があります。

file main.py
folder templates
  file main.html

下記の様にテンプレートファイルを名を指定してその内容を返却します。

from flask import render_template

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

テンプレートにパラメータを渡すこともできます。

render_template("main.html", name="Yamada", age=26)
<div>Name: {{ name }}</div>
<div>Age: {{ age }}</div>

オブジェクトやクラスインスタンスを渡すこともできます。オブジェクトで渡す場合、クラスインスタンスで渡す場合どちらでも、テンプレート側では p.name でも p["name"] でも受け取ることができます。

p = { "name": "Yamada", "age": 26 }
render_template("main.html", p=p)
<div>Name: {{ p.name }}</div>
<div>Age: {{ p.age }}</div>

リストを渡すこともできます。

users = [ "Yamada", "Tanaka", "Suzuki" ]
return render_template("main.html", users=users)
{% for user in users %}
  <div>{{ user }}</div>
{% endfor %}

Jinja2 に関する詳細は下記を参照してください。

参考:https://jinja.palletsprojects.com/en/3.0.x/

スタティックファイル

スタティックファイルは static フォルダ配下に置きます。

file main.py
folder static
   file css
     file common.css
   folder img
     file sample.png
   folder js
     file common.js
<!doctype html>
<html lang="ja">
 <head>
  <title>MAIN</title>
  <link rel="stylesheet" href="/static/css/common.css">
  <script src="/static/js/common.js"></script>
 </head>
 <body>
  <h1>Main</h1>
  <img src="/static/img/sample.png">
 </body>
</html>

レスポンスヘッダ

レスポンスヘッダを指定するには下記の様にします。

@app.route("/")
def main():
    resp = make_response("OK")
    resp.headers["X-TEST-HEADER"] = "This is test header."
    return resp

Cookie を読み取るには request.cookies、設定するには set_cookie() を使用します。

@app.route("/")
def main():
    user_name = request.cookies.get("user_name")
    resp = make_response("...")
    resp.set_cookie("user_name", user_name)
    return resp

参考:https://flask.palletsprojects.com/en/2.0.x/api/#flask.Request.cookies

リダイレクト

他のページにリダイレクトするには、redirect() を使用します。

from flask import redirect

@app.route("/")
def main():
    return redirect("/login")

参考:https://flask.palletsprojects.com/en/2.0.x/api/#flask.redirect

URLを関数名で指定する

環境によってエンドポイントのパス名も変わることがあるため、URLは絶対パスで指定しないほうがよいとされています。url_for() を用いることで "do_login" というメソッド名の文字列から、そのメソッドに対応するURLを得ることができます。

from flask import url_for

@app.route("/login")
def do_login():
    ...

@app.route("/")
def main():
    return redirect(url_for("do_login"))

下記の様に引数付きのURLを得ることもできます。

@app.route("/users/<user_id>")
def get_user(user_id):
    ...

@app.route("/")
def main():
    print(url_for("get_user", user_id="U12345"))	# => "/users/U12345"
    return "OK\n"

下記の様にテンプレートの中で使用することもできます。

<a href="{{ url_for("get_user", user_id="U12345") }}">...</a>

関数名 "static" はスタティックディレクトリを示します。

<link rel="stylesheet" href="{{ url_for("static", filename="css/common.css") }}">
<script src="{{ url_for("static", filename="js/common.js") }}"></script>
<img src="{{ url_for("static", filename="img/sample.png") }}">

その他ノウハウ

エラーページのカスタマイズ

なんらかのエラーが発生した場合に Flask が返却するエラーページをカスタマイズすることができます。@app.errorhandler() の代わりに app.register_error_handler(404, not_found) で登録することもできます。

app = Flask(__name__)

@app.errorhandler(404)
def not_found(error):
    return render_template("error_404.html"), 404

JSONを扱う

JSONデータを受け取るには、クライアントから Content-Type: application/json ヘッダをつけて渡してやる必要があります。

$ curl -X POST -H "Content-Type: application/json" http://localhost:5000/ -d '{"name":"Tanaka"}'

データ返却時は、オブジェクトをそのまま返却します。JSONを文字列として返却する場合は レスポンスヘッダContent-Type: application/json を指定します。

@app.route("/")
def main():
    print request.json
    return { "name": "Yamada", "age": 26 }

セッション

セッションを用いたアクセス回数表示サンプルです。5以上でクリアしています。app.secret_key にシステム毎に異なるランダムデータを設定しておく必要があります。セッション情報は secret_key で暗号化され、ブラウザの Cookie に保存されますので、あまり大きなデータを格納しようとすると Cookie の上限によってうまく保存できなくなります。

app = Flask(__name__)
app.secret_key = b"efb94fcefa1ef7f281d69a979cdf251b2b9bdd8b770d7a0fbfb9427287fec9f6"

@app.route("/")
def main():
     count = session.get("count", 0)
     count = count + 1
     session["count"] = count
     if count >= 5:
         session.clear()
     return "count = %d" % count

ロギング

INFO 以上のログを標準エラー出力とログファイルに書き出すサンプルです。Python 標準の logging を使用しているので、詳細はそちらを参照してください。ログ設定の変更時はサーバを再起動する必要があります。デバッグモードの場合は常にすべてのログが出力されます。

from flask import Flask
from logging.config import dictConfig

dictConfig({
    "version": 1,
    "formatters": {
        "default": {
            "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
            "formatter": "default"
        },
        "file": {
            "class": "logging.FileHandler",
            "filename": "/tmp/flask.log",
            "formatter": "default"
        }
    },
    "root": {
        "level": "INFO",
        "handlers": ["console", "file"]
    },
    "disable_existing_loggers": False,
})

app = Flask(__name__)

@app.route("/")
def main():
    app.logger.debug("This is debug message.")
    app.logger.info("This is info message.")
    app.logger.warning("This is warning message.")
    app.logger.error("This is error message.")
    app.logger.critical("This is critical message.")
    return "OK"

前処理・後処理

すべてのリクエストの前処理・後処理ハンドラを登録することができます。スタティックファイル呼び出しの際にも呼ばれることに注意してください。

def before_handler():
    print("=== Before URL handler")

def after_handler(response):
    print("=== After URL handler")
    return response

app.before_request(before_handler)
app.after_request(after_handler)

グローバルオブジェクト

g というグローバルオブジェクトに、1回のリクエストの間で有効なグローバル情報を保存することができます。

from flask import g

g.sample_data = "Sample"
if "sample_data" in g:
    print(g.sample_data)

コンフィグ

app.config はコンフィグ情報を管理します。

app = Flask(__main__)
app.config["DEBUG"] = True

from_pyfile(), from_envvar(), form_json(), from_object() などを使用して各種フォーマットのコンフィグファイルから一括して読み込むこともできます。

app.config.form_json("config.json")

コンフィグファイルは環境別にソースファイルとは別の場所に置くことが推奨されます。下記の様にしてコンフィグファイルを /etc/myapp 配下に置くことができます。

app = Flask(__name__, instance_path="/etc/myapp", instance_relative_config=True)
app.config.from_pyfile("config.py")

標準のコンフィグ情報には下記などがあります。アプリケーション専用のコンフィグ値を定義することもできます。

ENV				production
DEBUG				False
TESTING				False
PROPAGATE_EXCEPTIONS		None
PRESERVE_CONTEXT_ON_EXCEPTION	None
SECRET_KEY			None
PERMANENT_SESSION_LIFETIME	datetime.timedelta(days=31)
USE_X_SENDFILE			False
SERVER_NAME			None
APPLICATION_ROOT		/
SESSION_COOKIE_NAME		session
SESSION_COOKIE_DOMAIN		None
SESSION_COOKIE_PATH		None
SESSION_COOKIE_HTTPONLY		True
SESSION_COOKIE_SECURE		False
SESSION_COOKIE_SAMESITE		None
SESSION_REFRESH_EACH_REQUEST	True
MAX_CONTENT_LENGTH		None
SEND_FILE_MAX_AGE_DEFAULT	None
TRAP_BAD_REQUEST_ERRORS		None
TRAP_HTTP_EXCEPTIONS		False
EXPLAIN_TEMPLATE_LOADING	False
PREFERRED_URL_SCHEME		http
JSON_AS_ASCII			True
JSON_SORT_KEYS			True
JSONIFY_PRETTYPRINT_REGULAR	False
JSONIFY_MIMETYPE		application/json
TEMPLATES_AUTO_RELOAD		None
MAX_COOKIE_SIZE			4093

詳細:https://flask.palletsprojects.com/en/2.0.x/config/#builtin-configuration-values

クラスメソッドを呼び出す

as_view() を用いることで、ハンドラとしてクラスメソッドを呼び出すことができます。クラスは dispatch_request() メソッドを実装しておく必要があります。methods でHTTPメソッドを指定できます。

from flask import Flask
from flask.views import View

app = Flask(__name__)

class GetUser(View):
    methods = ["GET", "POST"]
    def dispatch_request(self, user_id):
        return "GET_USER(%s)\n" % user_id

app.add_url_rule("/users/<user_id>", view_func=GetUser.<em>as_view("get_users")</em>)

実装サンプル

ログイン認証

セッションを用いてでログインを行うサンプルです。Flask-Login というライブラリもよく利用されます。

from flask import Flask, request, session, render_template, redirect, url_for

app = Flask(__name__)
app.secret_key = b"efb94fcefa1ef7f281d69a979cdf251b2b9bdd8b770d7a0fbfb9427287fec9f6"

# @login_requiredデコレータの実装
# 関数呼び出し前にセッションを確認してNoneであればログイン画面にリダイレクトする
def login_required(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if session.get("username") is None:
            return redirect(url_for("login"))
        else:
            return func(*args, **kwargs)
    return wrapper

# メイン画面(ログイン認証が必要)
@app.route("/")
@login_required
def main():
    return render_template("main.html")

# メンバ画面(ログイン認証が必要)
@app.route("/member")
@login_required
def member():
    return render_template("member.html")

# ログイン画面
# ログインが成功するとセッションにusernameを設定する
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        if check_password(username, password):
            session["username"] = username
            return redirect(url_for("main"))
        else:
            return render_template("login.html", error_msg="Login error.")
    else:
        return render_template("login.html")

# ログアウト画面(セッションをクリアする)
@app.route("/logout")
def logout():
    session.clear()
    return render_template("login.html")

# パスワードチェック関数
# 簡易的にusernameとpasswordが合致すればOKとしている
def check_password(username, password):
    return username == password

REST-APIサンプル

MariaDB に接続してユーザ情報の CRUD(Create, Read, Update, Delete) を行う簡単な REST-API のサンプルです。エラー処理は省略しています。

from flask import Flask, request
import mysql.connector

app = Flask(__name__)
db = mysql.connector.connect(host="...", user="...", password="...", database="...")

def dbExec(sql, params={}):
    cursor = db.cursor()
    cursor.execute(sql, params)
    result = cursor.fetchall()
    cursor.close()
    return result

@app.route("/users")
def listUsers():
    result = dbExec("SELECT user_id, email FROM users")
    users = []
    for (user_id, email) in result:
        users.append({"user_id": user_id, "email": email})
    return {"users": users}

@app.route("/users", methods=["POST"])
def addUser():
    params = {"user_id": request.json["user_id"], "email": request.json["email"]}
    dbExec("INSERT INTO users VALUES (%(user_id)s, %(email)s)", params)
    return {"result": "OK"}

@app.route("/users/<user_id>")
def getUser(user_id):
    params = {"user_id": user_id}
    result = dbExec("SELECT user_id, email FROM users WHERE user_id = %(user_id)s", params)
    return {"user_id": result[0][0], "email": result[0][1]}

@app.route("/users/<user_id>", methods=["PATCH"])
def updateUser(user_id):
    params = {"user_id": user_id, "email": request.json["email"]}
    dbExec("UPDATE users SET email = %(email)s WHERE user_id = %(user_id)s", params)
    return {"result": "OK"}

@app.route("/users/<user_id>", methods=["DELETE"])
def deleteUser(user_id):
    params = {"user_id": user_id}
    dbExec("DELETE FROM users WHERE user_id = %(user_id)s", params)
    return {"result": "OK"}

下記の様に呼び出します。

$ curl http://localhost:5000/users
$ curl -X POST -H "Content-Type: application/json" http://localhost:5000/users \
  -d '{"user_id": "U12345", "email": "foo@example.com" }'
$ curl http://localhost:5000/users/U12345
$ curl -X PATCH -H "Content-Type: application/json" http://localhost:5000/users/U12345 \
  -d '{"email": "foo@example.com" }'
$ curl -X DELETE http://localhost:5000/users/U12345