1. 程式人生 > >Django基礎(17): 如何上傳處理檔案,檔案格式驗證及Ajax檔案上傳示範(附GitHub原始碼)

Django基礎(17): 如何上傳處理檔案,檔案格式驗證及Ajax檔案上傳示範(附GitHub原始碼)

小編我今天要寫篇值得大家收藏的文章。我將重點解釋Django上傳處理檔案中需要考慮的重要事項,並提供一般檔案上傳及Ajax檔案上傳的示範(附GitHub原始碼)。如果你的專案需要用到檔案上傳,你可以從GitHub獲取原始碼,簡化你的開發。

Django檔案上傳需要考慮的重要事項

檔案一般通過表單進行。使用者在前端點選檔案上傳,然後以POST方式將資料和檔案提交到伺服器。伺服器在接收到POST請求後需要將其儲存在伺服器上的某個地方。Django預設的儲存地址是相對於根目錄的/media/資料夾,儲存的預設檔名就是檔案本來的名字。上傳的檔案如果不大於2.5MB,會先存入伺服器記憶體中,然後再寫入磁碟。如果上傳的檔案很大,Django會把檔案先存入臨時檔案,再寫入磁碟。

Django預設處理方式會出現一個問題,所有檔案都儲存在一個資料夾裡。不同使用者上傳的有相同名字的檔案可能會相互覆蓋。另外使用者還可能上傳一些不安全的檔案如js和exe檔案,我們必需對允許上傳檔案的型別進行限制。因此我們在利用Django處理檔案上傳時必需考慮如下3個因素:

  • 設定儲存上傳檔案的資料夾地址

  • 對上傳檔案進行重新命名

  • 對可接受的檔案型別進行限制(表單驗證)

本文將會講解在Django示範程式碼中如何實現上述3個功能。

Django檔案上傳的3種常見方式

Django檔案上傳一般有3種方式(如下所示)。我們會針對3種方式分別提供程式碼示範。

  • 使用一般的表單上傳,在檢視中手動編寫程式碼處理上傳的檔案

  • 使用由模型建立的表單(ModelForm)上傳,使用form.save()方法自動儲存

  • 使用Ajax實現檔案非同步上傳,上傳頁面無需重新整理即可顯示新上傳的檔案

專案建立與設定

我們先使用django-admin startproject命令建立一個叫file_project的專案,然後cd進入file_project, 使用python manage.py startapp建立一個叫file_upload的app。

我們首先需要將file_upload這個app加入到我們專案裡,然後設定/media/和/STATIC_URL/資料夾。我們上傳的檔案都會放在/media/資料夾裡。我們還需要使用css和js這些靜態檔案,所以需要設定STATIC_URL。

#file_project/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'file_upload',
]

#file_project/settings.py

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static"), ]

# specify media root for user uploaded files,
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

#file_project/urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('file/', include("file_upload.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

建立模型

使用Django上傳檔案建立模型不是必需,然而如果我們需要對上傳檔案進行系統化管理,模型還是很重要的。我們的File模型包括file和upload_method兩個欄位。我們通過upload_to選項指定了檔案上傳後儲存的地址,並對上傳的檔案進行了重新命名。如果你想了解如何自定義使用者上傳資料夾地址和對上傳檔案進行重新命名,請閱讀這裡

#file_upload/models.py

from django.db import models
import os
import uuid

# Create your models here.
# Define user directory path

def user_directory_path(instance, filename):
    ext = filename.split('.')[-1]
    filename = '{}.{}'.format(uuid.uuid4().hex[:10], ext)
    return os.path.join("files", filename)


class File(models.Model):
    file = models.FileField(upload_to=user_directory_path, null=True)
    upload_method = models.CharField(max_length=20, verbose_name="Upload Method")

注意:

  • 如果你不使用ModelForm,你還需要手動編寫程式碼儲存上傳檔案。

URLConf配置

本專案一共包括5個urls, 分別對應普通表單上傳,ModelForm上傳和Ajax上傳。還有兩個urls,一個用來顯示檔案清單,一個專門處理ajax請求。

#file_upload/urls.py

from django.urls import re_path, path
from . import views

# namespace
app_name = "file_upload"

urlpatterns = [

    # Upload File Without Using Model Form
    re_path(r'^upload1/$', views.file_upload, name='file_upload'),

    # Upload Files Using Model Form
    re_path(r'^upload2/$', views.model_form_upload, name='model_form_upload'),

    # Upload Files Using Ajax Form
    re_path(r'^upload3/$', views.ajax_form_upload, name='ajax_form_upload'),

    # Handling Ajax requests
    re_path(r'^ajax_upload/$', views.ajax_upload, name='ajax_upload'),

    # View File List
    path('', views.file_list, name='file_list'),

]

