Bootcamp i backendutvikling med FastAPI · leksjon

Asynkron bilde- og dokumenttransformasjon

Behandle miniatyrbilder, skalering og formatkonvertering i bakgrunnsarbeidere for å holde ventetiden for forespørsler lav.

Leksjon 4 av 413 trinn

Asynkron bilde- og dokumenttransformasjon er en gratis leksjon i Bootcamp i backendutvikling med FastAPI på CoddyKit. Dette er leksjon 4 av 4. Du kan lese valgfritt 3 leksjoner fra denne læringsstien gratis i sin helhet – deretter låser CoddyKit PRO opp alle leksjoner, samt praktisk øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Bootcamp i backendutvikling med FastAPI, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Bootcamp i backendutvikling med FastAPI inneholder totalt 4 leksjoner.

Hvorfor flytte mediebehandlingen

Å endre størrelsen på et bilde eller konvertere en PDF kan ta fra flere hundre millisekunder til flere sekunder. Hvis du gjør dette arbeidet inne i forespørselshåndtereren, må klienten vente, og worker-prosessen din blir blokkert.

Mønsteret for FastAPI-tjenester på B2-nivå er:

  • Ta imot opplastingen og lagre originalen raskt
  • Returner 202 Accepted med en jobb-ID
  • Lag miniatyrbilder, endre størrelse og konverter format i en bakgrunnsarbeider

Dette holder ventetiden for forespørsler lav og gjør det mulig å skalere tungt CPU-arbeid uavhengig.

BackgroundTasks kontra en ekte kø

FastAPI leveres med BackgroundTasks, som kjører en funksjon etter at svaret er sendt, men fortsatt i den samme prosessen. Det passer fint til billige og raske oppfølgingsoppgaver (sende en e-post, skrive en logg).

For CPU-tunge medietransformasjoner er dette feil verktøy: Det konkurrerer med hendelsesløkken og avsluttes hvis prosessen starter på nytt. Foretrekk en dedikert oppgavekø (Celery, RQ, Dramatiq eller arq) støttet av Redis, slik at arbeidet overlever utrullinger og kan skaleres horisontalt.

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 imot raskt, behandle senere

Endepunktet bør gjøre minst mulig: validere filen, strømme den til lagring, opprette en jobboppføring og legge en oppgave i kø. Legg merke til at vi leser opplastingen i deler, slik at en stor fil aldri lastes helt inn i minnet.

  • await file.read(chunk) unngår store plutselige minnetopper
  • Vi returnerer en job_id som klienten kan spørre
  • Selve transformasjonen skjer 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"}

Generere miniatyrbilder med Pillow

Pillows Image.thumbnail() endrer størrelsen på stedet samtidig som sideforholdet bevares, og bildet skaleres aldri opp. Dette er riktig grunnfunksjon for miniatyrbilder fordi resultatet får plass innenfor boksen du angir.

Bruk Image.LANCZOS-resampling for skarp nedskalering, og kall img.convert("RGB") før du lagrer som JPEG, slik at bilder med alfakanaler (PNG) ikke får koderen til å krasje.

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-workeroppgave

Hver transformasjon blir en Celery-oppgave. Oppgaven er en vanlig funksjon dekorert med @app.task; køen håndterer nye forsøk, kvitteringer og samtidighet.

  • Generer flere størrelser i én oppgave for å fordele kostnaden ved bildeavkodingen
  • Oppdater jobbstatusen når du er ferdig, slik at API-et kan rapportere fremdrift
  • Angi autoretry_for slik at midlertidige I/O-feil prøves på nytt automatisk
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 og WebP

Ved å levere WebP i stedet for JPEG/PNG reduseres nyttelastens størrelse med 25–35 % med tilsvarende kvalitet. Det reduserer båndbreddebruken og gjør at sider lastes raskere.

Pillow konverterer ganske enkelt ved at du velger utdataformatet i save(). Behold også en kopi i originalformatet, siden enkelte eldre klienter ikke kan dekode WebP. Eksempelet nedenfor lager både en JPEG- og en WebP-fil fra én avkoding.

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__)

Følge jobbstatus

Klienter må vite når de avledede filene er klare. Lagre en liten statusoppføring (i Redis eller databasen) med job_id som nøkkel, og eksponer et endepunkt for forespørsler.

Livssyklustilstander er vanligvis queued -> processing -> done eller failed. Workeren oppdaterer oppføringen når oppgaven starter og avsluttes; API-et leser den bare.

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

Endepunkt for forespørsler og resultatadresser

Statusendepunktet returnerer den nåværende tilstanden og, når jobben er ferdig, adressene til de genererte ressursene. Returner 404 for en ukjent jobb og 200 med tilstanden ellers.

