如何用Python向一个基于Django的网络应用程序上传文件

398 阅读8分钟

简介

万维网促进了联网计算机之间大量数据的传输,它是一个创造和分享大量数据的社区。这些数据可以有各种形式和形状,一些常见的人类可理解的格式是图像、视频和音频文件。

用户已经习惯于在各种各样的软件中进行文件共享,以至于它的新颖性已经不复存在,其功能也常常被认为是标准的。

在本指南中,我们将看看如何用Python向一个基于Django的网络应用程序上传文件。

上传的文件可以以不同的形式进行额外的处理,也可以保持其原始状态。上传文件也引起了一个存储(文件最终在哪里)和显示(如何检索和显示)的问题。在整个指南中,我们将考虑到这些问题,建立一个小项目,为用户提供向Django网络应用程序上传文件的能力。

项目设置

我们将建立一个小项目,在那里我们可以实现Django的文件上传、存储和显示功能,有一个数据库,然而将图片存储在硬盘上。

让我们假设我们生活在一个假想的宇宙中,在那里我们和哈利波特书中的神奇生物一起生活,我们世界的神奇动物学家需要一个应用程序来跟踪他们研究的每一种神奇生物的信息。 我们将创建一个表格,他们可以通过这个表格记录每一种野兽的描述和图片,然后我们将渲染这个表格,存储信息,并在需要时显示给用户。

我们首先创建一个虚拟环境,以避免我们的依赖性导致与其他项目的版本不匹配问题。这一步是可选的,但强烈推荐,并被认为是保持Python环境清洁的良好做法。让我们创建一个目录,它将作为环境的一个容器。

打开你的命令提示符/shell,在我们刚刚创建的目录中,运行。

$ mkdir fileupload
$ cd fileupload
$ python -m venv ./myenv
# OR
$ python3 -m venv ./myenv

现在我们的虚拟环境已经被创建,剩下的就是通过运行activate 脚本来激活它。

# Windows
$ myenv/Scripts/activate.bat
# Linux
$ source myenv/Scripts/activate
# MacOS
$ source env/bin/activate

一旦环境被激活,如果我们安装依赖项,它们将只适用于该环境,而不会与其他环境,甚至是系统环境发生冲突。在这里,我们可以通过pip 来安装Django。

$ pip install "Django==3.0.*"

现在,让我们创建一个项目,通过django-admin 模块的startproject 命令命名为fantasticbeasts 。一旦项目骨架被创建,我们就可以进入该目录,并通过startapp 来启动该应用程序。

$ django-admin startproject fantasticbeasts
$ cd fantasticbeasts
$ django-admin startapp beasts

最后,让我们在fantasticbeasts/settings.py 文件中注册这个应用程序,把它添加到INSTALLED_APPS 的列表中。

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

棒极了!现在我们都准备好了。我们可以为一个Beast 定义一个简单的模型,创建一个表单和模板来显示给终端用户,以及处理他们通过表单发送的文件。

用Django上传文件

创建模型

让我们从定义一个Beast 的模型开始,它直接与一个数据库表相匹配。然后可以创建一个表单来代表这个模型的空白板,让用户填写细节。在beasts/models.py 文件中,我们可以定义一个模型,它扩展了models.Model 类,然后继承了要保存在数据库中的功能。

from django.db import models

class Beast(models.Model):
    MOM_CLASSIFICATIONS = [
    ('XXXXX', 'Known Wizard  Killer'),
    ('XXXX', 'Dangerous'),
    ('XXX', 'Competent wizard should cope'),
    ('XX', 'Harmless'),
    ('X', 'Boring'),
 ]
    name = models.CharField(max_length=60)
    mom_classification = models.CharField(max_length=5, choices=MOM_CLASSIFICATIONS)
    description = models.TextField()
    media = models.FileField(null=True, blank=True)

每个野兽都有一个namedescription 、伴随的media (对野兽的目击)以及一个mom_classification (M.O.M代表魔法部)。