使用一般表單上傳檔案

我們先定義一個一般表單FileUploadForm,並通過clean方法對使用者上傳的檔案進行驗證,如果上傳的檔名不以jpg, pdf或xlsx結尾,將顯示錶單驗證錯誤資訊。關於表單的自定義和驗證更多內容見Django基礎(5): 表單forms的設計與使用

#file_upload/forms.py

from django import forms
from .models import File


# Regular form
class FileUploadForm(forms.Form):
    file = forms.FileField(widget=forms.ClearableFileInput(attrs={'class': 'form-control'}))
    upload_method = forms.CharField(label="Upload Method", max_length=20,
                                    widget=forms.TextInput(attrs={'class': 'form-control'}))

    def clean_file(self):
        file = self.cleaned_data['file']
        ext = file.name.split('.')[-1].lower()
        if ext not in ["jpg", "pdf", "xlsx"]:
            raise forms.ValidationError("Only jpg, pdf and xlsx files are allowed.")
        # return cleaned data is very important.
        return file

注意: 

  • 使用clean方法對錶單欄位進行驗證時,別忘了return驗證過的資料,即cleaned_data。只有返回了cleaned_data, 檢視中才可以使用form.cleaned_data.get('xxx')獲取驗證過的資料。

對應一般檔案上傳的檢視file_upload方法如下所示。當用戶的請求方法為POST時,我們通過form.cleaned_data.get('file')獲取通過驗證的檔案,並呼叫自定義的handle_uploaded_file方法來對檔案進行重新命名,寫入檔案。如果使用者的請求方法不為POST,則渲染一個空的FileUploadForm在upload_form.html裡。我們還定義了一個file_list方法來顯示檔案清單。

#file_upload/views.py

from django.shortcuts import render, redirect
from .models import File
from .forms import FileUploadForm, FileUploadModelForm
import os
import uuid
from django.http import JsonResponse
from django.template.defaultfilters import filesizeformat

# Create your views here.


# Show file list
def file_list(request):
    files = File.objects.all().order_by("-id")
    return render(request, 'file_upload/file_list.html', {'files': files})


# Regular file upload without using ModelForm
def file_upload(request):
    if request.method == "POST":
        form = FileUploadForm(request.POST, request.FILES)
        if form.is_valid():
            # get cleaned data
            upload_method = form.cleaned_data.get("upload_method")
            raw_file = form.cleaned_data.get("file")
            new_file = File()
            new_file.file = handle_uploaded_file(raw_file)
            new_file.upload_method = upload_method
            new_file.save()
            return redirect("/file/")
    else:
        form = FileUploadForm()

    return render(request, 'file_upload/upload_form.html', {'form': form,
                                                            'heading': 'Upload files with Regular Form'})


def handle_uploaded_file(file):
    ext = file.name.split('.')[-1]
    file_name = '{}.{}'.format(uuid.uuid4().hex[:10], ext)
    # file path relative to 'media' folder
    file_path = os.path.join('files', file_name)
    absolute_file_path = os.path.join('media', 'files', file_name)

    directory = os.path.dirname(absolute_file_path)
    if not os.path.exists(directory):
        os.makedirs(directory)

    with open(absolute_file_path, 'wb+') as destination:
        for chunk in file.chunks():
            destination.write(chunk)

    return file_path

注意: 

  • handle_uploaded_file方法裡檔案寫入地址必需是包含/media/的絕對路徑,如果/media/files/xxxx.jpg,而該方法返回的地址是相對於/media/資料夾的地址,如/files/xxx.jpg。存在資料中欄位的是相對地址,而不是絕對地址。

  • 構建檔案寫入絕對路徑時請用os.path.join方法,因為不同系統資料夾分隔符不一樣。

  • 寫入檔案前一個良好的習慣是使用os.path.exists檢查目標資料夾是否存在,如果不存在先建立資料夾,再寫入。

上傳表單模板upload_form.html程式碼如下。

#file_upload/templates/upload_form.html

{% extends "file_upload/base.html" %}
{% block content %}
{% if heading %}
<h3>{{ heading }}</h3>
{% endif %}
<form action="" method="post" enctype="multipart/form-data" >
  {% csrf_token %}
  {{ form.as_p }}
 <button class="btn btn-info form-control " type="submit" value="submit">Upload</button>
</form>
{% endblock %}

注意: 

  • 傳送<form>必需有屬性enctype="multipart/form-data",否則表單不能傳送檔案,request.FILES為空。

  • 我們的模板繼承了base.html, 別忘了新增哦, 目的是為了顯示更漂亮。

