Django Forms 表單建立與驗證入門 | Django 教學

2026/06/06 2026/05/26
Django Forms 表單建立與驗證入門 | Django 教學

在 Web 開發中,表單(Form)是使用者與伺服器之間最重要的互動橋梁。Django Forms 系統提供了 資料驗證(Validation)、HTML 渲染(Rendering)與 資料清理(Cleaning)三合一的解決方案,讓開發者不需要手動解析 request.POST,就能安全、有效率地處理使用者輸入。本篇將帶你從零開始認識 Django Forms 的核心概念,包含 Form 類別定義、欄位類型、表單渲染、驗證流程、自訂驗證方法,以及 CSRF 防護機制。

Django Forms 系統簡介

傳統的表單處理流程通常包含三個步驟:在 HTML 中撰寫 <form> 標籤、在 View 中手動從 request.POST 取出每個欄位值、自行撰寫驗證邏輯。這種方式不僅冗長,還容易遺漏驗證規則,產生安全漏洞。

Django Forms 將這三個步驟自動化封裝:

  1. 定義 Form 類別 – 宣告表單需要哪些欄位、各欄位的驗證規則
  2. 自動渲染 HTML – 根據欄位類型自動生成對應的 HTML 表單控件(Input、Textarea、Select 等)
  3. 自動驗證與清理 – 呼叫 is_valid() 即可完成所有欄位的驗證,並將原始字串轉為 Python 物件

整個流程可以用以下方式理解:

使用者提交原始資料(字串)
    ↓
1. 驗證(Validation):資料是否符合規則?
    ↓
2. 清理(Cleaning):字串 → Python 型別
    ↓
3. cleaned_data:可安全使用的 Python 資料字典

定義 Form 類別

Django 的表單定義寫在 forms.py 檔案中。建立一個 Form 類別只需繼承 forms.Form,並宣告所需的欄位:

# forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100, label='姓名')
    email = forms.EmailField(label='電子郵件')
    message = forms.CharField(widget=forms.Textarea, label='訊息')

每個欄位都是 forms.Field 的子類別實例(Instance),Django 會根據欄位類型自動決定對應的 HTML 控件與驗證規則。


常用欄位類型

Django 內建了豐富的欄位類型(Field Types),以下是最常使用的幾種:

CharField:字串欄位

最基本的文字輸入欄位,預設渲染為 <input type="text">

# 基本文字欄位
name = forms.CharField(max_length=100, min_length=2)

EmailField:電子郵件欄位

繼承自 CharField,額外加上電子郵件格式驗證。

# 會自動驗證 email 格式
email = forms.EmailField()

IntegerField:整數欄位

接受整數輸入,自動將字串轉為 Python 的 int 型別。

# 可設定數值範圍
age = forms.IntegerField(min_value=0, max_value=150)

ChoiceField:選擇欄位

提供下拉選單(Dropdown),需指定 choices 參數。

# 下拉選單,choices 格式為 (值, 顯示文字) 的列表
subject = forms.ChoiceField(choices=[
    ('general', '一般詢問'),
    ('support', '技術支援'),
    ('billing', '帳單問題'),
])

BooleanField:布林欄位

渲染為核取方塊(Checkbox),預設為必填(勾選才算通過驗證)。若要設為非必填,需指定 required=False

# required=False 讓使用者可以不勾選
subscribe = forms.BooleanField(required=False, label='訂閱電子報')

DateField:日期欄位

接受日期格式的輸入,自動轉為 Python 的 datetime.date 物件。

# 搭配 DateInput widget 顯示日曆選擇器
birthdate = forms.DateField(
    required=False,
    widget=forms.DateInput(attrs={'type': 'date'})
)

欄位選項

每個欄位都支援多種選項(Options)來調整行為與外觀:

選項說明範例
required是否為必填欄位(預設 Truerequired=False
label欄位標籤文字,顯示在表單中label='電子郵件'
initial欄位的預設初始值initial='2000-01-01'
widget指定 HTML 控件類型widget=forms.Textarea
help_text欄位下方的提示文字help_text='請輸入有效的 Email'
error_messages自訂錯誤訊息error_messages={'required': '此欄位為必填'}

以下是一個綜合使用各選項的範例:

# 綜合運用各種欄位選項
message = forms.CharField(
    required=True,
    label='訊息內容',
    initial='請在此輸入您的訊息...',
    widget=forms.Textarea(attrs={
        'class': 'form-control',
        'rows': 5,
        'placeholder': '至少輸入 10 個字元'
    }),
    help_text='訊息長度需介於 10 到 2000 個字元之間。',
    min_length=10,
    max_length=2000,
    error_messages={
        'required': '請輸入訊息內容。',
        'min_length': '訊息至少需要 10 個字元。',
        'max_length': '訊息不可超過 2000 個字元。',
    }
)

透過 widgetattrs 參數,你可以為 HTML 控件添加 CSS 類別(Class)、placeholder 等屬性,輕鬆整合 Bootstrap 等前端框架的樣式。


表單渲染方式

Django 提供了多種方式將 Form 物件渲染為 HTML。

自動渲染

Django 內建三種快速渲染方法,適合快速開發與原型驗證:

<!-- 以 <p> 標籤包裹每個欄位 -->
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">送出</button>
</form>

<!-- 以 <tr> 標籤包裹每個欄位(需包在 <table> 中) -->
<form method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <button type="submit">送出</button>
</form>

<!-- 以 <div> 標籤包裹每個欄位(Django 5.0+ 推薦) -->
<form method="post">
    {% csrf_token %}
    {{ form.as_div }}
    <button type="submit">送出</button>
</form>

手動渲染(逐欄位輸出)

當你需要更精細地控制表單的 HTML 結構時,可以逐一輸出每個欄位。這是實際專案中最常見的做法:

<form method="post">
    {% csrf_token %}

    <div class="form-group">
        <label for="{{ form.name.id_for_label }}">{{ form.name.label }}</label>
        {{ form.name }}
        {% if form.name.help_text %}
            <small class="form-text text-muted">{{ form.name.help_text }}</small>
        {% endif %}
        {% for error in form.name.errors %}
            <div class="text-danger">{{ error }}</div>
        {% endfor %}
    </div>

    <div class="form-group">
        <label for="{{ form.email.id_for_label }}">{{ form.email.label }}</label>
        {{ form.email }}
        {% for error in form.email.errors %}
            <div class="text-danger">{{ error }}</div>
        {% endfor %}
    </div>

    <div class="form-group">
        <label for="{{ form.message.id_for_label }}">{{ form.message.label }}</label>
        {{ form.message }}
        {% for error in form.message.errors %}
            <div class="text-danger">{{ error }}</div>
        {% endfor %}
    </div>

    <button type="submit" class="btn btn-primary">送出</button>
</form>

手動渲染的優點是可以完全掌控 HTML 結構與 CSS 樣式,缺點是需要為每個欄位撰寫重複的模板代碼。你也可以使用迴圈來簡化:

<form method="post">
    {% csrf_token %}
    {% for field in form %}
        <div class="form-group">
            <label for="{{ field.id_for_label }}">{{ field.label }}</label>
            {{ field }}
            {% for error in field.errors %}
                <div class="text-danger">{{ error }}</div>
            {% endfor %}
        </div>
    {% endfor %}
    <button type="submit">送出</button>
</form>

表單驗證流程

表單驗證是 Django Forms 最核心的功能。整個驗證流程由 is_valid() 方法觸發。

is_valid() 方法

在 View 中接收到 POST 請求後,將 request.POST 傳入 Form 建構式,再呼叫 is_valid() 進行驗證:

# views.py
from django.shortcuts import render
from .forms import ContactForm

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)  # 綁定使用者提交的資料
        if form.is_valid():
            # 驗證通過,從 cleaned_data 取得資料
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            message = form.cleaned_data['message']
            # 執行後續邏輯(寄信、存入資料庫等)
        # 驗證失敗時,form 會自動攜帶錯誤訊息
    else:
        form = ContactForm()  # 未綁定資料的空白表單
    return render(request, 'contact.html', {'form': form})

cleaned_data:取得驗證後的資料

cleaned_data 是一個字典(dict),包含所有通過驗證且經過型別轉換的欄位值。例如,使用者在 IntegerField 中輸入的字串 "25" 會被轉為 Python 的 int 型別 25

重要原則:永遠從 cleaned_data 取得使用者輸入,而非直接存取 request.POST,因為 cleaned_data 中的資料已經過驗證與清理。

errors:錯誤訊息

