檔案上傳

當 Django 處理檔案上傳時,檔案資料最終會放置在 request.FILES 中(有關 request 物件的更多資訊,請參閱請求和回應物件的文件)。本文將說明檔案如何儲存在磁碟和記憶體中,以及如何自訂預設行為。

警告

如果您接受來自不受信任使用者的上傳內容,則存在安全風險!請參閱安全指南中關於使用者上傳內容的主題,以了解緩解措施的詳細資訊。

基本檔案上傳

考慮一個包含 FileField 的表單

forms.py
from django import forms


class UploadFileForm(forms.Form):
    title = forms.CharField(max_length=50)
    file = forms.FileField()

處理此表單的視圖將在 request.FILES 中接收檔案資料,這是一個字典,其中包含表單中每個 FileField (或 ImageField,或其他 FileField 子類別) 的鍵。因此,上述表單中的資料可以透過 request.FILES['file'] 來存取。

請注意,只有當請求方法是 POST,至少實際提交了一個檔案欄位,並且發送請求的 <form> 具有屬性 enctype="multipart/form-data" 時,request.FILES 才會包含資料。否則,request.FILES 將為空。

大多數情況下,您會將來自 request 的檔案資料傳遞到表單中,如 將上傳的檔案繫結到表單 中所述。看起來會像這樣

views.py
from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import UploadFileForm

# Imaginary function to handle an uploaded file.
from somewhere import handle_uploaded_file


def upload_file(request):
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            handle_uploaded_file(request.FILES["file"])
            return HttpResponseRedirect("/success/url/")
    else:
        form = UploadFileForm()
    return render(request, "upload.html", {"form": form})

請注意,我們必須將 request.FILES 傳遞到表單的建構函式中;這是檔案資料如何繫結到表單的方式。

以下是一種您可能處理上傳檔案的常見方式

def handle_uploaded_file(f):
    with open("some/file/name.txt", "wb+") as destination:
        for chunk in f.chunks():
            destination.write(chunk)

使用 UploadedFile.chunks() 迴圈,而不是使用 read(),可以確保大型檔案不會使您的系統記憶體不堪負荷。

UploadedFile 物件上還有其他一些可用的方法和屬性;有關完整參考,請參閱 UploadedFile

使用模型處理上傳的檔案

如果您要將檔案儲存到具有 FileFieldModel 上,使用 ModelForm 會使此過程更容易。當呼叫 form.save() 時,檔案物件將儲存到對應 FileFieldupload_to 參數指定的位置

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import ModelFormWithFileField


def upload_file(request):
    if request.method == "POST":
        form = ModelFormWithFileField(request.POST, request.FILES)
        if form.is_valid():
            # file is saved
            form.save()
            return HttpResponseRedirect("/success/url/")
    else:
        form = ModelFormWithFileField()
    return render(request, "upload.html", {"form": form})

如果您要手動建構物件,您可以將來自 request.FILES 的檔案物件指派給模型中的檔案欄位

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import UploadFileForm
from .models import ModelWithFileField


def upload_file(request):
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            instance = ModelWithFileField(file_field=request.FILES["file"])
            instance.save()
            return HttpResponseRedirect("/success/url/")
    else:
        form = UploadFileForm()
    return render(request, "upload.html", {"form": form})

如果您要在請求之外手動建構物件,您可以將類似 File 的物件指派給 FileField

from django.core.management.base import BaseCommand
from django.core.files.base import ContentFile


class MyCommand(BaseCommand):
    def handle(self, *args, **options):
        content_file = ContentFile(b"Hello world!", name="hello-world.txt")
        instance = ModelWithFileField(file_field=content_file)
        instance.save()

上傳多個檔案

如果您想使用一個表單欄位上傳多個檔案,請建立該欄位 widget 的子類別,並將其 allow_multiple_selected 類別屬性設定為 True

為了讓所有這些檔案都由您的表單驗證(並且欄位的值包含所有這些檔案),您還必須對 FileField 進行子類別化。請參閱下面的範例。

多個檔案欄位

Django 將來很可能會支援適當的多個檔案欄位。

forms.py
from django import forms


class MultipleFileInput(forms.ClearableFileInput):
    allow_multiple_selected = True


