Blow Up by Black Swan

Python-Flask の使い方(クイックスタート編 Part.3)

クイックスタート編のPart.3です。CSSやJavaScriptファイルを利用するためのスタティックフォルダ、テンプレートのレンダリング、そしてリクエストデータの扱いについてまとめています。

・各記事の記載内容
Part.1 -> 1.最小アプリケーション~3.デバッグモード
Part.2 -> 4.ルーティング
Part.3 -> 5.スタティックフォルダ〜7.リクエストデータへのアクセス
Part.4 -> 8.リダイレクトエラー〜最後

公式ドキュメント: Quickstart – Flask Documentation

※なお当記事は 2019 年 9 月現在の最新バージョンである Flask1.1.x の公式ドキュメントに基づいています。今後の Flask や Python のアップデートによっては記事内容に何らかの齟齬が生じることもありますので、事前にバージョンを確認頂くか、適宜公式ドキュメントを参照頂ければと思います。

5. スタティックフォルダ

動的なウェブアプリケーションでも静的(スタティック/static)ファイルは必要になります。静的ファイルは、ほとんどCSSファイル(style.cssなど)やJavaScriptファイル(jquery.jsなど)になります。その静的ファイルをWebサーバが読み込むよう設定されているのが理想ですが、開発中だとFlaskがその機能をサポートします。パッケージやモジュールの隣に/staticフォルダを作り静的ファイルを格納することで、そこに格納された静的ファイルはアプリケーションで利用することが可能となります。

静的ファイル用のURLを生成するためには、特別なstaticエンドポイントを指定します。

# "static/style.css"のファイルの場合のコード例
url_for('static', filename='style.css')

6. テンプレートのレンダリング

PythonでHTMLを生成させるのは、アプリケーションの安全性を保つためにHTMLのエスケープ処理を常に行う必要があり、実に厄介で手間がかかります。このエスケープ処理を自動化するため、FlaskではJinja2テンプレートエンジンを設定しています。

テンプレートを読み込むにはrender_template()メソッドを使います。テンプレートファイルを利用するには、テンプレートエンジンに送りたいテンプレートファイルの名称を指定し、変数の場合はキーワード引数として指定します。

Flaskはtemplateフォルダからテンプレートファイルを探し出します。フォルダの設置場所はスタティックファイルの場合と同じで、アプリケーションがモジュールの場合はモジュールフォルダの隣に、パッケージの場合はパッケージ内となります。

テンプレートファイルでは、Jinja2テンプレートの機能を全て利用できます(Jinja2 Documentation)。

  • app.render_template( filepath, keyword_args )…テンプレートを読み取るためのメソッド
    • 第1引数にはテンプレートファイル(ファイル名やそれを格納したリスト、変数名など)、キーワード引数でテンプレート内で使用する値を指定する
<モジュールの場合>
/application.py
/templates
    /hello.html
<パッケージの場合>
/application
    /__init__.py
    /templates
        /hello.html
from flask import Flask, render_template
app = Flask(__name__)

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)
# テンプレートファイル: hello.html
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
  <h1>Hello {{ name }}!</h1>
{% else %}
  <h1>Hello, World!</h1>
{% endif %}

・実行フロー

  1. “/hello”or”/hello/<name>”にアクセス
  2. hello関数が発動
  3. nameの値を確認
  4. render_templateメソッドの発動
  5. テンプレートを読み込みつつnameのif文を判定し、HTMLを生成
  6. ブラウザにHTMLを返す
quickstart9
quickstart10

テンプレート内では、requestsessiongオブジェクトにアクセスでき、get_flashed_messages()メソッドを使うことができます(gオブジェクトは保持したい情報をグローバルに持ち続けるオブジェクト)。

Jinja2のテンプレートは、テンプレートの継承機能によって高い利便性を持ちます(参照:テンプレートの継承)。テンプレートの継承は、基本的にヘッダーやナビゲーション、フッターのような各ページで共通して使用されるパートを一つのテンプレートでまとめて管理できるようにします。

