PythonのFastAPIのStreamingResponseでGeneratorを使用しながら、UTF-8 BOMのファイルをダウンロードさせるAPIを作成しました。ただ、作成したファイルを確認したところ、ファイルの途中で余計なBOM(\ufeff)が付与されてしまっていました。
今回の記事では、ファイルの途中でBOMが挟まれてしまった原因を残します。
環境
- FastAPI
- 0.75.2
- (Pandas)
- 1.5.0
理由
Generatorで処理するたびに、BOMを付与していたため。
言葉で見れば当たり前の話ですので、ソースコードで見れば納得していただけると思います。
詳細
ファイルを作成してくれるStreamingResponse
を使用するソースコードは次のとおりです。ここの処理は特に問題ありません。
@router.get("/download") async def download_task(): return StreamingResponse( self.create_csv(), headers={"Content-Disposition": 'attachment; filename="file.txt"'}, )
問題のソースコードは次のとおりです。Generator
を使用したいので、返却値はyield
にします。ファイルは最終的にはUTF-8 BOM
でエンコードしたいので、utf_8_sig
を指定していました。
async def create_csv() -> AsyncGenerator[bytes, None]: for i in range(100): df = pd.DataFrame(i) stream = StringIO() _ = df.to_csv(stream, index=False, header=False, encoding="utf_8_sig") yield stream.getvalue().encode("utf_8_sig")
ただし、この処理だとGenerator
が実行されるたびにUTF-8 BOM
でエンコードされてしまいます。エンコードされるタイミングでBOM
が付与されてしまいます。
そのため、初回のみutf_8_sig
でエンコードし、それ以外はutf_8
でエンコードする必要があります。
async def create_csv() -> AsyncGenerator[bytes, None]: for i in range(100): encoding = "utf_8" if i == 0: encoding = "utf_8_sig" df = pd.DataFrame(i) stream = StringIO() _ = df.to_csv(stream, index=False, header=False, encoding=encoding) yield stream.getvalue().encode(encoding)
ソースコード
- https://github.com/hirotoKirimaru/fastapi-practice/blob/2cdc3042aabe7c6667e85a8fdb1f8a40f623ecf4/src/routers/task.py#L45-L50
- https://github.com/hirotoKirimaru/fastapi-practice/blob/2cdc3042aabe7c6667e85a8fdb1f8a40f623ecf4/src/cruds/task.py#L72-L74
- https://github.com/hirotoKirimaru/fastapi-practice/blob/2cdc3042aabe7c6667e85a8fdb1f8a40f623ecf4/src/models/csvs.py
終わりに
普段ファイルを作成する時は、初回に文字コードをエンコードして、そのファイルに対して追記していましたので問題は起きませんでした。
今回のようなローカルにファイルを作成せず、Streamingしながらクライアント側でファイルにする処理ということを初めて経験したため、障害調査にすごい時間かかってしまいました。
また、もともと、この処理自体は次の流れの中で使用していました。
- 現状をCSV化してダウンロードする
- 今回の問題点
- ローカルで加工する
- 加工したCSVをアップロードする
問題発覚したのがアップロードするタイミングだったため、ファイル読み込み側の不備を疑っていました。調べても「ファイル読み込む際に\ufeffもついてた!」だから「ファイルはutf-8ではなく、utf_8_sigで読み込もう!」って記事ばっかりだったので、ファイル作成時にやらかしていることに気づいたのはだいぶ後でした。
この記事を見た人が、二の轍を踏まないように祈るばかりです。