#file_upload/templates/base.html

{% load static %}

<html lang="en">
<head>
<title>{% block title %}Django File Upload and Download{% endblock %} </title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
</head>

<body>
 <!-- Page content of course! -->
<main class="container-fluid">
<div class="container">

 {% block content %} {% endblock %}

</div> 
</main>

<!-- Bootstrap core JavaScript
================================================== -->

<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

{% block js %} {% endblock %}

</body>
</html>

普通表單上傳檔案頁面顯示如下所示:

顯示檔案清單模板file_list.html程式碼如下所示:

#file_upload/templates/file_list.html

{% extends "file_upload/base.html" %}

{% block content %}
<h3>File List</h3>
<p> <a href="/file/upload1/">RegularFormUpload</a> | <a href="/file/upload2/">ModelFormUpload</a>
    | <a href="/file/upload3/">AjaxUpload</a></p>
{% if files %}
<table class="table table-striped">

    <tbody>
    <tr>
        <td>Filename & URL</td>
        <td>Filesize</td>
        <td>Upload Method</td>
    </tr>
    {% for file in files %}
    <tr>
        <td><a href="{{ file.file.url }}">{{ file.file.url }}</a></td>
        <td>{{ file.file.size | filesizeformat }}</td>
        <td>{{ file.upload_method }}</td>
    </tr>
    {% endfor %}
    </tbody>
</table>

{% else %}
<p>No files uploaded yet. Please click <a href="{% url 'file_upload:file_upload' %}">here</a>
    to upload files.</p>
{% endif %}

{% endblock %}

注意: 

  • 對於上傳的檔案我們可以呼叫file.url, file.name和file.size來檢視上傳檔案的連結,地址和大小。

  • 上傳檔案的大小預設是以B顯示的,數字非常大。使用Django模板過濾器filesizeformat可以將檔案大小顯示為人們可讀的方式,如MB,KB。

檔案清單顯示效果如下所示:

使用ModelForm上傳檔案

使用ModelForm上傳是小編我推薦的上傳方式,前提是你已經在模型中通過upload_to選項自定義了使用者上傳檔案儲存地址,並對檔案進行了重新命名。

我們首先要自定義自己的FileUploadModelForm,由模型重建的。程式碼如下所示:

#file_upload/forms.py

from django import forms
from .models import File


# Model form
class FileUploadModelForm(forms.ModelForm):
    class Meta:
        model = File
        fields = ('file', 'upload_method',)

        widgets = {
            'upload_method': forms.TextInput(attrs={'class': 'form-control'}),
            'file': forms.ClearableFileInput(attrs={'class': 'form-control'}),
        }

    def clean_file(self):
        file = self.cleaned_data['file']
        ext = file.name.split('.')[-1].lower()
        if ext not in ["jpg", "pdf", "xlsx"]:
            raise forms.ValidationError("Only jpg, pdf and xlsx files are allowed.")
        # return cleaned data is very important.
        return file

使用ModelForm處理檔案上傳的檢視model_form_upload方法非常簡單,只需使用form.save()即可,無需再手動編寫程式碼寫入檔案。

#file_upload/views.py

from django.shortcuts import render, redirect
from .models import File
from .forms import FileUploadForm, FileUploadModelForm
import os
import uuid
from django.http import JsonResponse
from django.template.defaultfilters import filesizeformat

# Create your views here.


# Upload File with ModelForm
def model_form_upload(request):
    if request.method == "POST":
        form = FileUploadModelForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return redirect("/file/")
    else:
        form = FileUploadModelForm()

    return render(request, 'file_upload/upload_form.html', {'form': form,
                                                            'heading': 'Upload files with ModelForm'})

上傳表單模板也是upload_form.html,和前例供用的。

#file_upload/templates/upload_form.html

{% extends "file_upload/base.html" %}
{% block content %}
{% if heading %}
<h3>{{ heading }}</h3>
{% endif %}
<form action="" method="post" enctype="multipart/form-data" >
  {% csrf_token %}
  {{ form.as_p }}
 <button class="btn btn-info form-control " type="submit" value="submit">Upload</button>
</form>
{% endblock %}

顯示效果如下所示:

使用Ajax上傳檔案

使用Ajax上傳檔案的好處是,你上傳檔案後無需重新整理頁面或跳轉即可立刻顯示新上傳的檔案資訊(如下所示)。Ajax應用場景還是非常普遍的,比如使用者上傳頭像後無需重新整理實時顯示新上傳的頭像。或則使用者新增評論後無需重新整理頁面直接顯示新增的評論。