class MultipleFileField(forms.FileField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("widget", MultipleFileInput())
        super().__init__(*args, **kwargs)

    def clean(self, data, initial=None):
        single_file_clean = super().clean
        if isinstance(data, (list, tuple)):
            result = [single_file_clean(d, initial) for d in data]
        else:
            result = [single_file_clean(data, initial)]
        return result


class FileFieldForm(forms.Form):
    file_field = MultipleFileField()

然後覆寫 FormView 子類別的 form_valid() 方法來處理多個檔案上傳

views.py
from django.views.generic.edit import FormView
from .forms import FileFieldForm


class FileFieldFormView(FormView):
    form_class = FileFieldForm
    template_name = "upload.html"  # Replace with your template.
    success_url = "..."  # Replace with your URL or reverse().

    def form_valid(self, form):
        files = form.cleaned_data["file_field"]
        for f in files:
            ...  # Do something with each file.
        return super().form_valid(form)

警告

這將允許您僅在表單層級處理多個檔案。請注意,您不能使用它將多個檔案放在單一模型實例(在單一欄位中),例如,即使自訂 widget 用於與模型 FileField 相關的表單欄位也是如此。

上傳處理器

當使用者上傳檔案時,Django 會將檔案資料傳遞給上傳處理器 — 一個處理上傳檔案資料的小類別。上傳處理器最初在 FILE_UPLOAD_HANDLERS 設定中定義,預設為

[
    "django.core.files.uploadhandler.MemoryFileUploadHandler",
    "django.core.files.uploadhandler.TemporaryFileUploadHandler",
]

結合 MemoryFileUploadHandlerTemporaryFileUploadHandler 提供了 Django 的預設檔案上傳行為,即將小檔案讀取到記憶體中,將大檔案讀取到磁碟上。

您可以撰寫自訂處理器,以自訂 Django 處理檔案的方式。例如,您可以使用自訂處理器來實施使用者層級的配額、即時壓縮資料、渲染進度列,甚至直接將資料傳送到另一個儲存位置,而無需將其儲存在本機。請參閱 撰寫自訂上傳處理器,以了解如何自訂或完全取代上傳行為的詳細資訊。

上傳的資料儲存在哪裡

在儲存上傳的檔案之前,資料需要儲存在某個地方。

預設情況下,如果上傳的檔案小於 2.5 MB,Django 會將上傳的全部內容保存在記憶體中。這表示儲存檔案僅涉及從記憶體讀取和寫入磁碟,因此速度非常快。

但是,如果上傳的檔案太大,Django 會將上傳的檔案寫入儲存在系統臨時目錄中的臨時檔案中。在類似 Unix 的平台上,這表示您可以預期 Django 會產生一個類似 /tmp/tmpzfp6I6.upload 的檔案。如果上傳的檔案夠大,您可以在 Django 將資料串流到磁碟時觀察此檔案的大小增長。

這些細節 — 2.5 MB;/tmp;等等 — 是「合理的預設值」,可以按照下一節所述進行自訂。

變更上傳處理器的行為

有一些設定可以控制 Django 的檔案上傳行為。請參閱 檔案上傳設定 以了解詳細資訊。

動態修改上傳處理器

有時,特定的視圖會需要不同的上傳行為。在這些情況下,您可以透過修改 request.upload_handlers,來針對每個請求覆寫上傳處理器。預設情況下,此列表會包含 FILE_UPLOAD_HANDLERS 所提供的上傳處理器,但您可以像修改其他列表一樣修改此列表。

例如,假設您撰寫了一個 ProgressBarUploadHandler,它會向某種 AJAX 小工具提供上傳進度的回饋。您會像這樣將此處理器新增到您的上傳處理器中:

request.upload_handlers.insert(0, ProgressBarUploadHandler(request))

在這種情況下,您可能想要使用 list.insert()(而不是 append()),因為進度列處理器需要任何其他處理器之前執行。請記住,上傳處理器會按照順序處理。

如果您想要完全替換上傳處理器,您可以指定一個新的列表:

request.upload_handlers = [ProgressBarUploadHandler(request)]

注意

您只能在存取 request.POSTrequest.FILES 之前修改上傳處理器 – 在上傳處理已經開始後變更上傳處理器是沒有意義的。如果您嘗試在從 request.POSTrequest.FILES 讀取資料後修改 request.upload_handlers,Django 會拋出錯誤。

因此,您應該盡可能在視圖中盡早修改上傳處理器。

此外,request.POST 會被預設啟用的 CsrfViewMiddleware 存取。這表示您需要對您的視圖使用 csrf_exempt(),以允許您變更上傳處理器。然後,您需要在實際處理請求的函數上使用 csrf_protect()。請注意,這表示處理器可能會在 CSRF 檢查完成之前開始接收檔案上傳。範例程式碼:

from django.views.decorators.csrf import csrf_exempt, csrf_protect


@csrf_exempt
def upload_file_view(request):
    request.upload_handlers.insert(0, ProgressBarUploadHandler(request))
    return _upload_file_view(request)


@csrf_protect
def _upload_file_view(request):
    # Process request
    ...

如果您使用基於類的視圖,您需要在其 dispatch() 方法上使用 csrf_exempt(),並在實際處理請求的方法上使用 csrf_protect()。範例程式碼:

from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt, csrf_protect


@method_decorator(csrf_exempt, name="dispatch")
class UploadFileView(View):
    def setup(self, request, *args, **kwargs):
        request.upload_handlers.insert(0, ProgressBarUploadHandler(request))
        super().setup(request, *args, **kwargs)

    @method_decorator(csrf_protect)
    def post(self, request, *args, **kwargs):
        # Process request
        ...
返回頂部