4
1

FastAPI でデータをダウンロードさせる

Last updated at Posted at 2023-11-28

Python で生成したデータを FastAPI のエンドポイントからダウンロードさせるにはどうしたらよいか、と思ってやってみたら普通に ResponseContent-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 だとこうすればできた。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1