Python で生成したデータを FastAPI のエンドポイントからダウンロードさせるにはどうしたらよいか、と思ってやってみたら普通に Response
に Content-Disposition
を指定するだけでできた、という話。
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/download")
def get_download():
"""データを CSV 形式でダウンロードする"""
content = "あ,い,う\n1,2,3" # CSV data
filename = "data.csv"
return Response(
content=content.encode("cp932"), # Microsoft Excel で開くために CP932 (≒ Shift_JIS) にエンコードする
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
media_type="text/csv",
)
ハンドラが返す型は以下のようにすればよさそう。
- (今回のように) インメモリなデータをそのまま返したいときは通常の Response
- ファイルとして保存されているデータを返したいときは FileResponse
- BytesIO などでストリームを扱いたい場合は StreamingResponse
(参考) Custom Response - HTML, Stream, File, others - FastAPI
(追記) エンコーディングを UTF-8 with BOM にする場合
UTF-8 文字列は必ずしも CP932 にエンコードできるわけではない。
'🍣'.encode('cp932') #=> UnicodeEncodeError: 'cp932' codec can't encode character '\U0001f363' in position 0: illegal multibyte sequence
🍣 という文字は CP932 には存在しないので当然で、これを正面から解決することは困難といえる。
しかし、CP932 にエンコードしたい理由が「エクセルで開きたいから」なのであれば、UTF-8 のまま BOM をつけるという方法がある。Microsoft Excel は、BOM さえあれば UTF-8 の CSV ファイルを開くことができる。
UTF-8 with BOM にエンコードしたい場合、Python では utf-8-sig
が使える。
content=content.encode("utf-8-sig"),
(参考) codecs — Codec registry and base classes — Python documentation
(追記) ファイル名に非 ASCII 文字を含みたい場合
ファイル名に日本語などの非 ASCII 文字を使用すると「latin-1 に変換できない」と怒られる。
filename = "日本語ファイル名.csv"
return Response(
content="あ,い,う\n1,2,3".encode("utf-8-sig"),
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
media_type="text/csv",
)
#=> UnicodeEncodeError: 'latin-1' codec can't encode character in position ...
これは FastAPI のバグではなく「HTTP ヘッダは ISO-8859-1 (= latin-1) を使用する」と RFC7230 で定められているからで、それ以外の文字を HTTP ヘッダで使いたい場合は URL エンコードする必要がある。
また RFC6266 によると、Content-Disposition
ヘッダでは filename*=UTF-8''ファイル名
と指定することで、URL エンコードされた UTF-8 文字列を送ることが出来るらしい。
from urllib.parse import quote
filename = "日本語ファイル名.csv"
return Response(
content="あ,い,う\n1,2,3".encode("utf-8-sig"),
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"},
media_type="text/csv",
)
Python だとこうすればできた。