media 是一个 的实例,初始化时, 的参数设置为 。这个初始化让数据库知道,如果输入数据的用户没有任何媒体可以附加,那么 字段为空是可以的。由于我们将把这个模型映射到一个表单上--Django为我们处理验证问题,我们需要让Django知道FileField null True media 表单中的 的输入可以是空白的,这样它就不会在验证过程中引发任何异常。 指的是数据库,而 指的是用户端验证,一般来说,你希望这两个参数被设置为相同的值以保持一致性。media null blank

**注意:**如果你想强制用户添加媒体,将这些参数设置为False

一个FileField ,默认情况下将只处理一个文件,并允许用户从他们的文件系统中上传一个项目。

创建模型表格

一旦我们的模型被定义,我们将把它绑定到一个表单。我们不需要在前端手动做这个,因为Django可以为我们引导这个功能。

from django.forms import ModelForm
from .models import Beast

class BeastForm(ModelForm):
    class Meta: 
        model = Beast
        fields = '__all__'

我们只是创建了一个BeastForm ,并将Beast 模型绑定到它。我们还将fields 设置为__all__ ,这样当我们在 HTML 页面上使用模型时,模型的所有字段都会显示出来。如果你想让一些字段保持隐藏,你可以在这里单独调整字段,不过,对于我们的简单模型来说--我们想全部显示。

用管理员注册模型

Django会自动创建一个管理员网站,供开发者在整个开发过程中使用。在这里,我们可以测试我们的模型和字段,而不需要自己去制作页面。不过对于用户来说,你要创建他们,并在上线前禁用管理网站。

让我们通过将我们的模型添加到beasts/admin.py 文件中,将其注册到管理网站。

from django.contrib import admin
from .models import Beast

admin.site.register(Beast)

注册URL路径

随着应用程序结构的准备,模型的定义和注册,以及与表单的绑定--让我们来配置URL路径,这将允许用户使用这个应用程序。要做到这一点,让我们在我们的应用程序中创建一个urls.py 文件。然后我们就可以在项目级的urls.py 文件中 "包含 "其内容。

我们的beasts/urls.py 将看起来像这样。

from django.urls import path
from .import views

urlpatterns = [
	path("", views.addbeast,  name='addbeast')
 ]

而项目级的[urls.py]将有这样的添加。

urlpatterns = [
    path("", include("reviews.urls"))
]

我们为我们的URL添加一个空字符串,只是因为这是一个袖珍项目,没有必要使它复杂化。我们还没有创建一个视图,但在创建之前在这里注册了它的名字。接下来我们来创建HTML模板和views.addbeast 视图。

创建一个模板来显示我们的表单

为了保存我们的模板,让我们在beasts 目录下创建一个templates 文件夹。这个名字是不容置疑的,因为Django只会在名为templates 的文件夹下寻找HTML模板。

在我们的新文件夹中,让我们添加一个entry.html 文件,它有一个<form> ,接受与Beast 有关的字段。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fantastic Beasts</title>
</head>
<body>
    <form action="/" method="POST" enctype="multipart/form-data">
        {% csrf_token %}
        {% for entry in form %}
           <div>
                {{ entry.label_tag }}
           </div>
           <div>
               {{entry}}
           </div>
        {% endfor %}
        <button>
            Save!
        </button>
    </form>
</body>
</html>

action="/" 属性指向我们将在用户选择*"保存!"*按钮时点击的请求处理程序。表单的输入决定了数据的编码方式,所以我们将enctype 设置为multipart/form-data 类型,以允许文件上传。每当你在Django表单中添加一个"file" 类型的输入时,你就必须将enctype 设置为multipart/form-data

{% csrf_token %} 是任何带有action = "POST" 的表单的另一个必备条件。它是 Django 好心为每个客户端创建的唯一令牌,以确保接受请求时的安全性。CSRF令牌对这个表单的每个POST 请求都是唯一的,它们使CSRF攻击成为不可能。

CSRF攻击包括恶意用户以另一个用户的名义伪造请求,通常是通过另一个域,如果缺少一个有效的令牌,向服务器发出的请求会被拒绝。

我们在for each循环中迭代的form 变量({% for entry in form %})将由视图传递给这个HTML模板。这个变量是我们的BeastForm 的一个实例,它有一些很酷的技巧。我们使用entry.label_tag ,它返回给我们模型表单字段的标签(除非另外指定,否则标签将是字段的名称),并且我们将表单字段包裹在一个div 中,以使我们的表单看起来像样。

