【问题标题】:How to upload a CSV file in FastAPI and convert it into JSON?在 FastAPI 中上传 CSV 文件并将其转换为 JSON 格式
【发布时间】:2022-04-29 19:32:30
【问题描述】:

我正在尝试通过先将 CSV 文件上传到 FastAPI 来将其转换为 JSON,但是当我尝试直接处理它(而不将其存储在某处)时,我收到错误:

Error : FileNotFoundError: [Error 2] No such file or directory : "testdata.csv"

代码:

async def upload(file: UploadFile = File(...)):
    data = {}    
    with open(file.filename,encoding='utf-8') as csvf:
        csvReader = csv.DictReader(csvf)
        for rows in csvReader:             
            key = rows['No']
            data[key] = rows    
    return {data}```

【问题讨论】:

  • os.getcwd()的输出是什么,和testdata.csv的位置一样吗?
  • 实际上我是直接在 UI 中上传文件,我没有存储在任何地方,所以当我使用 getcwd() 命令时,我确实得到了 200 响应代码,但响应正文 [ null ]

标签: python csv upload fastapi csvtojson


【解决方案1】:

UploadFile 使用 Python 的 SpooledTemporaryFile,这是一个“存储在内存中的文件”,并且“一旦关闭就会被销毁”。有关这方面的更多信息,请查看this answer

选项 1

要在途中解决问题(即,从 csv 文件读取而不使用从 contents = await file.read() 获得的文件内容),您可以将文件内容复制到 NamedTemporaryFile 中(再次检查 @987654324 @out 了解更多信息),然后使用它来迭代 csv 内容。下面是一个工作示例:

import uvicorn
from fastapi import FastAPI, File, UploadFile
from tempfile import NamedTemporaryFile
import os
import csv

app = FastAPI()
    

@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    contents = await file.read()
    data = {}
    file_copy = NamedTemporaryFile(delete=False)
    
    try:
        with file_copy as f:  # The 'with' block ensures that the file closes and data are stored
            f.write(contents);
        
        with open(file_copy.name,'r', encoding='utf-8') as csvf:
            csvReader = csv.DictReader(csvf)
            for rows in csvReader:             
                key = rows['No']
                data[key] = rows  
    finally:
        file_copy.close()  # Remember to close any file instances before removing the temp file
        os.unlink(file_copy.name)  # delete the file
    
    return data
    

选项 2

或者,如前所述,更优雅的解决方案是使用上传文件的字节数据,从而避免将它们复制到新的临时文件中。将字节转换为字符串,然后将字符串对象加载到内存中的文本缓冲区(即StringIO)中,如here 所述,您可以将其传递给csv阅读器。下面的例子:

from fastapi import FastAPI, File, UploadFile
import csv
from io import StringIO

app = FastAPI()

@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    data = {}
    contents = await file.read()
    decoded = contents.decode()
    buffer = StringIO(decoded)
    csvReader = csv.DictReader(buffer)
    for rows in csvReader:             
        key = rows['No']
        data[key] = rows  
        
    buffer.close()
    return data

选项 3

您还可以将上传文件中的字节写入BytesIO 流,然后您可以将其转换为 pandas 数据帧。接下来,使用to_dict() 方法,您可以将数据帧转换为字典并返回(默认情况下,FastAPI 将使用jsonable_encoder 转换为JSON 并返回JSONResponse)。

from fastapi import FastAPI, File, UploadFile
from io import BytesIO
import pandas as pd

app = FastAPI()
    
@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    contents = await file.read()
    buffer = BytesIO(contents)
    df = pd.read_csv(buffer)
    buffer.close()
    return df.to_dict(orient='records')

【讨论】:

  • file.read() 必须是 file.file.read()
  • @snowmanstark 所有UploadFile 方法“调用下面的相应文件方法(使用内部SpooledTemporaryFile”。请查看documentation
  • 我说“file.read() 必须是 file.file.read()”是错误的。如果我们要转换 """ contents = await file.read() decoded = contents.decode() buffer = StringIO(decoded) """ 那么它将是buffer = StringIO(file.file.read().decode())
  • 不应将内容的异步读取更改为同步,除非您已决定声明您的路线使用def,而不是async def。这样做(当使用async def 声明路由时)将导致整个服务器阻塞,直到该操作完成。请看下面的参考资料来理解async/await的概念:123
  • 谢谢克里斯,我明白你的意思了!
【解决方案2】:

您获得Error : FileNotFoundError: [Error 2] No such file or directory : "testdata.csv" 的原因是因为您正在尝试读取未存储在本地的文件。

如果您想以这种方式读取文件,您应该在继续之前保存上传的文件:

async def upload(uploaded_file: UploadFile = File(...)):
    # save csv to local dir
    csv_name = uploaded_file.filename
    csv_path = 'path_to/csv_dir/'
    file_path = os.path.join(csv_path, csv_name)
    with open(file_path, mode='wb+') as f:
        f.write(uploaded_file.file.read())

    # read csv and convert to json
    data = {}
    with open(file_path, mode='r', encoding='utf-8') as csvf:
        csvReader = csv.DictReader(csvf)
        for rows in csvReader:             
            key = rows['No']
            data[key] = rows    
    return {data}

【讨论】:

  • 但是我试图直接这样做,就像直接在 UI 中上传文件一样,它应该被提取并转换为 JSON 格式,稍后我会将 JSOn 数据保存到 MySQL 数据库,但我的代码是仅获取上传文件的名称,而不是实际文件本身
  • 你有没有在函数定义后直接尝试content = await file.read()?之后可以使用file.content_type属性获取文件扩展名来确定处理方式。
【解决方案3】:

异步函数upload()中的file已经打开,可以直接从中获取字符,无需再次打开。同样在 FastAPI 中,UploadFile 类实际上是从标准库tempfile.SpooledTemporaryFile 派生的,不能通过指定临时文件的路径来访问。

例如,如果您使用 CPython 并在类 Unix 系统中读取 upload() 中的 file.filename 的值,它会返回一个数字而不是格式良好的路径,因为 @987654329 类的任何实例@ 将创建一个文件描述符(在某些时候当前存储的数据超过 max_size)并在访问 SpooledTemporaryFile.filename 时简单地返回文件描述符(在 Unix 中应该是一个数字)

【讨论】:

  • 那么我应该如何使用这个 Spooled 临时文件我很困惑
猜你喜欢
  • 1970-01-01
  • 2022-08-20
  • 1970-01-01
  • 2018-03-06
  • 2019-08-15
  • 2021-02-14
  • 2018-08-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多