Ausgabe
Ich habe einen FastAPI- GET
Endpunkt, der eine große Menge an JSON-Daten zurückgibt (~ 160.000 Zeilen und 45 Spalten). Es überrascht nicht, dass es extrem langsam ist, die Daten mit json.dumps()
. Ich lese zuerst die Daten aus einer Datei mit json.loads()
und filtere sie nach den eingegebenen Parametern. Gibt es eine schnellere Möglichkeit, die Daten an den Benutzer zurückzugeben, als die Verwendung von return data
? Im aktuellen Zustand dauert es fast eine Minute.
Mein Code sieht derzeit so aus:
# helper function to parse parquet file (where data is stored)
def parse_parquet(file_path):
df = pd.read_parquet(file_path)
result = df.to_json(orient = 'records')
parsed = json.loads(result)
return parsed
@app.get('/endpoint')
# has several more parameters
async def some_function(year = int | None = None, id = str | None = None):
if year is None:
data = parse_parquet(f'path/{year}_data.parquet')
# no year
if year is not None:
data = parse_parquet(f'path/all_data.parquet')
if id is not None:
data = [d for d in data if d['id'] == id]
return data
Lösung
Einer der Gründe für die langsame Antwort ist, dass Sie in Ihrer parse_parquet()
Methode die Datei zunächst in JSON konvertieren (mit df.to_json()
), dann in ein Wörterbuch (mit json.loads()
) und schließlich wieder in JSON, da FastAPI im Hintergrund automatisch die Rückgabe konvertiert -Wert in JSON mithilfe von jsonable_encoder
(was ziemlich langsam ist, selbst im Vergleich zur normalen JSON-Bibliothek, dh mit json.dumps()
).
Diese Antwort von @MatsLindh im Kommentarbereich schlägt vor, alternative JSON-Encoder wie orjson oder ujosn zu verwenden , die den Prozess tatsächlich beschleunigen würden, verglichen mit der Verwendung der Standardeinstellung jsonable_encoder
für die Konvertierung von dict
in JSON. Die Verwendung von Pandas to_json()
und die direkte Rückgabe eines benutzerdefinierten Befehls Response
– wie in Option 1 ( Update 2 ) dieser Antwort beschrieben – scheint jedoch die leistungsstärkste Lösung zu sein. Sie können den unten angegebenen Code verwenden, der eine benutzerdefinierte APIRoute
Klasse verwendet , um die Antwortzeit für alle verfügbaren Lösungen zu vergleichen.
Verwenden Sie Ihre eigene Parkettdatei oder den folgenden Code, um eine Beispielparkettdatei zu erstellen, die aus 160.000 Zeilen und 45 Spalten besteht.
create_parquet.py
import pandas as pd
import numpy as np
columns = ['C' + str(i) for i in range(1, 46)]
df = pd.DataFrame(data=np.random.randint(99999, 99999999, size=(160000,45)),columns=columns)
df.to_parquet('data.parquet')
Führen Sie die FastAPI-App unten aus und greifen Sie separat auf jeden Endpunkt zu, um die Zeit zu überprüfen, die zum Abschluss des Ladevorgangs und der Konvertierung der Daten in JSON benötigt wird.
app.py
from fastapi import FastAPI, APIRouter, Response, Request
from fastapi.routing import APIRoute
from typing import Callable
import pandas as pd
import json
import time
import ujson
import orjson
class TimedRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
before = time.time()
response: Response = await original_route_handler(request)
duration = time.time() - before
response.headers["Response-Time"] = str(duration)
print(f"route duration: {duration}")
return response
return custom_route_handler
app = FastAPI()
router = APIRouter(route_class=TimedRoute)
@router.get("/defaultFastAPIencoder")
def get_data_default():
df = pd.read_parquet('data.parquet')
return df.to_dict(orient="records")
@router.get("/orjson")
def get_data_orjson():
df = pd.read_parquet('data.parquet')
return Response(orjson.dumps(df.to_dict(orient='records')), media_type="application/json")
@router.get("/ujson")
def get_data_ujson():
df = pd.read_parquet('data.parquet')
return Response(ujson.dumps(df.to_dict(orient='records')), media_type="application/json")
# Preferable way to do it
@router.get("/pandasJSON")
def get_data_pandasJSON():
df = pd.read_parquet('data.parquet')
return Response(df.to_json(orient="records"), media_type="application/json")
app.include_router(router)
Even though the response time is quite fast using /pandasJSON
above (and this is the preferable way to go), you may encounter some delay on displaying the data on the browser. That, however, has nothing to do with the server side, but with the client side, as the browser is trying to display a large amount of data. If you don’t want to display the data, but instead let the user download the data to their device (which would be much faster), you can set the Content-Disposition
header to the Response
using the attachment
parameter and passing a filename
as well, indicating to the browser that the file should be downloaded. For more details, have a look at this answer and this answer.
@router.get("/download")
def get_data():
df = pd.read_parquet('data.parquet')
headers = {'Content-Disposition': 'attachment; filename="data.json"'}
return Response(df.to_json(orient="records"), headers=headers, media_type='application/json')
Ich sollte auch erwähnen, dass es eine Bibliothek namens Dask
gibt, die große Datensätze verarbeiten kann, wie hier beschrieben , falls Sie eine große Menge an Datensätzen verarbeiten müssen und der Vorgang zu lange dauert. Ähnlich wie bei Pandas können Sie die .read_parquet()
Methode zum Lesen der Datei verwenden. Da Dask anscheinend keine äquivalente .to_json()
Methode bereitstellt, können Sie den Dask DataFrame mithilfe von in Pandas DataFrame konvertieren df.compute()
und dann Pandas verwenden df.to_json()
, um den DataFrame in eine JSON-Zeichenfolge zu konvertieren und ihn wie oben gezeigt zurückzugeben.
Beantwortet von – Chris
Antwort geprüft von – Marilyn (FixError Volunteer)