はじめに
FastAPIを使う場合ファイルのアップロードや提供をさせる時に色んな書き方がありますね。調べてみても書き方はそれぞれ違うものがあってどんな書き方をしたらいいか迷ってしまいます。だからこの記事では纏めてみたいと思います。
この記事の説明はFastAPIを含めたPythonがメインですが、全体の仕様をわかりやすいように、htmlとJavaScriptで書いたウェブページのコードもここに載せて一緒に説明します。又、基本的に動けばいいという機能重視なのでcssを使わず地味なページとなります。
インストール
この記事は入門の記事ではないから書く必要ないかもしれませんが、一応必要なライブラリーのことも書いておきます。FastAPIを使う際にfastapiとuvicornは勿論ですが、フォームやファイルの扱いをしたい場合python-multipartもインストールしておく必要があります。
pip install fastapi uvicorn python-multipart
前提
今回のサンプルコードはFastAPIで書いたPythonコードをweb.py
に、htmlとJavaScriptで書いた静的ウェブページはindex.html
に保存しますので、2つのファイルから基本的に構成されます。
web.py
index.html
そしてこのコマンド実行します。
uvicorn web:app --reload
そうしたらhttp://127.0.0.1:8000
でウェブアプリにアクセスします。
ファイル提供させる方法
まずはファイルを提供する側から説明します。
FileResponseを使う
FastAPIで準備しておいたファイルをダウンロードとして提供したい時色んな方法がありますが、一番簡単なのはFileResponse
を使うことです。これは名前の通りファイルという形でレスポンスするためのクラスです。
例えばxxx.txt
というファイル(名前は適当)があってダウンロードのために準備する場合はこう簡単に書けます。
from fastapi import FastAPI,responses
app = FastAPI()
@app.get('/xxx')
async def download():
return responses.FileResponse('xxx.txt',filename='fff.txt')
これでhttp://127.0.0.1:8000/xxx
にアクセスしたらファイルダウンロードは始めてfff.txt
という名前のファイルが保存されるはずです。
ここでxxx.txt
はファイルのパスで、fff.txt
ダウンロードされる時に保存される名前です。
ここに注意するべきはfilename=
はちゃんと書く必要があります。これがないとファイルダウンロードではなく、ただブラウザにそのファイルの内容を表示させるだけとなります。(この仕様はブラウザの設定にもよりますが)
URLから任意のファイルをダウンロードさせる
任意の名前のファイルを指定してその中のファイルを読み込ませてダウンロードさせることもできます。
例えばこう書きます。
from fastapi import FastAPI,responses
app = FastAPI()
@app.get('/download/{filename}')
async def download(filename: str):
return responses.FileResponse(filename,filename=filename)
これでhttp://127.0.0.1:8000/download/yyy.txt
にアクセスしたらyyy.txt
ファイルをダウンロードできます。
ページ内でダウンロードさせる
FastAPIだけの話ではないのですが、サーバーから提供されたファイルをページ移動せずにJavaScriptでダウンロードを始めさせる方法です。
まずPythonの方は静的ページを指定する部分を追加します。
from fastapi import FastAPI,responses,staticfiles
app = FastAPI()
@app.get('/download/{filename}')
async def download(filename: str):
return responses.FileResponse(filename)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
因みにfilename=
は今回入れていません。JavaScriptで仕様を制御するから、データだけ渡せばいいです。
htmlとJavaScriptの方は、fetchを使う場合。
<button onclick="dload()">実行</button>
<script>
function dload() {
fetch("/download/xxx.txt")
.then((res) => res.blob())
.then((blob) => {
let a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "fff.txt";
a.click();
});
}
</script>
わかりにくいからasync
とawait
で書き換えた方がいいかも。
<button onclick="dload()">実行</button>
<script>
async function dload() {
let blob = await (await fetch("/download/xxx.txt")).blob()
let a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "fff.txt";
a.click();
}
</script>
代わりにaxiosを使う場合。
<button onclick="dload()">実行</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
async function dload() {
let res = await axios.get("/download/xxx.txt", { responseType: "blob" })
let a = document.createElement("a");
a.href = URL.createObjectURL(res.data);
a.download = "fff.txt";
a.click();
}
</script>
そうしたらhttp://127.0.0.1:8000/
にアクセスするとこのように地味なボタンがある画面になります。
これを押したらダウンロードが始まります。
Responseを使う
以上説明したFileResponse
は予め保存しておいたファイルをそのまま渡す方法です。でもその他にも例えばファイルを別々で準備したり読み込んで少しの処理したりしたい場合は使えません。この場合はResponse
を使うという方法がいいです。
from fastapi import FastAPI,Response
app = FastAPI()
@app.get('/download')
async def download():
return Response('ああぁ',headers={'Content-Disposition': f'attachment; filename="aaa.txt"'})
これでaaa.txt
という「ああぁ」しかないテキストファイルができます。
ここでヘッダーのところにContent-Disposition
を入れてファイルとしてダウンロードさせることを示します。ファイルの名前もここに入れることになります。FileResponse
の時filename=
を入れるのと同じです。
又、ファイルを読み込んでResponse
に使うこともできます。例えばこう書いたらFileResponse
を使う時と同じ結果になります。
from fastapi import FastAPI,Response
app = FastAPI()
@app.get('/download/{filename}')
async def download(filename: str):
with open(filename,'rb') as f:
file = f.read()
return Response(file,headers={'Content-Disposition': f'attachment; filename="{filename}"'})
これ例ならFileResponse
を使った方が簡潔なのでResponse
を使う必要ないかもしれません。
ASCIIでない名前の場合
Response
を使う時に一つ問題があります。実はこのままでは日本語などASCIIでない文字を名前にしたらエラーになります。
そうしないようにちょっと面倒ですが、このように文字を変換する必要があります。例えばこのように書き換えます。
from fastapi import FastAPI,Response
from urllib.parse import quote
app = FastAPI()
@app.get('/download/{filename}')
async def download(filename: str):
with open(filename,'rb') as f:
file = f.read()
hd = {'Content-Disposition': f"attachment; filename*=utf-8''{quote(filename)}"}
return Response(file,headers=hd)
StreamingResponseを使う
ストリームとしてダウンロードさせることもできます。この場合はStreamingResponse
を使います。
使い方は色々ありますが、例えばこのようにファイルを読み込んでyield from
でジェネレーターを作るのです。
from fastapi import FastAPI,responses
from urllib.parse import quote
app = FastAPI()
@app.get('/download/{filename}')
async def download(filename: str):
def iterstream():
with open(filename,'rb') as f:
yield from f
hd = {'Content-Disposition': f"attachment; filename*=utf-8''{quote(filename)}"}
return responses.StreamingResponse(iterstream(),headers=hd)
書き方はわかりにくいが、Response
とFileResponse
で書いた場合と同じようにファイルダウンロードできます。
尚、yield from
を見て見慣れなくて意味わからないと感じる人も多いと思います。iterstream()
関数はfrom
を使わずにこうやってfor
ループで書き換えることもできます。
def iterstream():
with open(filename,'rb') as f:
for st in f:
yield st
用は、この書き方をするとファイルの中身は一気に読み込まれるのではなく、何度も繰り返して読み込まれることになるのです。
アップロードを受け取る方法
次はブラウザからのファイルを受け取らせる方法です。
Requestを使う方法
まずは一番基本的な方法から説明します。それはRequest
クラスとしてpostメソッドによって送られたデータを抽出することです。
例えばファイルを受け取ってすぐ保存するならこのように書きます。
from fastapi import FastAPI,Request,responses,staticfiles
app = FastAPI()
@app.post('/upload')
async def upload(request: Request):
form = await request.form()
file = form['ufile']
bf = await file.read()
with open(file.filename,'wb') as f:
f.write(bf)
return responses.RedirectResponse('/',status_code=301)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
ファイルをアップロードするメインのindex.html
はとりあえずJavaScriptすら書かずにただhtmlのフォームを使ってもいいです。
<form action="/upload" method="post" enctype="multipart/form-data">
<input name="ufile" type="file"><br><br>
<button type="submit">実行</button>
</form>
このようにファイル選択と実行するボタンしかないページができます。
ここでファイルを選択して実行したらフォームとしてファイルがサーバーへ送られて処理されます。
送られたフォームはFastAPIでRequest
クラスのオブジェクトに含まれ、await request.form()
で抽出できます。フォームの中で色々入れられますが、今回はファイルだけ。htmlでname="ufile"
としたので、ここではform['ufile']
で抽出するということになります。
formタグの代わりにJavaScriptでFormData
を作る
上述htmlのformタグを使うという方法ではページ移動が行われますが、そうではなくJavaScriptで実行してデータだけ送ることもできます。そうしたら元のページに戻るためのRedirectResponse
も必要ないです。代わりに何かレスポンスをreturn
こともできますが、特に何もなければreturn
なしでいいです。
FastAPIの方はちょっと書き直します。
from fastapi import FastAPI,Request,staticfiles
app = FastAPI()
@app.post('/upload')
async def upload(request: Request):
form = await request.form()
file = form['ufile']
bf = await file.read()
with open(file.filename,'wb') as f:
f.write(bf)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
index.html
の方はこうやってFormData
で書きます。
<input id="ufile" type="file"><br><br>
<button onclick="upload()">実行</button>
<script>
function upload() {
let inputelm = document.getElementById("ufile");
let file = inputelm.files[0];
if (file) {
let formdata = new FormData();
formdata.append("ufile", file);
fetch("/upload", {
method: "post",
body: formdata,
});
inputelm.value = "";
}
}
</script>
formタグがなくても見た目は変わりません。ファイル選択できて、実行をクリックしたらファイルが送られます。複雑になりますが、ページ移動せずにデータを送ることができます。
UploadFileを使う
上述の方法よりも簡潔で、フォームのことを意識せずにすぐファイルを扱うことができます。それはrequest: Request
をufile: UploadFile
に書き換えることです。
from fastapi import FastAPI,UploadFile,staticfiles
app = FastAPI()
@app.post('/upload')
async def upload(ufile: UploadFile):
bf = await ufile.read()
with open(ufile.filename,'wb') as f:
f.write(bf)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
index.html
の方は上の例から変更ないので省略します。
response
はresponse
でこの名前でなければならないのに対し、ここでのufile
は固定の名前ではなく、フォームの中で指定した任意の名前です。今回の例ではformdata.append("ufile", file);
のところで決まったのです。
型ヒントでUploadFile
を指定したことでFastAPIの中で自動的にフォームの中のファイルを抽出してくれるのです。そのおかげで書き方は簡潔で便利です。
bytesとして受け取る
UploadFile
を使う方法よりも更に簡潔な方法があります。それはUploadFile
の代わりにbytes = File()
を書くことです。そうしたら直接にバイトデータとして取得することになって、こうなるとawait
を使う必要もなく、async
も省略できます。
ただしこの方法ではファイルのデータしか取得できなくてファイルの名前などの情報が取得できないので、本当にファイルの中身しか必要としない場合だけ使った方がいいでしょう。例えば名前は既に決まっている場合です。
そうしたらこう書きます。
from fastapi import FastAPI,File,staticfiles
app = FastAPI()
@app.post('/upload')
def upload(ufile: bytes = File()):
with open('xxx.txt','wb') as f:
f.write(ufile)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
又、その他にAnnotated
を使う書き方もありますが、結果は同じなので必要ありません。一応比べられるようにその書き方もここに載せておきます。
from fastapi import FastAPI,File,staticfiles
from typing import Annotated
app = FastAPI()
@app.post('/upload')
def upload(ufile: Annotated[bytes,File()]):
with open('xxx.txt','wb') as f:
f.write(ufile)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
ファイルの名前を取得したいならrequest: Request
と一緒に使ってもいいです。
例えばこう書きます。
from fastapi import FastAPI,Request,File,staticfiles
app = FastAPI()
@app.post('/upload')
async def upload(request: Request,ufile: bytes = File()):
form = await request.form()
with open(form['ufile'].filename,'wb') as f:
f.write(ufile)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
データタイプやサイズを調べて対応する
ファイルと一緒に送られる情報はファイル名の他にも色々あります。例えばデータタイプやサイズです。これを調べてどう対応するか決めることもできます。
例えば50kB
以内のjpeg
かpng
しか受け取って、それ以外エラーを返すということもできます。
from fastapi import FastAPI,UploadFile,File,staticfiles,HTTPException
app = FastAPI()
@app.post('/upload')
async def upload(ufile: UploadFile = File()):
if(ufile.size>50*1024): # 50kB以内
raise HTTPException(status_code=400, detail='太すぎる!')
if(ufile.content_type not in ['image/jpeg','image/png']):
raise HTTPException(status_code=400, detail='jpgとpngしか受け取らない')
else:
bf = await ufile.read()
with open(ufile.filename,'wb') as f:
f.write(bf)
return 'アップロード完了'
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
index.html
の方もエラーを表示するように書きます。
<input id="ufile" type="file"><br><br>
<button onclick="upload()">実行</button>
<script>
function upload() {
let inputelm = document.getElementById("ufile");
let file = inputelm.files[0];
if (file) {
let formdata = new FormData();
formdata.append("ufile", file);
fetch("/upload", {
method: "post",
body: formdata,
}).then(async (res) => {
if (!res.ok)
alert("エラー:" + (await res.json()).detail);
});
inputelm.value = "";
}
}
</script>
尚、content_type
属性は拡張子を元に自動的に追加されたが、中身は違う場合もあります。その場合他の方法で対策する必要がありますが、それについては別の話なので割愛します。
複数ファイルをアップロードさせる
単体のUploadFile
の代わりにリストのlist[UploadFile]
に書き換えたら複数のファイルをリストとして受け取ることができます。
そして例えばこのようにfor
ループで一つずつ保存することができます。
from fastapi import FastAPI,UploadFile,staticfiles
app = FastAPI()
@app.post('/upload')
async def upload(lis_file: list[UploadFile]):
for file in lis_file:
bf = await file.read()
with open(file.filename,'wb') as f:
f.write(bf)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
index.html
の方も複数ファイル選択できるように書き直します。
<input id="lis_file" type="file" multiple><br><br>
<button onclick="upload()">実行</button>
<script>
function upload() {
let inputelm = document.getElementById("lis_file");
if (inputelm.files.length) {
let formdata = new FormData();
Array.from(inputelm.files).forEach((file) => {
formdata.append("lis_file", file);
});
fetch("/upload", {
method: "post",
body: formdata,
});
inputelm.value = "";
}
}
</script>
その他にも、list[UploadFile]
を使わずにrequest: Request
を使うこともできます。
from fastapi import FastAPI,Request,staticfiles
app = FastAPI()
@app.post('/upload')
async def upload(request: Request):
form = await request.form()
for file_ in form._list:
file = file_[1]
bf = await file.read()
with open(file.filename,'wb') as f:
f.write(bf)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
list[UploadFile]
を使った方が簡潔でわかりやすいですね。
フォームを使わずにblobとして扱う
以上のやり方では全部フォームを通じてアップロードする方法ですが、実はフォームを使わずにデータだけそのまま送る方法もあります。ただしこの場合ファイル名などのデータはヘッダーに入れる必要があります。
使う例です。FastAPI側はrequest: Request
で受け取って、.body()
でファイルデータを抽出します。名前の方は.headers['file-name']
から。
from fastapi import FastAPI,Request,staticfiles
app = FastAPI()
@app.post('/upload')
async def upload(request: Request):
bf = await request.body()
name = request.headers['file-name']
with open(name,'wb') as f:
f.write(bf)
app.mount('/', staticfiles.StaticFiles(directory='.',html=True))
index.html
の方はFormData
を作成せずに単にinput
タグの中のファイルをそのままfetch
に渡します。ただしheaders
では"file-name"
属性にファイルの名前を入れます。
<input id="ufile" type="file"><br><br>
<button onclick="upload()">実行</button>
<script>
function upload() {
let inputelm = document.getElementById("ufile");
let file = inputelm.files[0];
if (file) {
fetch("/upload", {
method: "post",
body: file,
headers: { "file-name": file.name }
});
inputelm.value = "";
}
}
</script>
尚、ここで"file-name"
は任意の名前ではなく、固定です。"filename"
などにしたら読み込めなくなります。ヘッダーの属性は何もかも勝手に入れられるわけではないからです。
この方法では一つしか送れないので、複数のファイルを送りたい場合やはりフォームに纏め送る必要があります。
参考&もっと読む
- [FastAPI]複数ファイルをrequests.post()で渡す
- 【リクエストとレスポンスを追いながら】丁寧に理解するFastAPI
- FastAPI】アップロード・ダウンロードファイルの操作
- 【Python】REST API でファイルをアップロードする (FastAPI)
- JavaScriptのFetch API について
- [JavaScript] ファイル選択ダイアログを表示してファイルを読み書きする
- FastAPI でデータをダウンロードさせる
- 【Python】FastAPIでファイルをダウンロードするWeb APIをつくる
- FastAPIでファイルダウンロードさせる時のContent-Dispositionヘッダーの公開方法
- FastAPIで画像をアップロードしてダウンロードする
- ファイルをレスポンスする【FastAPI】
- OpenCVで読み込んだ画像をFastAPIで返したいとき
- Python で手軽にWEB画面を作りたい
- FastAPI を使って PC のファイルを別の場所にコピーするだけの無駄機能を作る
- fastAPI入門
- Pythonの型ヒント「Annotated」にDeepDiveしてみる
- Pythonとyieldとreturnと時々yield from
- Python Fast API ステータスコードやエラーハンドリングについてまとめてみた
- FastAPIで実装した様々なエンドポイントのテストを書く(フォームデータの送信、クッキーの確認、ファイルのアップロード等)
- FastApiで静的ファイルをマウントする
- FastAPIを用いて音声ファイルをMongoDBに保存
- FastAPI勉強メモ
- FastAPIでStarletteとPydanticはどのように使われているか
- FastAPIとsqlite3による簡単なウェブサイトを実装する