驗證失敗時,form.errors 會以字典形式記錄每個欄位的錯誤訊息:

# 查看錯誤訊息
form = ContactForm({'name': '', 'email': 'invalid'})
form.is_valid()  # False

print(form.errors)
# {'name': ['這個欄位是必填的。'], 'email': ['請輸入有效的電子郵件地址。']}

# 也可以用 as_json() 取得 JSON 格式
print(form.errors.as_json())

自訂驗證方法

Django 提供兩種自訂驗證的方式,分別對應單一欄位驗證與跨欄位驗證。

clean_fieldname():單一欄位驗證

方法命名格式為 clean_ 加上欄位名稱。此方法在內建驗證通過後被呼叫,必須回傳清理後的值

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100, label='姓名')
    email = forms.EmailField(label='電子郵件')

    def clean_name(self):
        name = self.cleaned_data.get('name', '')
        if len(name) < 2:
            raise forms.ValidationError('姓名至少需要 2 個字元。')
        return name.strip()  # 必須回傳清理後的值

    def clean_email(self):
        email = self.cleaned_data.get('email', '')
        blocked_domains = ['spam.com', 'fake.org']
        domain = email.split('@')[-1]
        if domain in blocked_domains:
            raise forms.ValidationError(f'不允許使用 {domain} 的電子郵件。')
        return email.lower()  # 統一轉為小寫

clean():跨欄位驗證

當驗證邏輯需要同時存取多個欄位的值時,覆寫 clean() 方法:

class RegistrationForm(forms.Form):
    password = forms.CharField(widget=forms.PasswordInput, label='密碼')
    password_confirm = forms.CharField(widget=forms.PasswordInput, label='確認密碼')

    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        password_confirm = cleaned_data.get('password_confirm')

        if password and password_confirm and password != password_confirm:
            raise forms.ValidationError('兩次輸入的密碼不一致。')
        return cleaned_data

需要注意的是,clean() 中只有先前通過驗證的欄位才會存在於 self.cleaned_data 中。如果某個欄位在 clean_fieldname() 階段就失敗了,它不會出現在 cleaned_data 字典裡,因此存取前應先用 get() 方法確認。

完整的驗證執行順序如下:

is_valid()
    ↓
full_clean()
    ├── 1. _clean_fields()
    │   ├── field.clean()         → 欄位內建驗證
    │   └── clean_<fieldname>()   → 自訂欄位驗證
    ├── 2. _clean_form()
    │   └── clean()               → 跨欄位驗證
    └── 3. _post_clean()
    ↓
有錯誤 → form.errors
無錯誤 → form.cleaned_data

CSRF 防護機制

CSRF(Cross-Site Request Forgery,跨站請求偽造)是一種常見的 Web 攻擊手法。攻擊者透過偽造請求,讓使用者在不知情的情況下對已登入的網站執行非預期操作(例如轉帳、修改密碼)。

Django 透過 CsrfViewMiddleware 中介層(Middleware)自動啟用 CSRF 防護。在每個 POST 表單中,你必須加入 {% csrf_token %} 模板標籤(Template Tag):

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">送出</button>
</form>

{% csrf_token %} 會在 HTML 中生成一個隱藏的 <input> 欄位,包含一段由伺服器產生的隨機驗證碼。當表單提交時,Django 會比對此驗證碼是否與伺服器端的記錄吻合。若不一致或缺少 Token,Django 將回傳 403 Forbidden 錯誤。

注意事項

  • 只有 POSTPUTPATCHDELETE 等修改資料的請求需要 CSRF Token
  • GET 請求不需要(也不應該用 GET 來修改資料)
  • 如果使用 AJAX 發送 POST 請求,需要在 HTTP Header 中帶上 CSRF Token

完整範例:聯絡表單

以下是一個包含 Form、View、Template 三個部分的完整聯絡表單範例,展示 Django Forms 從定義到呈現的完整流程。

定義 Form(forms.py)

