Django从零开始(五):表单处理与数据验证
一、Django表单概述
Django表单(Forms)系统是框架的核心组件之一,它负责处理最常见的Web开发任务:接收用户输入、验证数据、清洗数据、显示错误信息。Django表单与HTML表单不同,它是一个Python类,在服务器端处理数据。
Django表单的三大功能
- 渲染HTML:自动生成表单的HTML标签
- 数据验证:服务端验证用户提交的数据
- 数据清洗:将原始输入转换为Python数据类型
二、Form类基本使用
2.1 定义表单类
# blog/forms.py
from django import forms
from .models import Article, Category, Comment
class ContactForm(forms.Form):
"""联系表单(不关联模型)"""
name = forms.CharField(
label='姓名',
max_length=100,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入您的姓名',
})
)
email = forms.EmailField(
label='邮箱',
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': '请输入您的邮箱',
})
)
subject = forms.CharField(
label='主题',
max_length=200,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
message = forms.CharField(
label='留言内容',
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
'placeholder': '请输入留言内容',
})
)
priority = forms.ChoiceField(
label='优先级',
choices=[
('low', '低'),
('medium', '中'),
('high', '高'),
],
initial='medium',
widget=forms.Select(attrs={'class': 'form-control'})
)
attachment = forms.FileField(
label='附件',
required=False,
widget=forms.ClearableFileInput(attrs={'class': 'form-control'})
)
2.2 在视图中使用表单
# blog/views.py
from django.shortcuts import render, redirect
from .forms import ContactForm
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST, request.FILES) # 绑定提交数据
if form.is_valid():
# 获取清洗后的数据
name = form.cleaned_data['name']
email = form.cleaned_data['email']
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
attachment = form.cleaned_data.get('attachment')
# 处理数据(发送邮件等)
send_mail(
subject=f'[联系我们] {subject}',
message=f'来自 {name} ({email}):\n\n{message}',
from_email=email,
recipient_list=['admin@example.com'],
)
return redirect('blog:contact_success')
else:
form = ContactForm() # 未绑定表单(显示空表单)
return render(request, 'blog/contact.html', {'form': form})
2.3 在模板中渲染表单
<!-- blog/templates/blog/contact.html -->
{% extends "base.html" %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{# 方式1:快速渲染整个表单 #}
{{ form.as_p }}
{# 方式2:按字段手动渲染 #}
<div class="form-group">
{{ form.name.label_tag }}
{{ form.name }}
{% if form.name.errors %}
<div class="invalid-feedback">
{% for error in form.name.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
{% if form.name.help_text %}
<small class="form-text text-muted">{{ form.name.help_text }}</small>
{% endif %}
</div>
{# 方式3:完全手动渲染 #}
<div class="form-group">
<label for="{{ form.email.id_for_label }}">邮箱</label>
<input type="email"
name="{{ form.email.html_name }}"
id="{{ form.email.id_for_label }}"
class="form-control {% if form.email.errors %}is-invalid{% endif %}"
value="{{ form.email.value|default:'' }}"
placeholder="请输入邮箱">
{% for error in form.email.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
{% endblock %}
三、ModelForm(模型表单)
ModelForm是Django最常用的表单类型,它直接从模型类自动生成表单字段,大幅减少重复代码。
# blog/forms.py
class ArticleForm(forms.ModelForm):
"""文章模型表单"""
class Meta:
model = Article
fields = ['title', 'category', 'tags', 'content', 'summary', 'status']
# 或者排除某些字段
# exclude = ['author', 'views', 'created_at', 'updated_at']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入文章标题',
}),
'content': forms.Textarea(attrs={
'class': 'form-control markdown-editor',
'rows': 15,
}),
'summary': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': '文章摘要(可选)',
}),
'category': forms.Select(attrs={
'class': 'form-control',
}),
'tags': forms.CheckboxSelectMultiple(),
'status': forms.Select(attrs={
'class': 'form-control',
}),
}
labels = {
'title': '文章标题',
'content': '文章内容',
'category': '所属分类',
'tags': '标签',
'summary': '摘要',
'status': '发布状态',
}
help_texts = {
'title': '标题将自动生成URL别名',
'summary': '留空则自动截取正文前200字',
}
def clean_title(self):
"""自定义标题验证"""
title = self.cleaned_data.get('title', '')
if len(title) < 5:
raise forms.ValidationError('标题至少需要5个字符')
# 检查标题是否重复
if Article.objects.filter(title=title).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError('该标题已存在,请使用其他标题')
return title
def clean(self):
"""全局验证(涉及多个字段)"""
cleaned_data = super().clean()
status = cleaned_data.get('status')
category = cleaned_data.get('category')
content = cleaned_data.get('content', '')
# 已发布文章必须有分类
if status == 'published' and not category:
self.add_error('category', '发布文章必须选择分类')
# 已发布文章内容不少于100字
if status == 'published' and len(content) < 100:
self.add_error('content', '已发布文章内容不少于100字')
return cleaned_data
class CommentForm(forms.ModelForm):
"""评论表单"""
class Meta:
model = Comment
fields = ['nickname', 'email', 'content']
widgets = {
'nickname': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '您的昵称',
}),
'email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': '您的邮箱',
}),
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': '写下您的评论...',
}),
}
四、表单字段类型与验证器
4.1 常用字段类型
# 字符串字段
CharField(max_length=100, min_length=2)
EmailField() # 自动验证邮箱格式
URLField() # 自动验证URL格式
SlugField() # 只允许字母、数字、连字符、下划线
RegexField(regex=r'^\d{11}$') # 正则验证(如手机号)
UUIDField() # UUID格式验证
# 数值字段
IntegerField(min_value=0, max_value=100)
FloatField()
DecimalField(max_digits=10, decimal_places=2)
# 布尔字段
BooleanField(required=True) # 必须勾选(如同意协议)
NullBooleanField() # 可以为None
# 日期时间字段
DateField(widget=forms.DateInput(attrs={'type': 'date'}))
TimeField()
DateTimeField(widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}))
# 选择字段
ChoiceField(choices=[('a', 'A'), ('b', 'B')])
TypedChoiceField(choices=[(1, '一'), (2, '二')], coerce=int)
MultipleChoiceField(choices=[...])
# 文件字段
FileField()
ImageField() # 需要安装Pillow
FilePathField(path='/some/path')
# 隐藏字段
HiddenField()
4.2 内置验证器
from django.core.validators import (
MinValueValidator, MaxValueValidator,
MinLengthValidator, MaxLengthValidator,
RegexValidator, EmailValidator, URLValidator,
)
age = forms.IntegerField(
validators=[
MinValueValidator(0, '年龄不能为负数'),
MaxValueValidator(150, '年龄不能超过150'),
]
)
phone = forms.CharField(
validators=[
RegexValidator(
regex=r'^1[3-9]\d{9}$',
message='请输入有效的手机号',
code='invalid_phone'
)
]
)
password = forms.CharField(
min_length=8,
validators=[
RegexValidator(r'[A-Z]', '密码必须包含大写字母'),
RegexValidator(r'[a-z]', '密码必须包含小写字母'),
RegexValidator(r'\d', '密码必须包含数字'),
RegexValidator(r'[!@#$%^&*]', '密码必须包含特殊字符'),
],
widget=forms.PasswordInput,
help_text='密码至少8位,需包含大小写字母、数字和特殊字符'
)
五、自定义验证
5.1 字段级验证(clean_字段名)
class RegistrationForm(forms.Form):
username = forms.CharField(max_length=50)
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
def clean_username(self):
"""验证用户名是否已存在"""
username = self.cleaned_data.get('username')
if User.objects.filter(username=username).exists():
raise forms.ValidationError('该用户名已被注册')
if username.lower() in ['admin', 'root', 'system']:
raise forms.ValidationError('该用户名不可使用')
return username
def clean_email(self):
"""验证邮箱"""
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise forms.ValidationError('该邮箱已被注册')
# 只允许特定域名
domain = email.split('@')[1]
if domain not in ['gmail.com', 'qq.com', '163.com']:
raise forms.ValidationError('只支持gmail/qq/163邮箱')
return email
5.2 表单级验证(clean)
def clean(self):
"""全局验证"""
cleaned_data = super().clean()
password = cleaned_data.get('password')
confirm_password = cleaned_data.get('confirm_password')
if password and confirm_password and password != confirm_password:
# raise 会同时显示在两个字段上
raise forms.ValidationError('两次输入的密码不一致')
# 或者添加到特定字段
self.add_error('confirm_password', '密码不匹配')
return cleaned_data
5.3 自定义验证器函数
# validators.py
import re
from django.core.exceptions import ValidationError
def validate_chinese(value):
"""验证是否为中文字符"""
if not re.match(r'^[\u4e00-\u9fa5]+$', value):
raise ValidationError('请输入中文字符')
def validate_phone(value):
"""验证中国手机号"""
if not re.match(r'^1[3-9]\d{9}$', value):
raise ValidationError('%(value)s 不是有效的手机号', params={'value': value})
def validate_file_size(value):
"""验证文件大小不超过5MB"""
if value.size > 5 * 1024 * 1024:
raise ValidationError('文件大小不能超过5MB')
def validate_file_extension(value):
"""验证文件扩展名"""
import os
ext = os.path.splitext(value.name)[1].lower()
allowed = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx']
if ext not in allowed:
raise ValidationError(f'不支持的文件类型: {ext}')
# 在表单中使用
class UploadForm(forms.Form):
name = forms.CharField(validators=[validate_chinese])
phone = forms.CharField(validators=[validate_phone])
file = forms.FileField(validators=[validate_file_size, validate_file_extension])
六、表单集(Formsets)
表单集用于一次处理多个表单,常见场景如批量编辑、问卷调查等:
# 创建表单集
from django.forms import formset_factory, modelformset_factory
# 基于Form的表单集
ArticleFormSet = formset_factory(ContactForm, extra=3, max_num=10)
# 基于ModelForm的表单集
ArticleModelFormSet = modelformset_factory(
Article,
fields=['title', 'status'],
extra=0, # 不显示额外空行
can_delete=True, # 允许删除
)
# 在视图中使用
def bulk_edit(request):
if request.method == 'POST':
formset = ArticleModelFormSet(request.POST)
if formset.is_valid():
formset.save()
return redirect('blog:article_list')
else:
formset = ArticleModelFormSet(
queryset=Article.objects.filter(author=request.user)
)
return render(request, 'blog/bulk_edit.html', {'formset': formset})
模板中使用表单集
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<div class="form-row">
{{ form.id }}
{{ form.title }}
{{ form.status }}
{% if form.DELETE %}{{ form.DELETE }}{% endif %}
</div>
{% endfor %}
<button type="submit">保存全部</button>
</form>
七、文件上传处理
# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# forms.py
class AvatarUploadForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['avatar']
def clean_avatar(self):
avatar = self.cleaned_data.get('avatar')
# 验证文件类型
if avatar:
if avatar.content_type not in ['image/jpeg', 'image/png', 'image/gif']:
raise forms.ValidationError('只支持 JPG、PNG、GIF 格式')
# 验证文件大小(2MB)
if avatar.size > 2 * 1024 * 1024:
raise forms.ValidationError('图片大小不能超过 2MB')
# 验证图片尺寸
from PIL import Image
img = Image.open(avatar)
if img.width < 200 or img.height < 200:
raise forms.ValidationError('图片尺寸不能小于 200x200')
return avatar
# views.py
def upload_avatar(request):
if request.method == 'POST':
form = AvatarUploadForm(request.POST, request.FILES, instance=request.user.profile)
if form.is_valid():
form.save()
return redirect('profile')
else:
form = AvatarUploadForm(instance=request.user.profile)
return render(request, 'upload.html', {'form': form})
八、CSRF防护
Django默认开启CSRF防护,POST表单必须包含CSRF令牌:
<!-- 方式1:模板标签 -->
<form method="post">
{% csrf_token %}
...
</form>
<!-- 方式2:AJAX请求 -->
<script>
// 从cookie中获取CSRF令牌
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
fetch('/api/articles/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken,
},
body: JSON.stringify({title: '新文章', content: '内容'}),
});
</script>
九、总结
本章我们学习了Django表单系统的核心知识:
- Form类和ModelForm的定义与使用
- 表单渲染的三种方式
- 字段类型、内置验证器和自定义验证
- 表单集(Formset)批量处理
- 文件上传处理和安全验证
- CSRF防护机制
下一章我们将学习 Django用户认证与权限系统,实现注册、登录、权限控制等核心功能。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。







