Skip to content

文件上传

基于form表单上传

urls.py

python
from django.contrib import admin
from django.urls import path
from api import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
]

models.py

python
from django.db import models


class User(models.Model):
    name = models.CharField(max_length=64, default='')

    def __str__(self):
        return self.name

index.html

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>

</head>
<body>
<div>
    <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <input type="file" name="f1">
        <input type="text" name="user" value="张开">
        <input type="submit" value="提交">
        <span style="color: red;">{{ error }}</span>
    </form>
</div>
</body>
</html>

一般的,在普通的form表单提交时,请求头中的CONTENT_TYPE: application/x-www-form-urlencoded,然后数据是以键值对的形式传输的,服务端从request.POST取值,这没问题,并且CONTENT_TYPE: application/x-www-form-urlencoded这种编码类型满足大多数情况,一切都挺好的。

而要说使用form表单上传文件,就不得不多说两句了。 起初,http 协议中没有上传文件方面的功能,直到 rfc1867 为 http 协议添加了这个功能。当然在 rfc1867 中限定 form标签的 method 必须为 POSTenctype = "multipart/form-data" 以及<input type = "file">。 所以,当使用form表单上传文件的时候,请求头的content_typemultipart/form-data这种形式的,所以,我们需要在form标签添加enctype="multipart/form-data属性来进行标识。 如果你能打印上传文件的请求头,你会发现CONTENT_TYPE是这样的content_type:multipart/form-data; boundary=----WebKitFormBoundarylZZyJUkrgm6h34DU,那boundary=----WebKitFormBoundarylZZyJUkrgm6h34DU又是什么呢? 在multipart/form-data 后面有boundary以及一串字符,这是分界符,后面的一堆字符串是随机生成的,目的是防止上传文件中出现分界符导致服务器无法正确识别文件起始位置。那分界符又有啥用呢? 对于上传文件的post请求,我们没有使用原有的 http 协议,所以 multipart/form-data 请求是基于 http 原有的请求方式 post 而来的,那么来说说这个全新的请求方式与 post 的区别:

  1. 请求头的不同,对于上传文件的请求,contentType = multipart/form-data是必须的,而 post 则不是,毕竟 post 又不是只上传文件~。
  2. 请求体不同,这里的不同也就是指前者(上传文件请求)在发送的每个字段内容之间必须要使用分界符来隔开,比如文件的内容和文本的内容就需要分隔开,不然服务器就没有办法正常的解析文件,而后者 post 当然就没有分界符直接以key:value的形式发送就可以了。

当然,其中的弯弯绕绕不是三言两语能解释的清楚的,我们先知道怎么用就行。

views.py

python
import os
from openpyxl import load_workbook
from django.shortcuts import render, redirect, HttpResponse
from django.db import transaction
from api.models import User
def index(request):
    """ 导入Excel数据 """
    if request.method == 'POST':
        try:
            with transaction.atomic():   # 事物
                # post请求,我们仍然可以正常提交其他数据
                user = request.POST.get("user")
                # print(user)
                excel = request.FILES.get('f1')
                """
                # 先写到本地也行
                path = os.path.join("media", "files", excel.name)
                with open(path, 'wb') as f:
                    for line in excel:
                        f.write(line)
                wb = load_workbook(path)
                """
                # 直接openpyxl直接load也行
                wb = load_workbook(excel)
                sheet = wb.worksheets[0]
                for row in sheet.iter_rows():
                    # 获取每一行的内容
                    # print(row[0].value)    # 这里取出来每行的数据,打印如果是乱码,不要怕,正常写入数据就没问题的
                    User.objects.create(name=row[0].value)
                return HttpResponse('OK')
        except Exception as e:
            print(e)
            return render(request, 'index.html', {"error": "上传文件类型有误,只支持 xls 和 xlsx 格式的 Excel文档"})

    return render(request, 'index.html', {"error": ""})

基于Ajax上传

urls.py

python
from django.contrib import admin
from django.urls import path
from api import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('upload/', views.upload),
]

models.py

python
from django.db import models


class User(models.Model):
    name = models.CharField(max_length=64, default='')

    def __str__(self):
        return self.name

index.html

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- ajax上传文件开始 -->
<div>
    <!-- csrf_token 固定写法 -->
    {% csrf_token %}
    <input type="text" id="user" value="张开">
    <input type="file" id="ajaxFile">
    <button id="ajaxBtn">上传</button>
    <span style="color: red;" id="msg">{{ error }}</span>