エスケープは自動的に利用されるため、nameコンテンツが利用された場合、自動的にエスケープ処理されます。但し、変数や挿入されるHTMLが安全であるとわかっている場合(マークアップ記法をHTMLに変換するモジュールからHTMLが送られてくる場合など)、テンプレート内でMarkupクラス(Jinja2のクラス)か|safeフィルターを使うことで、そのHTMLを安全なものとみなしエスケープ処理を行わないようにすることもできます。

# Markupクラスの使用例
from flask import Markup

Markup('<strong>Hello %s!</strong>') % '<blink>hacker</blink>'
# 戻り値 => Markup(u'<strong>Hello <blink>hacker</blink>!</strong>')
# Markupクラスのインスタンスはエスケープされず文字列とみなされるためエスケープ処理されていない

Markup.escape('<blink>hacker</blink>')
# 戻り値 => Markup(u'<blink>hacker</blink>')
# escapeメソッドを使うことでインスタンスの文字列をエスケープ処理

>>> Markup('<em>Marked up</em> » HTML').striptags()
# 戻り値 => u'Marked up \xbb HTML'
# striptagsメソッドでタグだけ取り除いている

7. リクエストデータへのアクセス

ウェブアプリケーションでは、ユーザがサーバに送ったデータを処理することは非常に重要なことです。Flaskでは、この情報はグローバルオブジェクトのrequestオブジェクトによってアプリケーションに提供されます。Flaskは”コンテキストローカル”という仕組みによって、このオブジェクトをグローバルな状態に保ち、スレッドが安全な状態を維持することができます。

7-1. コンテキストローカル

(このパートは複雑なパートのため、コンテキストローカルがどのように機能し、テストするか知りたい場合のみ参照することが推奨されています)

Flaskの一部のオブジェクトは、グローバルオブジェクトです。これらのオブジェクトは実際には、特定のコンテキスト(文脈)に対してローカルであるオブジェクトのプロキシ(代替物)です。コンテキストがスレッドを処理する流れを整理すると、送られたリクエストに対し、ウェブサーバは新しいスレッドを生成し(基礎となっているオブジェクトは、スレッド以外の他の同時接続システムにも対応している)、Flaskがそのリクエストの処理をすることで、アプリケーションは現在のスレッドがアクティブなスレッドであると認識でき、現在のアプリケーションとWSGI環境をそのコンテキストに結びつける。この一連の処理はスマートに行われるため、あるアプリケーションは途中で中断することなく別のアプリケーションを呼び出すことができます。

これによって、ユーザはユニットテストのようなことを行わない限り、コンテキストの仕組みなどを知らずにアプリケーションを開発することができます。なお、ユニットテストではリクエストオブジェクトが存在しないため、リクエストに依存するコードが機能しなくなります。リクエストオブジェクトをユニットテスト自身で生成し、コンテキストに結びつけることで対応できますが、最も簡単な方法はコンテキストマネージャーのtest_request_context()メソッドを使うことです。withステートメントと一緒に利用することで、テストリクエストをコンテキストと結びつけ、やりとりできるようになります。

もう一つ可能な方法は、WSGIの環境全体をrequest_context()メソッドに送る方法です。

# ユニットテストのコード例(test_request_context()を使う方法)
from flask import request

with app.test_request_context('/hello', method='POST'):
    # now you can do something with the request until the
    # end of the with block, such as basic assertions:
    assert request.path == '/hello'
    assert request.method == 'POST'
# ユニットテストのコード例(request_context()を使う方法)
from flask import request

with app.request_context(environ):
    assert request.method == 'POST'

7-2. リクエストオブジェクト

リクエストオブジェクトについてはAPIセクション(Request)で説明されています。リクエストオブジェクトは、最も一般的に処理されるオブジェクトの一つです。

リクエストオブジェクトを利用するには、まずflaskモジュールからrequestオブジェクトをインポートします。様々なリクエストメソッド(HTTPメソッドのこと)は、method属性を指定することで、利用できるようになる。POSTPUTで送信されるデータにアクセスするにはform属性を利用します。

form属性にkeyが存在しない場合、特殊なKeyErrorが生じます。keyErrorを捕捉し処理することもできますが、キャッチしない場合は”HTTP 400 Bad Request”エラーページが表示されます。そのため、多くのケースでユーザはその問題に対処する必要はありません。

