Django 表单处理与数据验证

小飞兽 Django 7 次阅读 2026-07-29

Django 的表单系统(Forms)是处理用户输入的核心模块,从简单的文本输入到复杂的文件上传,Django forms 提供了完整的解决方案。本文深入讲解 Django 表单的使用方法。

Django Forms vs ModelForms

    • Form:独立表单,不依赖模型,适合与模型无关的表单(如搜索框、联系表单)
    • ModelForm:与模型绑定的表单,可自动生成字段并支持 save(),适合增删改查操作

    创建表单

    # blog/forms.py
    from django import forms
    from django.core.validators import MinLengthValidator, EmailValidator, URLValidator
    from .models import Article, Comment
    

    class SearchForm(forms.Form):
    """独立搜索表单(不依赖模型)"""
    q = forms.CharField(
    label="搜索",
    max_length=100,
    widget=forms.TextInput(attrs={
    "placeholder": "输入关键词搜索...",
    "class": "search-input"
    })
    )
    category = forms.ChoiceField(
    label="分类",
    required=False,
    choices=[("", "全部分类"), ("python", "Python"), ("django", "Django")]
    )
    sort = forms.ChoiceField(
    label="排序",
    required=False,
    initial="latest",
    choices=[("latest", "最新"), ("popular", "最热")]
    )

    class ArticleForm(forms.ModelForm):
    """模型表单:自动根据模型生成字段"""
    class Meta:
    model = Article
    fields = ["title", "slug", "content", "category", "tags", "is_published"]
    widgets = {
    "title": forms.TextInput(attrs={"class": "form-control"}),
    "content": forms.Textarea(attrs={"class": "form-control", "rows": 10}),
    "slug": forms.TextInput(attrs={"class": "form-control"}),
    }

    def clean_slug(self):
    """自定义字段验证:slug 唯一性检查"""
    slug = self.cleaned_data.get("slug")
    if Article.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
    raise forms.ValidationError("该 slug 已存在")
    return slug

    def clean_title(self):
    """自定义字段验证:标题长度与内容检查"""
    title = self.cleaned_data.get("title")
    if len(title) < 5:
    raise forms.ValidationError("标题长度不能少于 5 个字符")
    if any(word in title for word in ["spam", "广告"]):
    raise forms.ValidationError("标题包含敏感词")
    return title

    class CommentForm(forms.ModelForm):
    """评论表单"""
    class Meta:
    model = Comment
    fields = ["author_name", "email", "content"]
    widgets = {
    "content": forms.Textarea(attrs={"rows": 4, "placeholder": "写下你的评论..."}),
    }

    def clean_email(self):
    email = self.cleaned_data.get("email")
    if email and not email.endswith("@example.com"):
    raise forms.ValidationError("仅允许 example.com 域名的邮箱")
    return email

    视图中处理表单

    # blog/views.py
    from django.shortcuts import render, redirect, get_object_or_404
    from django.contrib import messages
    from django.core.paginator import Paginator
    from .forms import SearchForm, ArticleForm, CommentForm
    from .models import Article, Comment

    def article_create(request):
    """创建文章"""
    if request.method == "POST":
    form = ArticleForm(request.POST, request.FILES)
    if form.is_valid():
    article = form.save(commit=False)
    article.author = request.user
    article.save()
    form.save_m2m() # 保存多对多关系
    messages.success(request, "文章创建成功!")
    return redirect("article_detail", slug=article.slug)
    else:
    messages.error(request, "表单验证失败,请检查输入。")
    else:
    form = ArticleForm()
    return render(request, "blog/article_form.html", {"form": form, "action": "create"})

    def article_edit(request, slug):
    """编辑文章"""
    article = get_object_or_404(Article, slug=slug)
    if request.method == "POST":
    form = ArticleForm(request.POST, request.FILES, instance=article)
    if form.is_valid():
    form.save()
    form.save_m2m()
    messages.success(request, "文章更新成功!")
    return redirect("article_detail", slug=article.slug)
    else:
    form = ArticleForm(instance=article)
    return render(request, "blog/article_form.html", {
    "form": form, "article": article, "action": "edit"
    })

    def add_comment(request, article_id):
    """添加评论"""
    article = get_object_or_404(Article, id=article_id)
    if request.method == "POST":
    form = CommentForm(request.POST)
    if form.is_valid():
    comment = form.save(commit=False)
    comment.article = article
    comment.save()
    messages.success(request, "评论已提交!")
    else:
    messages.error(request, str(form.errors))
    return redirect("article_detail", slug=article.slug)

    def search(request):
    """搜索视图"""
    form = SearchForm(request.GET)
    articles = Article.objects.filter(is_published=True)
    if form.is_valid():
    q = form.cleaned_data.get("q")
    category = form.cleaned_data.get("category")
    if q:
    articles = articles.filter(title__icontains=q)
    if category:
    articles = articles.filter(category__slug=category)
    if form.cleaned_data.get("sort") == "popular":
    articles = articles.order_by("-views")
    else:
    articles = articles.order_by("-created_at")
    paginator = Paginator(articles, 20)
    page_obj = paginator.get_page(request.GET.get("page"))
    return render(request, "blog/search.html", {
    "form": form, "page_obj": page_obj
    })

    模板中使用表单

    {# blog/article_form.html #}

    {% csrf_token %}

    {# 非字段错误 #}
    {% if form.non_field_errors %}

    {{ form.non_field_errors }}

    {% endif %}

    {# 逐字段渲染 #}


    {{ form.title.label_tag }}
    {{ form.title }}
    {% if form.title.errors %}
    {{ form.title.errors.0 }}
    {% endif %}

    {{ form.slug.label_tag }} {{ form.slug }} {% for error in form.slug.errors %} {{ error }} {% endfor %}
    {{ form.content.label_tag }} {{ form.content }}
    {{ form.category.label_tag }} {{ form.category }}


    {# 或者使用 as_p / as_table / as_ul 快速渲染 #}


    {% csrf_token %}
    {{ form.as_p }}

    表单字段与验证器

    from django import forms
    from django.core.validators import MinLengthValidator, RegexValidator

    class ValidatedForm(forms.Form):
    # CharField 验证
    username = forms.CharField(
    min_length=3,
    max_length=20,
    validators=[RegexValidator(r"^[a-zA-Z0-9_]+$", message="仅允许字母数字下划线")]
    )
    # EmailField
    email = forms.EmailField(required=False)
    # URLField
    website = forms.URLField(required=False, validators=[URLValidator()])
    # IntegerField / FloatField
    age = forms.IntegerField(min_value=0, max_value=150)
    price = forms.FloatField(min_value=0.01)
    # DateField / DateTimeField
    birth_date = forms.DateField(input_formats=["%Y-%m-%d", "%d/%m/%Y"])
    # ChoiceField
    gender = forms.ChoiceField(choices=[("M", "男"), ("F", "女")], widget=forms.RadioSelect)
    # MultipleChoiceField
    hobbies = forms.MultipleChoiceField(
    choices=[("reading", "阅读"), ("coding", "编程"), ("gaming", "游戏")],
    widget=forms.CheckboxSelectMultiple
    )
    # FileField / ImageField
    avatar = forms.ImageField(required=False)
    # RegexField
    phone = forms.RegexField(regex=r"^1[3-9]d{9}$", error_message="手机号格式不正确")

    常见问题

    • Q: form.is_valid() 返回 True 但某些字段值为空?
      A:is_valid() 会运行所有字段级验证,但未传入的字段不会进入 cleaned_data。对于可选字段,检查 cleaned_data 中是否存在该键:if "email" in form.cleaned_data。
    • Q: 如何处理文件上传?
      A:表单需设置 enctype="multipart/form-data",视图需接收 request.FILES,ImageField 会自动验证文件类型。
    • Q: ModelForm 的 save(commit=False) 有什么用?
      A:commit=False 返回模型实例但不保存到数据库,适合在保存前修改字段(如设置 author);之后需手动 save() 或 save_m2m()(有多对多时)。
    • Q: 如何自定义表单错误消息?
      A:在字段定义时通过 error_messages 参数或在 clean_xxx() 方法中 raise forms.ValidationError("自定义消息")。

    延伸阅读

  • Django 表单字段参考
  • Django CSRF 保护机制详解与常见误区