</div>
<!-- ajax上传文件结束 -->
</body>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
    console.log($("[name='csrfmiddlewaretoken']").val());
    $("#ajaxBtn").click(function () {
        // 首先,实例化一个formdata对象
        var formData = new FormData();
        // 然后使用formdata的append来添加数据,即获取文件对象
        // var file_obj = $("#ajaxFile")[0].files[0];    // 使用jQuery获取文件对象
        var file_obj = document.getElementById('ajaxFile').files[0];   // 使用dom也行
        formData.append('f1', file_obj );
        // 处理csrftoken
        formData.append("csrfmiddlewaretoken", $("[name='csrfmiddlewaretoken']").val());
        // 也可以将其他的数据,以键值对的形式,添加到formData中
        formData.append('user',$("#user").val());
        $.ajax({
            url: "/upload/",
            type: "POST",
            data: formData,
            processData:false,  // 默认情况下会将发送的数据序列化以适应默认的内容类型application/x-www-form-urlencoded,如果发送不想转换的的信息的时候需要手动将其设置为false
            contentType:false,  // 固定写法
            success:function (dataMsg) {
                console.log(dataMsg);
                $("#msg").text(dataMsg['message'])
            }
        })
    })
</script>
</html>

在 ajax 中 contentType 设置为 false 是为了避免 JQuery 对请求头content_type进行操作,从而失去分界符,而使服务器不能正常解析文件。 在使用jQuery的$.ajax()方法的时候参数processData默认为true(该方法为jQuery独有的),默认情况下会将发送的数据序列化以适应默认的内容类型application/x-www-form-urlencoded 如果想发送不想转换的信息的时候需要手动将其设置为false即可。

views.py

python
from openpyxl import load_workbook
from django.shortcuts import render, redirect, HttpResponse
from django.http import JsonResponse
from django.db import transaction
from api.models import User


def upload(request):
    if request.method == 'POST':
        user = request.POST.get("user")
        print(user)
        f1 = request.FILES.get('f1')
        wb = load_workbook(f1)
        sheet = wb["北京"]
        for row in sheet.iter_rows():
            # 获取每一行的内容
            print(row[0].value)    # 这里取出来每行的数据,打印如果是乱码,不要怕,正常写入数据就没问题的
            User.objects.create(name=row[0].value)
        return JsonResponse({"message": "upload successful"})
    else:
        return render(request, 'upload.html')

文件下载

文件下载这里主要就是从前端获取要下载的文件。

这个文件可以是从数据库中读出来的,临时写入到服务器某个文件夹内,或者文件就在服务器上存着,这都无所谓。主要就是open读这个文件,然后让浏览器帮我们下载就行了。

文件下载通过两个类来完成:

python
from django.http import StreamingHttpResponse
from django.http import FileResponse  # 是StreamingHttpResponse的子类,都是流式下载,用哪个都行
from django.utils.encoding import escape_uri_path   # 导入这个家伙防止中文文件名乱码或者打不开

来看示例。

urls.py

python
from django.contrib import admin
from django.urls import path
from api import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('upload/', views.upload),
    path('download1/', views.download1),
    path('download2/', views.download2),
    path('users/', views.users),
]

models.py

python
from django.db import models


class User(models.Model):
    name = models.CharField(max_length=64, default='')

    def __str__(self):
        return self.name

user_list.html

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/download1/">点我下载</a>
<a href="/download2/">点我下载</a>
</body>
</html>

重点在views.py

python
import os
from openpyxl import load_workbook
from openpyxl.workbook import Workbook
from django.shortcuts import render, redirect, HttpResponse
from django.http import StreamingHttpResponse, FileResponse
from django.utils.encoding import escape_uri_path   # 导入这个家伙
from django.http import JsonResponse
from django.db import transaction
from api.models import User



def create_file(data):
    filename = '学生表.xlsx'
    file_path = os.path.join("media", "files", filename)
    wb = Workbook()
    sheet = wb['Sheet']

    for index, user in enumerate(data, 1):
        cell = sheet[f'A{index}']
        cell.value = user.name
    wb.save(file_path)
    return file_path, filename

def users(request):
    user_list = User.objects.all()
    return render(request, 'user_list.html', {"user_list": user_list})

def download1(request):
    user_list = User.objects.all()
    file_path, filename = create_file(user_list)
    file = open(file_path, 'rb')
    response = StreamingHttpResponse(file)
    response['Content-Type'] = 'application/octet-stream'
    # escape_uri_path中填写路径,防止中文文件名下载后乱码或者无法打开
    response['Content-Disposition'] = 'attachment;filename="{}"'.format(escape_uri_path(filename))
    return response

