Flaskアプリからデータをストリーミング方式で返送する

(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()

動作例

実際にブラウザー上で動かすと次のような感じになる:

f:id:sgryjp:20190814155448g:plain

なお総データ容量が分かっている場合は resp.content_length にそれを設定しやると、ブラウザー側では進捗(残り時間)を表示できるようになる。Chrome の場合は次のような感じ:

f:id:sgryjp:20190814155706g:plain

注意点など

  • Response に渡したジェネレータが yield するデータは、そのまま相手に返送される
    • 改行を含むテキストデータの場合は改行コードも含める必要あり
  • アップロード済みファイルなど、動的に生成しないデータなら flask.send_file()flask.send_from_directory() を使った方が効率的
  • Flask や Werkzeug というよりブラウザーの仕様だと思うけれど、誤った総データ容量を Content-Length ヘッダーに設定するとダウンロード結果が不正になるので要注意