创建一个视图来渲染我们的模板

现在,让我们创建一个视图来渲染这个模板,并把它连接到我们的后端。我们首先要导入renderHttpResponseRedirect 类--这两个都是Django内置的类,还有我们的BeastForm 对象。

我们可以选择做一个基于函数的视图,而不是基于类的视图,这对于像这样的简单原型和演示来说是很好的。

如果传入的请求是一个POST 请求,那么就会创建一个新的BeastForm 实例,其中包含POST 请求的主体(字段)和通过请求发送的文件。Django会自动将正文数据反序列化为一个对象,并将request.FILES 作为我们的文件字段注入。

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

def entry(request):
    if request.method == 'POST':
        form = BeastForm(request.POST, request.FILES)
        
        if form.is_valid():
            form.save()
            return HttpResponseRedirect("/") 
    else:
        form = BeastForm()

    return render(request, "entry.html", {
        "form": form
    })

为了验证输入,因为它可能是无效的,我们可以使用BeastForm 实例的is_valid() 方法,如果它是无效的,就清除表单。否则,如果表单是有效的--我们通过save() 方法将其保存到数据库,并将用户重定向到主页(这也是我们的entry.html 页面),提示用户输入另一个野兽的信息。

文件在哪里?

**注意:**通过这种方法,文件被保存在数据库中,不需要进行文件处理。虽然它很有效,但这并不是一个值得推荐的策略,我们将在下一节中用一个适当的文件处理系统来纠正它。

现在,让我们进行迁移,并通过迁移来提交对模型模式的修改(因为我们之前还没有这样做)。一旦我们运行了这个项目,我们就可以看到这一切在开发服务器上的样子。在终端上,当虚拟环境仍然处于活动状态时,运行。

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver

现在,一旦我们使用浏览器点击http://127.0.0.1:8000/ ,你应该看到像这样的东西。

django model form

你可以继续往前走,用一些随机的输入来填写表格,并添加一个文件;任何文件类型都可以,因为我们把这个字段命名为 "媒体",但给它分配了一个FileField ,这是通用的。

**注意:**你可以通过Django强制执行某些文件类型,比如图片,一旦我们涉及到一个更有效的文件存储系统,以及处理多个文件而不是只有一个,我们就会看一下。

提交表单后,你可以通过管理页面看到数据库中的数据。

将文件存储在硬盘而不是数据库中

目前,我们的代码能够将文件存储在数据库中。然而,这并不是一个理想的做法。随着时间的推移,我们的数据库会变得 "肥胖 "和缓慢,而我们不希望这种情况发生。图像已经很久没有以blob的形式存储在数据库中了,你通常会把图像保存在你自己的服务器上,也就是应用程序所在的服务器上,或者外部服务器或服务上,如AWS的S3。

让我们看看如何将上传的文件存储在磁盘上,在我们的项目下有一个漂亮的小文件夹。为了存放这些文件,我们在beasts 下添加一个uploads 文件夹,并修改BeastForm's media 字段,使其指向一个文件夹,而不是一个数据库。

media = models.FileField(upload_to="media", null=True, blank=True)

我们将FileField'的目标文件夹设置为"media" ,这个文件夹还不存在。由于可能会有其他文件被上传,uploads 文件夹将有一个名为"media" 的子目录,供用户上传野兽的图片。

为了让Django知道这个"media" 目录的位置,我们将其添加到settings.py 文件中。

MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads/')

os.path.join(BASE_DIR, 'uploads/') 将 附加到 --这个内置变量保存了我们项目文件夹的绝对路径。 告诉 Django 我们的文件将存放在哪里。"/uploads" BASE_DIR MEDIA_ROOT

让我们保存我们所做的所有改动,一旦我们应用我们的迁移,Django将创建一个名为"media" 的文件夹,如[upload_to="media"],在uploads

此后,所有提交的文件都会被保存在这个文件夹中。数据库臃肿的问题解决了!

用Django上传多个文件

处理多个文件的上传不需要太多的额外工作。我们需要做的就是让我们的模型的表单知道,媒体字段可以接受多于一个的输入。

