Bootcamp i backendutveckling med FastAPI · Lektion

Asynkron bild- och dokumenttransformering

Bearbeta miniatyrbilder, storleksändringar och formatkonvertering i bakgrundsworkers för att hålla request-latensen låg.

Lektion 4 av 413 steg

Asynkron bild- och dokumenttransformering är en gratis lektion i Bootcamp i backendutveckling med FastAPI på CoddyKit. Detta är lektion 4 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Bootcamp i backendutveckling med FastAPI, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Bootcamp i backendutveckling med FastAPI innehåller totalt 4 lektioner.

Varför flytta mediehanteringen?

Att ändra storlek på en bild eller konvertera en PDF kan ta från hundratals millisekunder till flera sekunder. Om ni utför arbetet i begärandehanteraren får klienten vänta och er worker-process blockeras.

Mönstret för FastAPI-tjänster på B2-nivå är:

  • Ta emot uppladdningen och spara originalet snabbt
  • Returnera 202 Accepted med ett jobb-id
  • Skapa miniatyrer, ändra storlek och konvertera format i en bakgrundsworker

Detta håller nere svarstiden för begäranden och gör det möjligt att skala tungt CPU-arbete oberoende.

BackgroundTasks eller en riktig kö

FastAPI levereras med BackgroundTasks, som kör en funktion efter att svaret har skickats, men fortfarande i samma process. Det passar bra för billiga, snabba efterföljande åtgärder, som att skicka ett e-postmeddelande eller skriva en logg.

För CPU-tunga medieomvandlingar är det fel verktyg: det konkurrerar med er händelseslinga och avslutas om processen startas om. Använd hellre en dedikerad uppgiftskö (Celery, RQ, Dramatiq eller arq) med Redis som backend, så att arbetet överlever driftsättningar och kan skalas horisontellt.

from fastapi import FastAPI, BackgroundTasks

app = FastAPI()

def log_upload(filename: str) -> None:
    # cheap follow-up work only
    print(f"received {filename}")

@app.post("/upload")
async def upload(background: BackgroundTasks):
    background.add_task(log_upload, "photo.png")
    return {"status": "accepted"}

Ta emot snabbt, bearbeta senare

Ändpunkten bör göra så lite som möjligt: validera filen, strömma den till lagringen, skapa en jobbrad och lägga en uppgift i kön. Observera att vi läser uppladdningen i delar, så att en stor fil aldrig behöver läsas in helt i minnet.

  • await file.read(chunk) undviker kraftiga minnestoppar
  • Vi returnerar ett job_id som klienten kan fråga efter
  • Den faktiska omvandlingen sker i process_image.delay(...)
import uuid, aiofiles
from fastapi import FastAPI, UploadFile, status

app = FastAPI()

@app.post("/images", status_code=status.HTTP_202_ACCEPTED)
async def create_image(file: UploadFile):
    job_id = str(uuid.uuid4())
    dest = f"/data/originals/{job_id}_{file.filename}"
    async with aiofiles.open(dest, "wb") as out:
        while chunk := await file.read(1024 * 1024):
            await out.write(chunk)
    process_image.delay(job_id, dest)  # enqueue
    return {"job_id": job_id, "status": "queued"}

Skapa miniatyrer med Pillow

Pillows Image.thumbnail() ändrar storleken på plats, bevarar bildförhållandet och förstorar aldrig bilden. Det är rätt grundfunktion för miniatyrer eftersom resultatet ryms inom den ruta ni anger.

Använd Image.LANCZOS-omampling för skarpa förminskningar och anropa img.convert("RGB") innan ni sparar som JPEG, så att bilder med alfakanal (PNG) inte får kodaren att krascha.

from PIL import Image

def make_thumbnail(src: str, dst: str, box=(256, 256)) -> None:
    with Image.open(src) as img:
        img = img.convert("RGB")
        img.thumbnail(box, Image.LANCZOS)
        img.save(dst, "JPEG", quality=85, optimize=True)

if __name__ == "__main__":
    print("thumbnail helper ready")

En Celery-workeruppgift

Varje omvandling blir en Celery-uppgift. Uppgiften är en vanlig funktion dekorerad med @app.task; kön hanterar nya försök, bekräftelser och samtidighet.

  • Skapa flera storlekar i samma uppgift för att fördela kostnaden för bildavkodningen
  • Uppdatera jobbstatusen när uppgiften är klar, så att API:t kan rapportera förloppet
  • Ange autoretry_for så att tillfälliga I/O-fel automatiskt försöks igen