def download2(request):
    user_list = User.objects.all()
    file_path, filename = create_file(user_list)
    file = open(file_path, 'rb')
    response = FileResponse(file)
    response['Content-Type'] = 'application/octet-stream'
    # escape_uri_path中填写路径,防止中文文件名下载后乱码或者无法打开
    response['Content-Disposition'] = 'attachment;filename="{}"'.format(escape_uri_path(filename))
    return response

前后端分离的上传下载

下载

window.URL.createObjectURL()和window.URL.revokeObjectURL()

URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。

当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。

浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。

参考:https://juejin.cn/post/7043990457117835271

示例参考:

html
<template>
	<!-- 其它的代码省略了 -->
	<el-button type="success" @click="doExportMobileSubmit">确认导出</el-button>
</template>
<script setup>
    const doExportMobileSubmit = () => {
        // state.ExportMobileForm是提交到后端的数据,后端根据这些数据进行处理,生成对应的文件内容
        axios.post('api/mobile_management/export_mobile/', state.ExportMobileForm).then((res)=>{
            // console.log(888, res)
            if (res.statusText === "OK"){
                // res.data是后端传来的原始数据内容,这里需要转为blob对象,然后后续方便处理
                const blob = new Blob([res.data])
                // 处理文件名,用例decodeURIComponent处理下后端传来的文件名包含中文的问题
                const fileName = decodeURIComponent(res.headers['filename'])
                // 模拟a标签点击下载的过程进行文件下载
                let a = document.createElement('a')
                a.href = window.URL.createObjectURL(blob)
                a.download = fileName  // 下载的文件名
                a.click()
                a.remove()  // 用完即删
                window.URL.revokeObjectURL(blob)  // 释放资源
            }
        })
    }
</script>
python
# 重点就是视图函数这里了
import json
import os
from copy import deepcopy
from io import StringIO
from datetime import datetime
from django.http import FileResponse, StreamingHttpResponse, HttpResponse
from wsgiref.util import FileWrapper
from rest_framework import status
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.decorators import action
from rest_framework.viewsets import GenericViewSet
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from rest_framework.exceptions import ValidationError
from rest_framework.pagination import PageNumberPagination
from rest_framework.filters import BaseFilterBackend
from rest_framework.mixins import DestroyModelMixin
from django.core.files.storage import default_storage
from ..models import PlatformKeyword, MobileManagement, IPManagement
from utils.mixins import CreateModelMixin, ListPageModelMixin, UpdateModelMixin, DestroyModelMixin
from django.conf import settings
from utils import ret_code, tools
from utils.xuehua import snowflake
from utils.encrypt import get_md5
from utils.jwt_auth import create_token
from utils.tools import get_nocharacter_now
from django.utils.encoding import escape_uri_path  # 导入这个家伙解决文件名中文乱码的问题

class MobileManagementViewSet(CreateModelMixin, ListPageModelMixin, UpdateModelMixin, DestroyModelMixin,
                              GenericViewSet):
    queryset = MobileManagement.objects.all().order_by('id')
    serializer_class = MobileManagementSerializer
    pagination_class = MobileManagementPagination
    filter_backends = [MobileManagementSearchFilter]

    @action(methods=['post'], detail=False, url_path='export_mobile')
    def export_mobile(self, request):
        """ 导出手机号 """
        # 获取手机号
        mobiles = MobileManagement.objects.filter(status=0).values_list('mobile', flat=True)
        # 生成文件
        file_name = escape_uri_path("20240807163850张开" + '.txt')
        file_obj = StringIO()  # 开辟到内存空间中去
        file_obj.write('hello\n')
        # 开始封装响应对象
        response = FileResponse(file_obj.getvalue(), content_type='text/plain')
        response['Content-Type'] = 'application/octet-stream'
        # 通过Access-Control-Expose-Headers暴露响应头,否则前端无法获取到响应头
        # 这里暴露了两个响应头,一个是Content-Disposition,一个是filename
        response['Access-Control-Expose-Headers'] = "Content-Disposition"
        response['Access-Control-Expose-Headers'] = "filename"
        response['filename'] = file_name  # 前端下载文件时指定的文件名
        response['Content-Disposition'] = 'attachment;filename="{}"'.format(file_name)  # 这个也照常写
        file_obj.close()  # 关闭临时文件
        return response

效果:

1832669337190662144.png