我们通过在我们的BeastForm 添加一个widgets 字段来做到这一点。

from django.forms import ModelForm, ClearableFileInput
from .models import Beast

class BeastForm(ModelForm):
    class Meta: 
        model = Beast
		fields = '__all__'
        widgets = {
            'media': ClearableFileInput(attrs={'multiple': True})
        }

现在,当在entry.html 页面时,用户被允许选择多个文件,request.FILES 属性将包含更多的文件而不是一个。

使用ImageField在Django中强制执行图像文件

Django定义了一个额外的字段类型--ImageField ,它可以将用户的输入限制在图像文件。我们已经收集了不同类型的文件用于我们的野兽文档,但更多时候,在我们的应用程序中,我们会要求用户输入一个特定的文件。

让我们把我们的FileField ,换成一个ImageField

media = models.ImageField(upload_to="media", null=True, blank=True,)

ImageField 是在Pillowimages上,它是一个广泛使用的Python库,用于处理和操作图像,所以如果你没有已经安装它,你会被提示有异常。

Cannot use ImageField because Pillow is not installed.
        HINT: Get Pillow at https://pypi.org/project/Pillow/ or run command "python -m pip install Pillow".

让我们继续下去,坚持终端的建议。暂时退出服务器运行。

$ python -m pip install Pillow

现在,如果我们继续做并应用我们的迁移,并运行我们的开发服务器,我们会看到,当我们试图上传文件时,我们的选项只限于图片。

显示上传的图片

我们已经非常接近终点线了。让我们看看如何检索和显示我们存储的图片,然后就可以收工了。

来吧,打开你的beasts/views.py 文件。我们将改变我们的if子句,这样当一个表单被成功提交时,视图不会重新加载页面,而是将我们重定向到另一个页面,该页面将包含一个所有野兽的列表及其信息,以及它们的相关图片。

 if form.is_valid():
      form.save()
      return HttpResponseRedirect("/success") 

现在让我们继续创建一个视图来渲染成功页面。在我们的beasts/views.py 文件中,插入。

def success(request):
    beasts = Beast.objects.order_by('name')
    return render(request, "success.html", {
        "beasts": beasts
    })

在我们的*"成功 "*页面上,我们将列出我们数据库中的野兽的名字和图片。要做到这一点,我们只需收集Beast 对象,按其名称排序,并在success.html 模板中渲染它们。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fantastic Beasts</title>
</head>
<body>
    {% for beast in beasts %}
       <div>
            {{ beast.name }}
       </div>
       {% if beast.media %}
        <div>
            <img src="{{ beast.media.url }}" width="500" height=auto alt="">
        </div>
       {% endif %}
    {% endfor %}   
</body>
</html>

我们已经提到,数据库的工作不是存储文件,它的工作是存储这些文件的路径FileFieldImageField 的任何实例都有一个URL属性,指向文件系统中的文件位置。在一个<img> 标签中,我们将这个属性反馈给src 属性,以显示我们的野兽的图片。

默认情况下,Django的安全功能会阻止我们从项目中向外部提供任何文件,这是一个受欢迎的安全检查。不过,我们"media" 文件中公开这些文件,所以我们必须定义一个媒体URL并将其添加到urls.py 文件中。

settings.py 文件中,让我们添加MEDIA_URL

MEDIA_URL = "/beast-media/"

在这里,/name-between/ 可以是任何你想要的东西,尽管它必须用引号和斜线包裹。现在,修改项目级的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("", include("ency.urls"))
]  + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

static() 函数将MEDIA_URL ,映射到我们的文件所在的实际路径,即MEDIA_ROOT 。试图到达我们任何文件的请求可以通过这个MEDIA_URL ,它被自动加到FileFieldImageField 实例的 [url] 属性的前缀。

如果我们保存我们的修改并进入我们的开发服务器,我们现在会看到一切都在顺利进行。

**注意:**这种方法只能在开发中使用,而且只有在MEDIA_URL 是本地的情况下。

总结

在本指南中,我们已经介绍了如何用Django上传文件、存储文件,最后是服务文件。

我们创建了一个小的应用程序,除了教育目的外,它并不十分有用。然而,它应该是一个坚固的垫脚石,让你开始尝试文件的上传。