# contact/forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(
        max_length=100,
        label='姓名',
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': '請輸入姓名'
        })
    )
    email = forms.EmailField(
        label='電子郵件',
        widget=forms.EmailInput(attrs={
            'class': 'form-control',
            'placeholder': 'example@email.com'
        })
    )
    subject = forms.ChoiceField(
        label='主題',
        choices=[
            ('general', '一般詢問'),
            ('support', '技術支援'),
            ('billing', '帳單問題'),
        ],
        widget=forms.Select(attrs={'class': 'form-control'})
    )
    message = forms.CharField(
        label='訊息',
        min_length=10,
        max_length=2000,
        widget=forms.Textarea(attrs={
            'class': 'form-control',
            'rows': 5,
            'placeholder': '請輸入您的訊息(至少 10 個字元)'
        }),
        error_messages={
            'min_length': '訊息至少需要 10 個字元。',
        }
    )
    subscribe = forms.BooleanField(
        required=False,
        label='訂閱電子報'
    )

    def clean_name(self):
        name = self.cleaned_data.get('name', '')
        if len(name) < 2:
            raise forms.ValidationError('姓名至少需要 2 個字元。')
        return name.strip()

    def clean(self):
        cleaned_data = super().clean()
        subject = cleaned_data.get('subject')
        message = cleaned_data.get('message', '')

        if subject == 'billing' and len(message) < 50:
            raise forms.ValidationError(
                '帳單問題的說明至少需要 50 個字元,以便我們更快處理。'
            )
        return cleaned_data

撰寫 View(views.py)

# contact/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm

def contact_view(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            # 從 cleaned_data 取得驗證後的資料
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            subject = form.cleaned_data['subject']
            message_text = form.cleaned_data['message']

            # 實際應用中,這裡可以寄送 Email 或存入資料庫
            # send_mail(subject, message_text, email, ['admin@example.com'])

            messages.success(request, f'感謝您的來信,{name}!我們會盡快回覆。')
            return redirect('contact')
    else:
        form = ContactForm()

    return render(request, 'contact/contact.html', {'form': form})

設定 URL(urls.py)

# contact/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('contact/', views.contact_view, name='contact'),
]

建立 Template(contact.html)

<!-- templates/contact/contact.html -->
{% extends "base.html" %}

{% block content %}
<h2>聯絡我們</h2>

{% if messages %}
    {% for msg in messages %}
        <div class="alert alert-success">{{ msg }}</div>
    {% endfor %}
{% endif %}

<!-- 顯示跨欄位驗證的錯誤(non_field_errors) -->
{% if form.non_field_errors %}
    <div class="alert alert-danger">
        {% for error in form.non_field_errors %}
            <p>{{ error }}</p>
        {% endfor %}
    </div>
{% endif %}

<form method="post">
    {% csrf_token %}

    {% for field in form %}
        <div class="form-group mb-3">
            <label for="{{ field.id_for_label }}">{{ field.label }}</label>
            {{ field }}
            {% if field.help_text %}
                <small class="form-text text-muted">{{ field.help_text }}</small>
            {% endif %}
            {% for error in field.errors %}
                <div class="text-danger">{{ error }}</div>
            {% endfor %}
        </div>
    {% endfor %}

    <button type="submit" class="btn btn-primary">送出</button>
</form>
{% endblock %}

當使用者成功送出表單後,頁面會顯示成功訊息並重新導向。如果驗證失敗,表單會自動保留使用者先前輸入的資料,並在對應欄位旁顯示錯誤訊息,讓使用者可以直接修正後重新送出。


總結

本篇介紹了 Django Forms 系統的核心基礎,從表單的三大職責 – 驗證清理渲染 – 出發,逐步建立起完整的表單處理知識。

Form 類別的定義 讓你可以用簡潔的 Python 程式碼宣告表單結構,Django 自動對應 HTML 控件與驗證規則。六種常用欄位類型(CharField、EmailField、IntegerField、ChoiceField、BooleanField、DateField)搭配豐富的 欄位選項(required、label、widget、help_text 等),幾乎能覆蓋所有常見的表單需求。

渲染方式 上,as_pas_tableas_div 適合快速原型,而手動渲染則提供了完全的 HTML 控制權。驗證流程is_valid() 統一觸發,通過 cleaned_data 取得安全的 Python 資料,透過 errors 取得錯誤訊息。自訂驗證 方面,clean_fieldname() 負責單一欄位邏輯,clean() 處理跨欄位邏輯。最後,CSRF 防護 只需在模板中加入 {% csrf_token %} 即可自動啟用。

掌握了 Django Forms 的基礎後,在下一篇教學中,我們將進一步介紹 ModelForm – 直接從 Model 自動產生表單的進階技巧,讓表單與資料庫的整合更加無縫。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。