Django 文件上传

Django 文件上传

在学习完 Django 的 Form 模块后,我们就用最后一个常用的文件上传的场景来结束本部分的内容。

1. Django 的文件上传实验

同样,话不多说,我们先通过两个上传的例子来看看 Django 的上传功能。

实验1:简单文件上传

准备本地文件,upload.txt,上传到服务器的 /root/test/django 目录下;

准备模板文件,显示上传按钮:

<form method=post action=/hello/file_upload/ enctype=multipart/form-data>{% csrf_token %}{{ forms }}<br><input type=submit value=提交></form>

完成 Form 表单以及视图函数的编写:

class FileUploadForm(forms.Form):file = forms.FileField(label=文件上传)def handle_uploaded_file(f):save_path = os.path.join('/root/test/django', f.name)with open(save_path, 'wb+') as fp:for chunk in f.chunks():fp.write(chunk)@csrf_exemptdef file_upload(request, *args, **kwargs):error_msg = if request.method == 'POST':forms = FileUploadForm(request.POST,request.FILES)if forms.is_valid():handle_uploaded_file(request.FILES['file'])return HttpResponse('上传成功')error_msg = 异常else:forms = FileUploadForm()return render(request,'test_file_upload.html',{'forms':forms, error_msg: error_msg})

编写 URLConf 配置:

urlpatterns = [# ...# 文件上传测试path('file_upload/', views.file_upload)]

只需要这样几步,一个简单的文件上传就完成了。接下来启动服务进行测试,参考如下的操作:

实验2:使用模型(model) 处理上传的文件

第一步,先设置 settings.py 中的 MEDIA_ROOT,这个设置上传文件保存的根目录;

# first_django_app/settings.py# ...MEDIA_ROOT = '/root/test/'# ...

第二步,准备文件上传模型类

# hello_app/models.py# ...class FileModel(models.Model):name = models.CharField('上传文件名', max_length=)upload_file = models.FileField(upload_to='django')

注意:这个 upload_to 参数和 settings.py 中的 MEDIA_ROOT 属性值一起确定文件上传的目录。它可以有很多种形式,比如写成upload_to='django/%Y/%m/%d' 这样的。此外,该参数可以接收方法名,该方法返回的是上传文件的目录。

第三步,我们必须要生成这个对应的表,使用如下命令:

(django-manual) [root@server first_django_app]# python manage.py makemigrations hello_app
(django-manual) [root@server first_django_app]# python manage.py migrate hello_app

执行完成这两步之后,在数据库里面,我们就生成了相应的表。默认的表面是[应用名_模型类名小写],即hello_app__filemodel。

图片描述

第三步, 准备相应的视图函数;

# hello_app/views.py# ...def file_upload2(request, *args, **kwargs):if request.method == 'POST':upload_file = request.FILES['upload_file']FileModel.objects.create(name=upload_file.name, upload_file=upload_file)return HttpResponse('上传成功')return render(request,'test_file_upload2.html',{})# ...
(django-manual) [root@server first_django_app]# cat templates/test_file_upload2.html 
{% load staticfiles %}

<form method=post action=/hello/file_upload2/ enctype=multipart/form-data> 
    {% csrf_token %}
    <label>选择上传文件:</label><input type=file name=file>
    <div><input type=submit value=提交 style=margin-top:10px></div>
</form>

注意:这里和之前保存文件方式略有不同,直接使用对应模型实例的保存数据方法即可,文件将会自动上传到指定目录下且会在数据库中添加一条记录。

编写对应的 URLconf 配置,如下:

# hello_app/urls.py# ...urlpatterns = [# ...# 文件上传测试path('file_upload2/', views.file_upload2)]

接下来,就是常规的启动服务,然后页面上测试。参考如下:

实验3:多文件上传实验

实现一次上传多个文件也比较简单,我们只需要改动前端的一行代码,就可以支持一次性上传多个文件。改动前端代码如下:

<!--原来的语句 <label>选择上传文件:</label><input type=file name=file> --><label>选择上传文件:</label><input type=file name=files multiple=>

接下来,简单调整下视图函数:

def file_upload2(request, *args, **kwargs):if request.method == 'POST':# 获取文件列表upload_files = request.FILES.getlist('files')# 遍历文件并保存for f in upload_files:FileModel.objects.create(name=f.name, upload_file=f)return HttpResponse('上传成功')return render(request,'test_file_upload2.html',{})

最后看我们的启动服务和测试接口过程如下:

2. Django 的文件上传代码分析

2.1 Django 中和上传文件相关的基础类

这一节主要是来分析下 Django 中和上传文件相关的代码。首先介绍下几个基础类:

FileProxyMixin 类:用于辅助文件上传的 mixin 类。来看看其源码长相:

# 源码路径: django/core/files/utils.pyclass FileProxyMixin:
    A mixin class used to forward file methods to an underlaying file
    object.  The internal file object has to be called file::

        class FileProxy(FileProxyMixin):
            def __init__(self, file):
                self.file = file
    encoding = property(lambda self: self.file.encoding)fileno = property(lambda self: self.file.fileno)flush = property(lambda self: self.file.flush)isatty = property(lambda self: self.file.isatty)newlines = property(lambda self: self.file.newlines)read = property(lambda self: self.file.read)readinto = property(lambda self: self.file.readinto)readline = property(lambda self: self.file.readline)readlines = property(lambda self: self.file.readlines)seek = property(lambda self: self.file.seek)tell = property(lambda self: self.file.tell)truncate = property(lambda self: self.file.truncate)write = property(lambda self: self.file.write)writelines = property(lambda self: self.file.writelines)@propertydef closed(self):return not self.file or self.file.closeddef readable(self):if self.closed:return Falseif hasattr(self.file, 'readable'):return self.file.readable()return Truedef writable(self):if self.closed:return Falseif hasattr(self.file, 'writable'):return self.file.writable()return 'w' in getattr(self.file, 'mode', '')def seekable(self):if self.closed:return Falseif hasattr(self.file, 'seekable'):return self.file.seekable()return Truedef __iter__(self):return iter(self.file)

注意:可以看到,想要继承这个 Mixin 并正常使用,继承的类应该有实例属性 file。这里 Mixin 中的属性和我们在 Python 中用 open()方法得到的文件对象的属性几乎一致,后面实验中可以得到佐证。

File 类:专门为上传文件的定义的基类,直接看源代码。

class File(FileProxyMixin):DEFAULT_CHUNK_SIZE =  *  ** def __init__(self, file, name=None):self.file = fileif name is None:name = getattr(file, 'name', None)self.name = nameif hasattr(file, 'mode'):self.mode = file.modedef __str__(self):return self.name or ''def __repr__(self):return <%s: %s> % (self.__class__.__name__, self or None)def __bool__(self):return bool(self.name)def __len__(self):return self.size

    @cached_propertydef size(self):if hasattr(self.file, 'size'):return self.file.sizeif hasattr(self.file, 'name'):try:return os.path.getsize(self.file.name)except (OSError, TypeError):passif hasattr(self.file, 'tell') and hasattr(self.file, 'seek'):pos = self.file.tell()self.file.seek(, os.SEEK_END)size = self.file.tell()self.file.seek(pos)return sizeraise AttributeError(Unable to determine the file's size.)def chunks(self, chunk_size=None):
        Read the file and yield chunks of ``chunk_size`` bytes (defaults to
        ``File.DEFAULT_CHUNK_SIZE``).
        chunk_size = chunk_size or self.DEFAULT_CHUNK_SIZEtry:self.seek()except (AttributeError, UnsupportedOperation):passwhile True:data = self.read(chunk_size)if not data:breakyield datadef multiple_chunks(self, chunk_size=None):
        Return ``True`` if you can expect multiple chunks.

        NB: If a particular file representation is in memory, subclasses should
        always return ``False`` -- there's no good reason to read from memory in
        chunks.
        return self.size > (chunk_size or self.DEFAULT_CHUNK_SIZE)# ...def open(self, mode=None):if not self.closed:self.seek()elif self.name and os.path.exists(self.name):self.file = open(self.name, mode or self.mode)else:raise ValueError(The file cannot be reopened.)return selfdef close(self):self.file.close()

这里就能看到我们之前在实验1中用来保存上传文件时用到的 chunks() 方法,我们现在通过 Django 的命令行模式来使用下这个 File 类,看它有哪些功能。

(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type help, copyright, credits or license for more information.
(InteractiveConsole)
>>> from django.core.files import File

接下来,我们看到 File 类实例化时要关联一个文件对象,我们使用之前实验1上传的文件 upload.txt 作为实例化参数:

>>> fp = open('/root/test/django/upload.txt', 'r+')>>> f = File(fp)

接下来我们就可以测试 File 对象中的各种属性和方法了。具体操作如下:

>>> f.name'/root/test/django/upload.txt'>>> f.size# 按照20字节大小,判断文件需不需要分块读入>>> f.multiple_chunks()True# 默认块大小64k,47字节太小了,所以不用分块读入>>> f.multiple_chunks()False

我们可以使用 chunks() 方法分块读取文件内容,然后做我们想做的事情,如下:

>>> for c in f.chunks():...     print('本次读入:{}'.format(c))... 本次读入:测试上传文件

xxxxx
spyinx test upload>>> for c in f.chunks():...     print('本次读入:{}'.format(c))... 本次读入:测试上传文件

xxxxx
spyinx
本次读入: test upload

上面测试了2种形式,一种不需要分块读如数据,一口气读完所有内容(因为默认的分块大小大于文件内容)。另一种则设置小一些分块大小,这样会每次读取最多20字节内容,依次打印读取到的内容。

接下来我们看下和上传相关的两个文件类:TemporaryUploadedFileInMemoryUploadedFile。这两个类都是继承自 UploadedFile,而 UploadedFile 又是继承至 File 类的。

# 源码路径: django/core/files/uploadedfile.pyclass UploadedFile(File):
    An abstract uploaded file (``TemporaryUploadedFile`` and
    ``InMemoryUploadedFile`` are the built-in concrete subclasses).

    An ``UploadedFile`` object behaves somewhat like a file object and
    represents some file data that the user submitted with a form.
    def __init__(self, file=None, name=None, content_type=None, size=None, charset=None, content_type_extra=None):super().__init__(file, name)self.size = size
        self.content_type = content_type
        self.charset = charset
        self.content_type_extra = content_type_extradef __repr__(self):return <%s: %s (%s)> % (self.__class__.__name__, self.name, self.content_type)def _get_name(self):return self._namedef _set_name(self, name):# Sanitize the file name so that it can't be dangerous.if name is not None:# Just use the basename of the file -- anything else is dangerous.name = os.path.basename(name)# File names longer than 255 characters can cause problems on older OSes.if len(name) > :name, ext = os.path.splitext(name)ext = ext[:]name = name[: - len(ext)] + ext

        self._name = name

    name = property(_get_name, _set_name)

这个类相比于 File 基类主要是增加了多个实例属性,其他方法到没啥变化。接下里来看继承这个类的两个 File 类:

class TemporaryUploadedFile(UploadedFile):
    A file uploaded to a temporary location (i.e. stream-to-disk).
    def __init__(self, name, content_type, size, charset, content_type_extra=None):_, ext = os.path.splitext(name)file = tempfile.NamedTemporaryFile(suffix='.upload' + ext, dir=settings.FILE_UPLOAD_TEMP_DIR)super().__init__(file, name, content_type, size, charset, content_type_extra)def temporary_file_path(self):Return the full path of this file.return self.file.namedef close(self):try:return self.file.close()except FileNotFoundError:# The file was moved or deleted before the tempfile could unlink# it. Still sets self.file.close_called and calls# self.file.file.close() before the exception.passclass InMemoryUploadedFile(UploadedFile):
    A file uploaded into memory (i.e. stream-to-memory).
    def __init__(self, file, field_name, name, content_type, size, charset, content_type_extra=None):super().__init__(file, name, content_type, size, charset, content_type_extra)self.field_name = field_namedef open(self, mode=None):self.file.seek()return selfdef chunks(self, chunk_size=None):self.file.seek()yield self.read()def multiple_chunks(self, chunk_size=None):# Since it's in memory, we'll never have multiple chunks.return False

这两段代码非常简单,代码展现的逻辑也非常清晰。TemporaryUploadedFile 打开的文件是临时生成的文件,而 InMemoryUploadedFile 类对于上传的文件会保存到内存中。我们熟悉了这两个类之后来对应的处理上传文件的 Handler,一个会使用 TemporaryUploadedFile 类使用临时文件保存上传的文件,另一个会使用 InMemoryUploadedFile 将上传文件的内容写到内存中:

class TemporaryFileUploadHandler(FileUploadHandler):
    Upload handler that streams data into a temporary file.
    def new_file(self, *args, **kwargs):
        Create the file object to append to as data is coming in.
        super().new_file(*args, **kwargs)# 这个文件是打开临时文件的句柄self.file = TemporaryUploadedFile(self.file_name, self.content_type, , self.charset, self.content_type_extra)# 将受到的数据写入到对应的临时文件中def receive_data_chunk(self, raw_data, start):self.file.write(raw_data)# 处理文件完毕def file_complete(self, file_size):# 文件指针,指向初始位置self.file.seek()# 设置文件大小self.file.size = file_sizereturn self.fileclass MemoryFileUploadHandler(FileUploadHandler):
    File upload handler to stream uploads into memory (used for small files).
    def handle_raw_input(self, input_data, META, content_length, boundary, encoding=None):
        Use the content_length to signal whether or not this handler should be
        used.
        # Check the content-length header to see if we should# If the post is too large, we cannot use the Memory handler.self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZEdef new_file(self, *args, **kwargs):super().new_file(*args, **kwargs)if self.activated:self.file = BytesIO()raise StopFutureHandlers()def receive_data_chunk(self, raw_data, start):Add the data to the BytesIO file.if self.activated:self.file.write(raw_data)else:return raw_datadef file_complete(self, file_size):Return a file object if this handler is activated.if not self.activated:returnself.file.seek()return InMemoryUploadedFile(file=self.file,field_name=self.field_name,name=self.file_name,content_type=self.content_type,size=file_size,charset=self.charset,content_type_extra=self.content_type_extra)

2.2 Django 中上传文件流程追踪

这部分内容会有点复杂和枯燥,我会尽量简化代码,并使用前面的上传实验帮助我们在源码中打印一些 print语句,辅助我们更好的理解整个上传过程。

思考问题:为什么上传文件时,我们能通过 request.FILES['file'] 拿到文件?Django 帮我们把文件信息存到这里面,那么它是如何处理上传的文件的呢?

我们现在的目的就是要搞清楚上面的问题,可能里面的代码会比较复杂,目前我们不深入研究代码细节,只是搞清楚整个过程以及 Django 帮我们做了哪些工作。

首先,我们打印下视图函数的 request 参数,发现它是 django.core.handlers.wsgi.WSGIRequest 的一个实例,这在很早之前也是介绍过的。我们重点看看 WSGIRequest 类中的 FILES 属性:

# 源码位置:django/core/handlers/wsgi.py# ...class WSGIRequest(HttpRequest):# ...
    @propertydef FILES(self):if not hasattr(self, '_files'):self._load_post_and_files()return self._files    # ...

看到这里,我们就大概知道 FILES 属性值的来源了,就是通过 self._load_post_and_files() 这个方法设置self._files 值,而这个就是 FILES 的值。接下来就是继续深入 self._load_post_and_files() 这个方法,但是我们不追究代码细节。

# 源码位置:django/http/request.pyclass HttpRequest:A basic HTTP request.# ...def _load_post_and_files(self):Populate self._post and self._files if the content-type is a form typeif self.method != 'POST':self._post, self._files = QueryDict(encoding=self._encoding), MultiValueDict()returnif self._read_started and not hasattr(self, '_body'):self._mark_post_parse_error()returnif self.content_type == 'multipart/form-data':if hasattr(self, '_body'):# Use already read datadata = BytesIO(self._body)else:data = selftry:self._post, self._files = self.parse_file_upload(self.META, data)except MultiPartParserError:# An error occurred while parsing POST data. Since when# formatting the error the request handler might access# self.POST, set self._post and self._file to prevent# attempts to parse POST data again.self._mark_post_parse_error()raiseelif self.content_type == 'application/x-www-form-urlencoded':self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict()else:self._post, self._files = QueryDict(encoding=self._encoding), MultiValueDict()# ...

一般而言,我们使用的是 form 表单提交的上传,对应的 content-type 大部分时候是 multipart/form-data。所以,获取 _files 属性的最重要的代码就是:

self._post, self._files = self.parse_file_upload(self.META, data)

咋继续追踪 self.parse_file_upload() 这个方法。

class HttpRequest:A basic HTTP request.# ...def _initialize_handlers(self):self._upload_handlers = [uploadhandler.load_handler(handler, self) for handler in settings.FILE_UPLOAD_HANDLERS]@propertydef upload_handlers(self):if not self._upload_handlers:# If there are no upload handlers defined, initialize them from settings.self._initialize_handlers()return self._upload_handlers

    @upload_handlers.setterdef upload_handlers(self, upload_handlers):if hasattr(self, '_files'):raise AttributeError(You cannot set the upload handlers after the upload has been processed.)self._upload_handlers = upload_handlersdef parse_file_upload(self, META, post_data):Return a tuple of (POST QueryDict, FILES MultiValueDict).self.upload_handlers = ImmutableList(self.upload_handlers,warning=You cannot alter upload handlers after the upload has been processed.)parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)return parser.parse()# ...

这三个涉及的函数都比较简单,主要是获取处理上传文件的 handlers。settings.FILE_UPLOAD_HANDLERS 这个值是取得 global_settings.py 中设置的,而非项目的 settings.py 文件(该文件默认没有设置该参数值)。但是我们可以在 settings.py 文件中设置 FILE_UPLOAD_HANDLERS 的值以覆盖默认的 handlers。

# 源码位置:django\conf\global_settings.py# ...# List of upload handler classes to be applied in order.FILE_UPLOAD_HANDLERS = ['django.core.files.uploadhandler.MemoryFileUploadHandler','django.core.files.uploadhandler.TemporaryFileUploadHandler',]# ...

最后可以看到 parse_file_upload() 方法的核心语句也只有一句:

parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)

最后调用 parser.parse() 方法获得结果。最后要说明的是 parser.parse() 比较复杂,我们简单看下函数的大致内容即可,课后在继续深究函数的细节:

# 源码位置:django/http/multipartparser.pyclass MultiPartParser:# ...def parse(self):
        Parse the POST data and break it into a FILES MultiValueDict and a POST
        MultiValueDict.

        Return a tuple containing the POST and FILES dictionary, respectively.
        from django.http import QueryDict

        encoding = self._encoding
        handlers = self._upload_handlers# HTTP spec says that Content-Length >= 0 is valid# handling content-length == 0 before continuingif self._content_length == :return QueryDict(encoding=self._encoding), MultiValueDict()# See if any of the handlers take care of the parsing.# This allows overriding everything if need be.for handler in handlers:result = handler.handle_raw_input(self._input_data,self._meta,self._content_length,self._boundary,encoding,)# Check to see if it was handledif result is not None:return result[], result[]# Create the data structures to be used later.self._post = QueryDict(mutable=True)self._files = MultiValueDict()# Instantiate the parser and stream:stream = LazyStream(ChunkIter(self._input_data, self._chunk_size))# Whether or not to signal a file-completion at the beginning of the loop.old_field_name = Nonecounters = [] * len(handlers)# Number of bytes that have been read.num_bytes_read = # To count the number of keys in the request.num_post_keys = # To limit the amount of data read from the request.read_size = None# ...# Signal that the upload has completed.# any() shortcircuits if a handler's upload_complete() returns a value.any(handler.upload_complete() for handler in handlers)self._post._mutable = Falsereturn self._post, self._files

可以看到,这个函数最后得到 self._post, self._files, 然后返回该结果。有兴趣的话可以自行在这几个重要的地方加上 print() 方法看看对应的 self._post, self._files 的输出结果,有助于加深印象。

3. 小结

本小节首先以三个文件上传实验演示了 Django 中的文件上传功能。接下来我们分析了 Django 中涉及文件上传相关的类以及对应的配置参数。在经过这一节的讲解后,我们第三部分内容算是彻底结束了,这部分涉及了许多源码的讲解,会有些枯燥,但是非常有意思。但是如果你能认真追下来,并课后继续阅读和调试代码,相信你会在日后成为 Django 高手,遇到任何问题都能够自己独立解决。