FastAPI 表单数据

FastAPI 表单数据(Form Data)完全指南


一、什么是表单数据?

表单数据 = application/x-www-form-urlencodedmultipart/form-data
常见于:

  • 登录表单
  • 搜索框
  • 文件上传 + 文本字段

FastAPI 使用 Form()UploadFile 处理。


二、安装依赖

pip install "fastapi[all]"
# 包含 python-multipart(处理表单和文件)

三、基本表单:Form()

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login/")
def login(username: str = Form(...), password: str = Form(...)):
    return {"username": username}

... 表示必填字段


四、完整登录表单示例

from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel

app = FastAPI()

# 模拟用户数据库
fake_users_db = {
    "alice": {"username": "alice", "password": "wonderland"},
    "bob": {"username": "bob", "password": "builder"}
}

# HTML 登录页面
@app.get("/", response_class=HTMLResponse)
def login_page():
    return """
    <html>
        <head><title>登录</title></head>
        <body>
            <h2>用户登录</h2>
            <form method="post" action="/login/">
                <p>
                    <label>用户名: <input type="text" name="username" required /></label>
                </p>
                <p>
                    <label>密码: <input type="password" name="password" required /></label>
                </p>
                <p>
                    <button type="submit">登录</button>
                </p>
            </form>
        </body>
    </html>
    """

# 登录处理
@app.post("/login/")
def login(username: str = Form(...), password: str = Form(...)):
    user = fake_users_db.get(username)
    if not user or user["password"] != password:
        raise HTTPException(status_code=400, detail="用户名或密码错误")

    return {"message": f"欢迎, {username}!", "token": "fake-jwt-token"}

五、表单 + 文件上传(混合)

from fastapi import File, UploadFile, Form
from typing import List

@app.post("/upload/")
def upload_file(
    file: UploadFile = File(...),
    description: str = Form(...),
    tags: List[str] = Form([])
):
    return {
        "filename": file.filename,
        "description": description,
        "tags": tags
    }

HTML 表单示例:

<form method="post" action="/upload/" enctype="multipart/form-data">
    <p>
        <label>文件: <input type="file" name="file" required /></label>
    </p>
    <p>
        <label>描述: <input type="text" name="description" /></label>
    </p>
    <p>
        <label>标签: <input type="text" name="tags" /></label> 
        <small>多个标签用逗号分隔</small>
    </p>
    <button type="submit">上传</button>
</form>

注意:enctype="multipart/form-data" 必须有!


六、多个文件 + 表单字段

@app.post("/upload-multiple/")
def upload_files(
    files: List[UploadFile] = File(...),
    category: str = Form(...),
    public: bool = Form(False)
):
    return {
        "filenames": [f.filename for f in files],
        "category": category,
        "public": public
    }

七、Pydantic 模型 + 表单(推荐)

from pydantic import BaseModel, EmailStr, Field
from typing import List

class UploadForm(BaseModel):
    title: str = Field(..., min_length=1, max_length=100)
    email: EmailStr
    tags: List[str] = []

@app.post("/submit-form/")
def submit_form(
    form_data: UploadForm = Form(...),
    file: UploadFile = File(...)
):
    return {
        "title": form_data.title,
        "email": form_data.email,
        "tags": form_data.tags,
        "filename": file.filename
    }

注意:Form(...) 替代 Body(...)


八、保存上传文件到磁盘

import shutil
import os