from celery import Celery
from PIL import Image

celery_app = Celery("media", broker="redis://localhost:6379/0")

SIZES = {"thumb": (256, 256), "medium": (1024, 1024)}

@celery_app.task(autoretry_for=(OSError,), retry_backoff=True, max_retries=3)
def process_image(job_id: str, src: str) -> dict:
    outputs = {}
    with Image.open(src) as base:
        base = base.convert("RGB")
        for name, box in SIZES.items():
            img = base.copy()
            img.thumbnail(box, Image.LANCZOS)
            dst = f"/data/derived/{job_id}_{name}.jpg"
            img.save(dst, "JPEG", quality=85, optimize=True)
            outputs[name] = dst
    return {"job_id": job_id, "outputs": outputs}

Formatkonvertering: PNG och WebP

Att leverera WebP i stället för JPEG/PNG minskar nyttolastens storlek med 25–35 % vid liknande kvalitet, vilket minskar bandbreddsanvändningen och snabbar upp sidinläsningen.

Pillow konverterar genom att ni helt enkelt väljer utdataformatet i save(). Behåll även en kopia i originalformat, eftersom vissa äldre klienter inte kan avkoda WebP. Exemplet nedan skapar både en JPEG och en WebP från samma avkodning.

from PIL import Image

def to_jpeg_and_webp(src: str, stem: str) -> dict:
    with Image.open(src) as img:
        rgb = img.convert("RGB")
        jpeg_path = f"{stem}.jpg"
        webp_path = f"{stem}.webp"
        rgb.save(jpeg_path, "JPEG", quality=85, optimize=True)
        rgb.save(webp_path, "WEBP", quality=80, method=6)
    return {"jpeg": jpeg_path, "webp": webp_path}

if __name__ == "__main__":
    print(to_jpeg_and_webp.__name__)

Spåra jobbstatus

Klienterna behöver veta när deras härledda filer är klara. Lagra en liten statuspost (i Redis eller er databas) med nyckeln job_id och exponera en ändpunkt för avfrågning.

Livscykelstatusar är vanligtvis queued -> processing -> done eller failed. Workern uppdaterar posten när uppgiften startar och avslutas; API:t läser bara posten.

import json, redis

r = redis.Redis()

def set_status(job_id: str, state: str, **extra) -> None:
    payload = {"state": state, **extra}
    r.set(f"job:{job_id}", json.dumps(payload), ex=86400)

def get_status(job_id: str) -> dict | None:
    raw = r.get(f"job:{job_id}")
    return json.loads(raw) if raw else None

Avfrågningsändpunkt och resultat-URL:er

Statusändpunkten returnerar det aktuella tillståndet och, när arbetet är klart, URL:erna till de genererade resurserna. Returnera 404 för ett okänt jobb och 200 med tillståndet i övriga fall.

En vanlig vidareutveckling är att returnera en försignerad S3-URL för varje härledd fil, så att klienten hämtar den direkt från objektlagringen i stället för via ert API.

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/images/{job_id}")
async def image_status(job_id: str):
    status = get_status(job_id)
    if status is None:
        raise HTTPException(status_code=404, detail="job not found")
    return {"job_id": job_id, **status}

Håll händelseslingan oblockerad

Även utanför en worker kan ni ibland behöva anropa ett blockerande bibliotek (Pillow eller ett PDF-verktyg) från en asynkron ändpunkt. Om ni anropar det direkt blockeras händelseslingan och alla samtidiga begäranden stannar upp.

Flytta körningen till en trådpool med asyncio.to_thread (eller Starlette:s run_in_threadpool). För CPU-bundna batchjobb över flera kärnor undviker en ProcessPoolExecutor GIL. Det körbara exemplet visar mönstret för att flytta arbetet till en tråd.

import asyncio, time

def blocking_resize(n: int) -> int:
    time.sleep(0.1)  # stand-in for Pillow work
    return n * n

async def handle(n: int) -> int:
    # runs blocking_resize in a worker thread, loop stays free
    return await asyncio.to_thread(blocking_resize, n)

async def main() -> None:
    results = await asyncio.gather(*(handle(i) for i in range(5)))
    print(results)

if __name__ == "__main__":
    asyncio.run(main())

Konvertera dokument till PDF/bilder

