Python
表单验证与文件上传
发布于 2026年7月22日
表单验证与文件上传
Django Form 不只是生成 HTML,它更重要的职责是把不可信的用户输入转换为经过校验的 Python 数据。本篇将完成普通表单、ModelForm、自定义校验和文件上传。
一、Form 的处理流程
一个标准表单请求有两条路径:
- GET:创建空表单并展示。
- POST:绑定请求数据,校验成功后执行业务并重定向;失败则带错误重新渲染。
def message_create(request):
if request.method == "POST":
form = MessageForm(request.POST, request.FILES)
if form.is_valid():
message = form.save()
return redirect("notes:detail", pk=message.pk)
else:
form = MessageForm()
return render(request, "notes/message_form.html", {"form": form})
POST 成功后重定向是 PRG 模式(Post/Redirect/Get),可以避免用户刷新页面导致重复提交。
二、普通 Form
普通 Form 适合搜索、登录、筛选或不直接对应模型的数据。
from django import forms
class MessageSearchForm(forms.Form):
q = forms.CharField(label="关键词", max_length=100, required=False)
status = forms.ChoiceField(
label="状态",
required=False,
choices=[
("", "全部"),
("draft", "草稿"),
("published", "已发布"),
("archived", "已归档"),
],
)
form = MessageSearchForm(request.GET)
if form.is_valid():
keyword = form.cleaned_data["q"]
只有 is_valid() 成功后才能可靠使用 cleaned_data。
三、ModelForm
from django import forms
from .models import Message
class MessageForm(forms.ModelForm):
class Meta:
model = Message
fields = ["name", "content", "status", "tags"]
widgets = {
"content": forms.Textarea(attrs={"rows": 6}),
}
help_texts = {
"content": "请填写 10~1000 个字符。",
}
应显式列出 fields,避免模型新增敏感字段后意外暴露在表单中。
编辑对象时传入 instance:
form = MessageForm(request.POST or None, instance=message)
四、字段级校验
方法名格式为 clean_字段名:
class MessageForm(forms.ModelForm):
class Meta:
model = Message
fields = ["name", "content", "status"]
def clean_content(self):
content = self.cleaned_data["content"].strip()
if len(content) < 10:
raise forms.ValidationError("留言内容至少需要 10 个字符。")
return content
返回值会写回 cleaned_data,因此可以在这里做安全的标准化,例如去除首尾空格。
五、跨字段校验
多个字段之间存在规则时重写 clean():
def clean(self):
cleaned_data = super().clean()
status = cleaned_data.get("status")
content = cleaned_data.get("content", "")
if status == "published" and len(content) < 30:
raise forms.ValidationError("发布内容至少需要 30 个字符。")
return cleaned_data
字段缺失时要使用 get(),因为此前的字段校验可能已经失败。
六、把当前用户写入模型
owner 不应该让普通用户在表单中选择,应由服务端决定:
if form.is_valid():
message = form.save(commit=False)
message.owner = request.user
message.save()
form.save_m2m()
return redirect("notes:detail", pk=message.pk)
使用 commit=False 后,多对多数据需要在实例保存后执行 form.save_m2m()。
七、模板中渲染表单
<form method="post" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<button type="submit">保存</button>
</form>
- 写操作表单必须带
{% csrf_token %}。 - 有文件字段时必须使用
enctype="multipart/form-data"。 novalidate可关闭浏览器原生提示,便于统一展示服务端错误;是否使用由项目体验决定。
八、文件上传
模型:
def attachment_path(instance, filename):
return f"attachments/{instance.message_id}/{filename}"
class Attachment(models.Model):
message = models.ForeignKey(Message, on_delete=models.CASCADE)
file = models.FileField(upload_to=attachment_path)
表单校验大小和扩展名:
from pathlib import Path
class AttachmentForm(forms.ModelForm):
class Meta:
model = Attachment
fields = ["file"]
def clean_file(self):
uploaded = self.cleaned_data["file"]
if uploaded.size > 5 * 1024 * 1024:
raise forms.ValidationError("文件不能超过 5 MB。")
allowed = {".pdf", ".png", ".jpg", ".jpeg"}
if Path(uploaded.name).suffix.lower() not in allowed:
raise forms.ValidationError("不支持该文件类型。")
return uploaded
只检查扩展名并不能证明文件内容安全。生产系统还应考虑 MIME 检查、随机文件名、病毒扫描、下载响应头和访问权限。
九、FormSet 简介
一次编辑多条同类数据时可使用 FormSet:
from django.forms import modelformset_factory
MessageFormSet = modelformset_factory(
Message,
fields=["name", "status"],
extra=0,
)
父子模型一起编辑时可以使用 Inline FormSet。它们功能强但复杂,先掌握单表单再使用。
十、常见问题
form.is_valid()总是失败:在模板中输出form.errors查看具体原因。- 文件字段一直为空:检查
request.FILES和表单enctype。 - 编辑时新增了一条记录:创建 ModelForm 时忘记传
instance。 - 多对多没有保存:使用
commit=False后忘记save_m2m()。 - 重复提交:保存成功后直接渲染模板,未采用重定向。
十一、本篇检查清单
- 能区分 Form 与 ModelForm 的使用场景。
- 能编写字段级和跨字段校验。
- 能安全地从
cleaned_data读取数据。 - 能用
commit=False写入当前用户。 - 能正确接收文件并理解上传安全边界。
上一篇:模板、静态文件与媒体文件 | 下一篇:CRUD、分页、搜索与消息框架