AJAX檔案上傳程式碼最重要的部分在前端(程式碼如下所示)。我們構建了FormData物件,添加了file和upload_method, 並通過設定processData=False告訴jQuery不要處理上傳的檔案,交由後臺處理。由於傳送POST請求還需要提供csrftoken,我們還通過jQuery的cookie庫獲取crsftoken,新增到請求頭裡,一起發到伺服器上。如果後臺返回的data沒有error_msg, 就顯示後臺返回的更新過的檔案清單。處理ajax的請求地址是/file/ajax_upload/, 對應的檢視方法是ajax_upload.

#file_upload/templates/ajax_upload_form.html

{% extends "file_upload/base.html" %}
{% block content %}

{% if heading %}
<h3>{{ heading }}</h3>
{% endif %}

<form action="" method="post" enctype="multipart/form-data" id="form">
    <ul class="errorlist"></ul>
    {% csrf_token %}
{{ form.as_p }}
 <input type="button" class="btn btn-info form-control" value="submit" id="btn" />
</form>
<table class="table table-striped" id="result">
</table>
{% endblock %}


{% block js %}
<script src=" https://cdn.jsdelivr.net/jquery.cookie/1.4.1/jquery.cookie.min.js ">
</script>
<script>
var csrftoken = $.cookie('csrftoken');
function csrfSafeMethod(method) {
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$(document).ready(function(){
   $('#btn').click(function(e){
        e.preventDefault();
        // 構建FormData物件
      var form_data = new FormData();
        form_data.append('file', $('#id_file')[0].files[0]);
        form_data.append('upload_method', $('#id_upload_method').val());
        $.ajax({
        url: '/file/ajax_upload/',
        data: form_data,
        type: 'POST',
        dataType: 'json',
        // 告訴jQuery不要去處理髮送的資料, 傳送物件。
      processData : false,
        // 告訴jQuery不要去設定Content-Type請求頭
      contentType : false,
        // 獲取POST所需的csrftoken
        beforeSend: function(xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }},
        success: function (data) {
            if(data['error_msg']) {
                var content = '<li>Only jpg, pdf and xlsx files are allowed.</li>';
                $('ul.errorlist').html(content);
            }
            else
            {
            var content= '<thead><tr>' +
            '<th>Name and URL</th>' +
            '<th>Size</th>' +
            '<th>Upload Method</th>' +
            '</tr></thead><tbody>';

             $.each(data, function(i, item) {
                  content = content +
                  '<tr><td>' +
                  "<a href= ' " +
                  item['url'] +
                  " '> " +
                  item['url'] +
                  '</a></td><td>' +
                  item['size'] +
                  '</td><td>' +
                  item['upload_method'] +
                   '</td><tr>'
                });
             content = content + "</tbody>";
             $('#result').html(content);
             }
           },
        });
   });
 });
  </script>
{% endblock %}

注意: 

  • Ajax程式碼部分程式碼請注意不要隨意變動,尤其評論//部分要特別注意。

負責處理Ajax請求的檢視ajax_upload方法如下所示。該方法將ajax發過來的資料於FileUploadModelForm先結合,然後直接呼叫form.save方法儲存,最後以json格式返回更新過的檔案清單。如何使用者上傳檔案不符合要求,返回錯誤資訊。

#file_upload/views.py

# Upload File with ModelForm
def ajax_form_upload(request):
    form = FileUploadModelForm()
    return render(request, 'file_upload/ajax_upload_form.html', {'form': form,
                                                            'heading': 'File Upload with AJAX'})

# handling AJAX requests
def ajax_upload(request):
    if request.method == "POST":
        form = FileUploadModelForm(data=request.POST, files=request.FILES)
        if form.is_valid():
            form.save()
            # Obtain the latest file list
            files = File.objects.all().order_by('-id')
            data = []
            for file in files:
                data.append({
                    "url": file.file.url,
                    "size": filesizeformat(file.file.size),
                    "upload_method": file.upload_method,
                    })
            return JsonResponse(data, safe=False)
        else:
            data = {'error_msg': "Only jpg, pdf and xlsx files are allowed."}
            return JsonResponse(data)
    return JsonResponse({'error_msg': 'only POST method accpeted.'})

GitHub原始碼

https://github.com/shiyunbo/django-file-upload-download

小結

本文提供並解讀了利用Django上傳檔案的3種主要方式(一般表單上傳,ModelForm上傳和Ajax上傳)及示範程式碼。我們後續會專題講解多檔案上傳和檔案下載(如大檔案下載), 歡迎關注我們的微信公眾號。

如果喜歡本文就加入微信收藏或點贊吧。

大江狗

2018.10.21