En vanlig forbedring er å returnere en forhåndssignert S3-adresse for hver avledede fil, slik at klienten laster ned direkte fra objektlagringen i stedet for via API-et.

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}

Holde hendelsesløkken ublokkert

Selv utenfor en worker må du noen ganger kalle et blokkerende bibliotek (Pillow, et PDF-verktøy) fra et asynkront endepunkt. Hvis du kaller det direkte, blokkerer du hendelsesløkken og stopper alle samtidige forespørsler.

Flytt det til en trådpool med asyncio.to_thread (eller Starlettes run_in_threadpool). For CPU-bundne batchjobber på tvers av kjerner unngår en ProcessPoolExecutor GIL. Det kjørbare eksempelet viser mønsteret for å flytte arbeid til 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())

Konvertere dokumenter til PDF/bilder

Dokumenttransformasjoner (DOCX til PDF, PDF-side til PNG-miniatyrbilde) starter vanligvis eksterne verktøy som LibreOffice (soffice --headless) eller pdftoppm gjennom skallet. Disse er tunge og langsomme, så de hører hjemme i en worker-oppgave, aldri i forespørselsbanen.

Kjør dem alltid med en tidsavbruddsgrense og fang opp feil, fordi eksterne konverterere kan henge ved ugyldige inndata.

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, begrensninger og opprydding

Ikke-klarerte medier utgjør en sikkerhetsrisiko. Beskytt behandlingsforløpet før tungt arbeid kjøres:

  • Bekreft typen basert på innholdet (for eksempel Image.open().verify() eller magiske byte), ikke bare filendelsen
  • Begrens dimensjonene for å avverge bilder som utnytter dekompresjonsbomber; angi Image.MAX_IMAGE_PIXELS
  • Håndhev størrelsesgrenser mens opplastingen strømmes
  • Rydd opp i originaler og avledede filer ved feil eller etter en TTL

Avvis ugyldige inndata tidlig, slik at en ondsinnet fil aldri når workeren.

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

Kunnskapssjekk

Test forståelsen din av hvor tungt mediearbeid hører hjemme.

Oppsummering

Du har lært hvordan du holder medietunge FastAPI-endepunkter raske:

  • Ta imot raskt, behandle senere: strøm opplastingen til lagring, returner 202 med en jobb-ID og legg arbeidet i kø
  • Bruk en ekte kø (Celery/RQ/arq + Redis) for CPU-tunge transformasjoner; reserver BackgroundTasks for billige oppfølgingsoppgaver
  • Transformer med Pillow: thumbnail() for endring av størrelse med bevart sideforhold, convert("RGB") før JPEG og WebP for mindre nyttelaster
  • Dokumentkonvertering starter verktøy som LibreOffice gjennom skallet, med en tidsavbruddsgrense, og alltid i en worker
  • Blokker aldri løkken: flytt enkeltstående blokkerende kall med asyncio.to_thread
  • Sikre inndata: bekreft typen, begrens pikselantallet, begrens størrelsen og rydd opp i avledede filer
Gratis å komme i gang

Lær deg Bootcamp i backendutvikling med FastAPI med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
21
Leksjoner
84

Ofte stilte spørsmål

Er leksjonen «Asynkron bilde- og dokumenttransformasjon» gratis?

Ja – du kan lese valgfritt 3 av leksjonene i læringsstien Bootcamp i backendutvikling med FastAPI, inkludert «Asynkron bilde- og dokumenttransformasjon», gratis i sin helhet her på nettet. Deretter låser CoddyKit PRO opp alle leksjoner, samt interaktiv øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Kurset i Bootcamp i backendutvikling med FastAPI inneholder totalt 4 leksjoner.

Hva lærer jeg i «Asynkron bilde- og dokumenttransformasjon»?

Behandle miniatyrbilder, skalering og formatkonvertering i bakgrunnsarbeidere for å holde ventetiden for forespørsler lav. Du øver på Bootcamp i backendutvikling med FastAPI med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Bootcamp i backendutvikling med FastAPI?

Ingen tidligere erfaring er nødvendig. Bootcamp i backendutvikling med FastAPI på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 4 av 4.

Hvor lang tid tar leksjonen «Asynkron bilde- og dokumenttransformasjon»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Bootcamp i backendutvikling med FastAPI-leksjonen?

Ja. Alle Bootcamp i backendutvikling med FastAPI-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Multipart-opplastinger og innholdsvalidering
  2. Strømmende svar og range-forespørsler
  3. Avlasting av lagring til S3-kompatible bøtter
  4. Asynkron bilde- og dokumenttransformasjon
← Tilbake til Bootcamp i backendutvikling med FastAPI