(2019-08-14: 全面的に書き換えました)
Flask アプリで動的に生成したデータ(例:RDB のクエリー結果を CSV にして返す)をダウンロードさせるような機能を実現する場合、これまでデータを一度 view 関数で完成させた後に返送するしかないと思っていた。が、ジェネレーター関数を使ってコンテンツを徐々に返送する、つまりストリーミングする機能が存在していた。
実装例
たとえば巨大な CSV のダウンロードが発生しうる場合、まず一行ずつ yield
するような関数を実装してやる。たとえば、大きな CSV を疑似的に作ってダウンロードする例は次のようになる:
from random import random import flask app = flask.Flask(__name__) _html_data = """<!DOCTYPE html><html><body> <p><a href="/download" download>Download</a></p> </body></html>""" def _generate_random_csv(shape=(100_000, 128)): """乱数のCSVを行ごとに生成する.""" num_rows, num_cols = shape[:2] for i in range(num_rows): yield ",".join([f"{random():.6f}" for j in range(num_cols)]) + "\n" @app.route("/") def index(): return flask.make_response(_html_data) @app.route("/download") def download(): resp = flask.Response(_generate_random_csv()) #resp.content_length = 115_200_000 # 総容量を前もって知らせる resp.content_type = "text/csv; charset=utf-8" resp.headers["Content-Disposition"] = "attachment; filename=data.csv" return resp if __name__ == "__main__": app.run()
動作例
実際にブラウザー上で動かすと次のような感じになる:
なお総データ容量が分かっている場合は resp.content_length
にそれを設定しやると、ブラウザー側では進捗(残り時間)を表示できるようになる。Chrome の場合は次のような感じ:
注意点など
- Response に渡したジェネレータが
yield
するデータは、そのまま相手に返送される- 改行を含むテキストデータの場合は改行コードも含める必要あり
- アップロード済みファイルなど、動的に生成しないデータなら
flask.send_file()
やflask.send_from_directory()
を使った方が効率的 - Flask や Werkzeug というよりブラウザーの仕様だと思うけれど、誤った総データ容量を Content-Length ヘッダーに設定するとダウンロード結果が不正になるので要注意