Dokumentomvandlingar (DOCX till PDF, PDF-sida till PNG-miniatyr) anropar vanligtvis externa verktyg som LibreOffice (soffice --headless) eller pdftoppm via skalet. Dessa är tunga och långsamma, så de hör hemma i en workeruppgift, aldrig i begärandevägen.

Kör dem alltid med en tidsgräns och fånga fel, eftersom externa konverterare kan fastna vid felaktiga indata.

import subprocess

def docx_to_pdf(src: str, out_dir: str) -> str:
    subprocess.run(
        ["soffice", "--headless", "--convert-to", "pdf",
         "--outdir", out_dir, src],
        check=True, timeout=120,
    )
    return out_dir

@celery_app.task(autoretry_for=(subprocess.TimeoutExpired,), max_retries=2)
def convert_document(job_id: str, src: str) -> dict:
    out = docx_to_pdf(src, "/data/derived")
    set_status(job_id, "done", out_dir=out)
    return {"job_id": job_id, "out_dir": out}

Validering, begränsningar och rensning

Otillförlitliga medier innebär en säkerhetsrisk. Skydda bearbetningskedjan innan något tungt arbete körs:

  • Verifiera typen utifrån innehållet (till exempel med Image.open().verify() eller magiska byte), inte bara filändelsen
  • Begränsa dimensionerna för att oskadliggöra bilder som kan orsaka dekompressionsattacker; ange Image.MAX_IMAGE_PIXELS
  • Tillämpa storleksgränser medan uppladdningen strömmas
  • Rensa bort original och härledda filer vid fel eller efter en TTL

Avvisa felaktiga indata tidigt, så att en skadlig fil aldrig når workern.

from PIL import Image, UnidentifiedImageError

Image.MAX_IMAGE_PIXELS = 50_000_000  # guard against decompression bombs

def is_safe_image(path: str) -> bool:
    try:
        with Image.open(path) as img:
            img.verify()  # checks integrity without full decode
        return True
    except (UnidentifiedImageError, OSError):
        return False

Snabb kontroll

Testa er förståelse av var tungt mediearbete hör hemma.

Sammanfattning

Ni har lärt er hur ni håller medietunga FastAPI-ändpunkter snabba:

  • Ta emot snabbt, bearbeta senare: strömma uppladdningen till lagringen, returnera 202 med ett jobb-id och lägg arbetet i kön
  • Använd en riktig kö (Celery/RQ/arq + Redis) för CPU-tunga omvandlingar; använd BackgroundTasks för billiga efterföljande åtgärder
  • Omvandla med Pillow: thumbnail() för storleksändring med bevarat bildförhållande, convert("RGB") före JPEG och WebP för mindre nyttolaster
  • Dokumentkonvertering anropar verktyg som LibreOffice via skalet med en tidsgräns, alltid i en worker
  • Blockera aldrig slingan: flytta enstaka blockerande anrop med asyncio.to_thread
  • Skydda indata: verifiera typen, begränsa antalet pixlar och storleken samt rensa bort härledda filer
Gratis att börja

Lär dig Bootcamp i backendutveckling med FastAPI med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
21
Lektioner
84

Vanliga frågor

Är lektionen ”Asynkron bild- och dokumenttransformering” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Bootcamp i backendutveckling med FastAPI, inklusive ”Asynkron bild- och dokumenttransformering”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Bootcamp i backendutveckling med FastAPI innehåller totalt 4 lektioner.

Vad lär jag mig i ”Asynkron bild- och dokumenttransformering”?

Bearbeta miniatyrbilder, storleksändringar och formatkonvertering i bakgrundsworkers för att hålla request-latensen låg. Ni övar på Bootcamp i backendutveckling med FastAPI med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Bootcamp i backendutveckling med FastAPI?

Du behöver inga förkunskaper. Utbildningen i Bootcamp i backendutveckling med FastAPI på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 4 av 4.

Hur lång tid tar lektionen ”Asynkron bild- och dokumenttransformering”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Bootcamp i backendutveckling med FastAPI-lektionen?

Ja. Varje Bootcamp i backendutveckling med FastAPI-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Multipart-uppladdningar och innehållsvalidering
  2. Strömmande svar och range requests
  3. Avlasta lagring till S3-kompatibla buckets
  4. Asynkron bild- och dokumenttransformering
← Tillbaka till Bootcamp i backendutveckling med FastAPI