如何使用FastAPI下载大文件?

Yuq*_*ang 5 python download starlette pydantic fastapi

我正在尝试从 FastAPI 后端下载一个大文件 ( .tar.gz)。在服务器端,我只是验证文件路径,然后Starlette.FileResponse返回整个文件\xe2\x80\x94,就像我在 StackOverflow 上的许多相关问题中看到的那样。

\n

服务器端:

\n
return FileResponse(path=file_name, media_type=\'application/octet-stream\', filename=file_name)\n
Run Code Online (Sandbox Code Playgroud)\n

之后,我收到以下错误:

\n
  File "/usr/local/lib/python3.10/dist-packages/fastapi/routing.py", line 149, in serialize_response\n    return jsonable_encoder(response_content)\n  File "/usr/local/lib/python3.10/dist-packages/fastapi/encoders.py", line 130, in jsonable_encoder\n    return ENCODERS_BY_TYPE[type(obj)](obj)\n  File "pydantic/json.py", line 52, in pydantic.json.lambda\nUnicodeDecodeError: \'utf-8\' codec can\'t decode byte 0x8b in position 1: invalid start byte\n
Run Code Online (Sandbox Code Playgroud)\n

我也尝试使用StreamingResponse,但得到了同样的错误。还有其他方法可以做到吗?

\n

我的代码中的StreamingResponse

\n
  File "/usr/local/lib/python3.10/dist-packages/fastapi/routing.py", line 149, in serialize_response\n    return jsonable_encoder(response_content)\n  File "/usr/local/lib/python3.10/dist-packages/fastapi/encoders.py", line 130, in jsonable_encoder\n    return ENCODERS_BY_TYPE[type(obj)](obj)\n  File "pydantic/json.py", line 52, in pydantic.json.lambda\nUnicodeDecodeError: \'utf-8\' codec can\'t decode byte 0x8b in position 1: invalid start byte\n
Run Code Online (Sandbox Code Playgroud)\n

好的,这里是这个问题的更新。\n我发现这个错误没有发生在这个 api 上,而是 api 做了转发请求。

\n
@x.post("/download")\nasync def download(file_name=Body(), token: str | None = Header(default=None)):\n    file_name = file_name["file_name"]\n    # should be something like xx.tar\n    def iterfile():\n        with open(file_name,"rb") as f:\n            yield from f\n    return StreamingResponse(iterfile(),media_type=\'application/octet-stream\')\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,如果我返回一个StreamingResponsewith.tar文件,它会导致(可能)编码问题。

\n

使用requests时,记得设置相同的media-type。这是media_type=\'application/octet-stream\'。它有效!

\n

Chr*_*ris 12

如果您发现使用类文件对象yield from f时速度相当慢,例如:StreamingResponse

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

some_file_path = 'large-video-file.mp4'
app = FastAPI()

@app.get('/')
def main():
    def iterfile():
        with open(some_file_path, mode='rb') as f:
            yield from f

    return StreamingResponse(iterfile(), media_type='video/mp4')
Run Code Online (Sandbox Code Playgroud)

您可以创建一个生成器,在其中使用指定的块大小以的形式读取文件;因此,加快了这一进程。示例如下。

请注意,StreamingResponse可以使用async生成器或普通生成器/迭代器来传输响应主体。如果您使用open()不支持async/的标准方法await,则必须使用 normal 声明生成器函数def。不管怎样,FastAPI/Starlette 仍然会异步工作,因为它会检查你传递的生成器是否是异步的(如源代码所示),如果不是,它将在单独的线程中运行生成器,使用iterate_in_threadpool,即然后等待。

您可以Content-Disposition在响应中设置标头(如本答案以及此处此处所述)以指示内容是否预期显示inline浏览器中(如果您正在流式传输,例如.mp4视频、.mp3音频文件、等),或作为下载attachment保存在本地(使用指定的filename)。

至于media_type(也称为 MIME 类型),有两种主要的 MIME 类型(请参阅常见 MIME 类型):

  • text/plain是文本文件的默认值。文本文件应该是人类可读的,并且不得包含二进制数据。
  • application/octet-stream是所有其他情况的默认值。未知文件类型应使用此类型

对于具有扩展名的文件.tar,如您的问题所示,您还可以使用与不同的子类型octet-stream,即x-tar. 否则,如果文件类型未知,请坚持使用application/octet-stream. 有关常见 MIME 类型的列表,请参阅上面的链接文档。

选项 1 - 使用普通发电机

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

CHUNK_SIZE = 1024 * 1024  # = 1MB - adjust the chunk size as desired
some_file_path = 'large_file.tar'
app = FastAPI()

@app.get('/')
def main():
    def iterfile():
        with open(some_file_path, 'rb') as f:
            while chunk := f.read(CHUNK_SIZE):
                yield chunk

    headers = {'Content-Disposition': 'attachment; filename="large_file.tar"'}
    return StreamingResponse(iterfile(), headers=headers, media_type='application/x-tar')

Run Code Online (Sandbox Code Playgroud)

选项 2 - 使用async发电机aiofiles

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import aiofiles

CHUNK_SIZE = 1024 * 1024  # = 1MB - adjust the chunk size as desired
some_file_path = 'large_file.tar'
app = FastAPI()

@app.get('/')
async def main():
    async def iterfile():
       async with aiofiles.open(some_file_path, 'rb') as f:
            while chunk := await f.read(CHUNK_SIZE):
                yield chunk

    headers = {'Content-Disposition': 'attachment; filename="large_file.tar"'}
    return StreamingResponse(iterfile(), headers=headers, media_type='application/x-tar')
Run Code Online (Sandbox Code Playgroud)