UPLOAD_DIR = "./uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/save-file/")
async def save_file(
    file: UploadFile = File(...),
    note: str = Form("")
):
    file_path = f"{UPLOAD_DIR}/{file.filename}"

    with open(file_path, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    return {
        "saved": file_path,
        "note": note,
        "size": os.path.getsize(file_path)
    }

九、文件类型限制

from fastapi import HTTPException

ALLOWED_TYPES = {"image/png", "image/jpeg", "application/pdf"}

@app.post("/upload-image/")
async def upload_image(file: UploadFile = File(...)):
    if file.content_type not in ALLOWED_TYPES:
        raise HTTPException(
            status_code=400,
            detail=f"不支持的文件类型: {file.content_type}"
        )

    # 保存文件...
    return {"filename": file.filename}

十、文件大小限制

from fastapi import Request

@app.middleware("http")
async def limit_upload_size(request: Request, call_next):
    if request.method == "POST":
        if "multipart/form-data" in request.headers.get("content-type", ""):
            # 限制 10MB
            if int(request.headers.get("content-length", 0)) > 10 * 1024 * 1024:
                return JSONResponse(
                    status_code=413,
                    content={"detail": "文件太大,最大 10MB"}
                )
    return await call_next(request)

十一、表单验证错误(422)

class LoginForm(BaseModel):
    username: str = Field(..., min_length=3)
    password: str = Field(..., min_length=6)

@app.post("/login-model/")
def login(form: LoginForm = Form(...)):
    return form

错误响应:

{
  "detail": [
    {
      "loc": ["body", "username"],
      "msg": "ensure this value has at least 3 characters",
      "type": "value_error.any_str.min_length"
    }
  ]
}

十二、完整实战:用户注册(表单 + 头像上传)

from fastapi import FastAPI, Form, File, UploadFile, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel, EmailStr, Field
import shutil
import os

app = FastAPI()
UPLOAD_DIR = "./avatars"
os.makedirs(UPLOAD_DIR, exist_ok=True)

class RegisterForm(BaseModel):
    username: str = Field(..., min_length=3, max_length=20)
    email: EmailStr
    password: str = Field(..., min_length=8)
    bio: str = Field("", max_length=500)

@app.get("/register/", response_class=HTMLResponse)
def register_page():
    return """
    <form method="post" action="/register/" enctype="multipart/form-data">
        <p><label>用户名: <input name="username" required /></label></p>
        <p><label>邮箱: <input type="email" name="email" required /></label></p>
        <p><label>密码: <input type="password" name="password" required /></label></p>
        <p><label>简介: <textarea name="bio"></textarea></label></p>
        <p><label>头像: <input type="file" name="avatar" accept="image/*" /></label></p>
        <button type="submit">注册</button>
    </form>
    """

@app.post("/register/")
async def register(
    username: str = Form(...),
    email: EmailStr = Form(...),
    password: str = Form(...),
    bio: str = Form(""),
    avatar: UploadFile = File(None)
):
    # 模拟验证
    if len(password) < 8:
        raise HTTPException(400, "密码至少8位")

    # 保存头像
    avatar_url = None
    if avatar and avatar.filename:
        ext = avatar.filename.split(".")[-1]
        avatar_path = f"{UPLOAD_DIR}/{username}.{ext}"
        with open(avatar_path, "wb") as f:
            shutil.copyfileobj(avatar.file, f)
        avatar_url = f"/avatars/{username}.{ext}"

    return {
        "message": "注册成功",
        "username": username,
        "email": email,
        "avatar": avatar_url
    }

# 提供头像静态文件
from fastapi.staticfiles import StaticFiles
app.mount("/avatars", StaticFiles(directory=UPLOAD_DIR), name="avatars")

十三、cURL 测试表单

# 登录
curl -X POST "http://127.0.0.1:8000/login/" \
     -d "username=alice" \
     -d "password=wonderland"

# 上传文件 + 表单
curl -X POST "http://127.0.0.1:8000/upload/" \
     -F "file=@photo.jpg" \
     -F "description=我的头像" \
     -F "tags=portrait,selfie"

总结:表单数据速查表

场景写法
文本字段username: str = Form(...)
可选字段note: str = Form(None)
多选/数组tags: List[str] = Form([])
文件上传file: UploadFile = File(...)
多文件files: List[UploadFile] = File(...)
Pydantic + Formform: MyForm = Form(...)
HTML 表单enctype="multipart/form-data"
保存文件shutil.copyfileobj()
静态文件app.mount("/static", StaticFiles(...))

下一步学习

主题关键词
OAuth2 登录表单oauth2
文件分片上传chunk
表单 + JWT 认证jwt form
富文本编辑器上传tinymce
表单验证前端zod

现在就运行你的表单,打开浏览器测试登录和上传吧!

需要我生成一个 完整用户中心模板(注册、登录、头像上传、资料修改)吗?
回复 表单模板 立刻获取!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注