from flask import request

@app.route('/login', methods=['POST', 'GET'])
def login():
    error = None
    # HTTPメソッドがPOSTの場合
    if request.method == 'POST':
        if valid_login(request.form['username'],    #vilid_login()はログインIDとパスワードを照合する仮メソッド
                       request.form['password']):
            return log_the_user_in(request.form['username'])  #log_the_user_in()もフォーム送信用の仮メソッド
        else:
            error = 'Invalid username/password'    #ログインIDまたはパスワードが不正な場合の仮メソッド
    return render_template('login.html', error=error

URLクエリ(?key=value)で送られてきたパラメータにアクセスするにはargs属性を使います。

URLパラメータにアクセスするにはgetメソッドを使うか、KeyErrorをキャッチすることが推奨されています。理由としては、ユーザはURLを変更する可能性があり、また”400 Bad Request”ページをレスポンとして返すのはユーザーフレンドリーではないためです。

# args属性のコード例
searchword = request.args.get('key', '')

7-3. ファイルのアップロード

Flaskでは簡単にアップロードされたファイルを処理できます。但し、HTMLのformでenctype="multipart/form-data"属性をセットする必要があり、これがないとブラウザはファイルを送信することができません。

アップロードされたファイルは、メモリやファイルシステムの一時保管場所に保存されます。リクエストオブジェクトのfiles属性を参照することで、これらのファイルにアクセスすることができます。このファイルはPythonのfileオブジェクトのように振舞いますが、save()メソッドを持ち、サーバのファイルシステムに保存することを可能とします。

from flask import request

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # files属性でアップロードファイルにアクセス
        f = request.files['the_file']    #投稿されたfileのkeyはHTMLフォームのnameタグの値
        
        #save()メソッドでサーバにファイルを保存
        f.save('/var/www/uploads/uploaded_file.txt')
    ...

アプリケーションにファイルがアップロードされる際にファイル名がなんなのか把握するには、filename属性を利用します。但し、この値は偽造することができるため、軽々しく信用するべき値ではなく、クライアントのファイル名を使用してファイルを保存する場合は、Werkzeugが提供するsecure_filename()メソッドを介在させることが推奨されます。

#secure_filename()メソッドの利用例
from flask import request
from werkzeug.utils import secure_filename

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        f = request.files['the_file']
        f.save('/var/www/uploads/' + secure_filename(f.filename))    #secure_filename()メソッドでスクリーニングしてから利用

7-4. クッキー

クッキーにアクセスするには、cookies属性を利用し、クッキーをセットするにはレスポンスオブジェクトのset_cookie()メソッドを使います。リクエストオブジェクトのcookies属性はクライアントが送った全てのクッキーを辞書型で保持します。セッションを使う場合は、直接クッキーを使ってはならず、代わりにFlaskのSessionオブジェクトを使います。これはクッキーのセキュリティを高めるためです。

クッキーはレスポンスオブジェクトにセットされます。たいていはview関数から文字列を返しますので(render_template()で生成されるHTML)、Flaskはそれらをレスポンスオブジェクトに変換します。明示的にレスポンスオブジェクトを生成する場合、meke_response()関数を使い、レスポンスオブジェクトを編集することができます。

レスポンスオブジェクトが存在しない時にクッキーをセットしたい場合は、据え置き型のリクエストコールバックを利用すれば可能となります(参照: https://flask.palletsprojects.com/en/1.1.x/patterns/deferredcallbacks/#deferred-callbacks )。レスポンスオブジェクトについてはResponse 参照。

#cookies属性の利用
from flask import request

@app.route('/')
def index():
    username = request.cookies.get('username')    #KeyErrorを避けるために[]ではなくgetメソッドを使っている

#cookies属性のセット
from flask import make_response
@app.route('/')
def index():
resp = make_response(render_template(...))    # responseオブジェクトを生成
resp.set_cookie('username', 'the username')   # クッキーをセット
return resp

続きは次の記事へ。

Python-Flask の使い方(クイックスタート編 Part.4)