Django-上传和下载

Django之图片文件上传

 就这么六步!

 一、settings配置文件中配置

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'medias').replace('\\', '/')#media即为图片上传的根路径

二、url路由中配置

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.index,name='index'),

] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) #如果单纯的是上传,文件并不用来显示或者读取,就不用加这个

三、models.py文件中的写法

class Book(models.Model):

    name = models.CharField(max_length=32)
    date1 = models.DateTimeField(auto_now=True,null=True)
    date2 = models.DateTimeField(auto_now_add=True,null=True)
    img = models.ImageField(upload_to='img',null=True) #写上upload_to,后面指定一个路径,那么将来上传的文件会直接生成到配置文件中的那个medias文件夹中的img文件夹中,不需要我们自己写读取文件内容写入本地文件的操作,django内部帮我们自动处理了

四、views视图函数中的写法,上传一个图片:

def index(request):

    if request.method == 'POST':
        print(request.POST)
        username = request.POST.get('username')
        print('files',request.FILES)
        file_obj = request.FILES.get('file')
        models.Book.objects.create(
            name=username,
            img=file_obj,
        )  #自动就会将文件上传到我们配置的img文件夹中
        return render(request,'index.html')

 五、更新上传了的文件(注意,只是会更新数据库中那个字段保存的文件的路径,但是之前上传的文件是不会被自动删除的,需要我们自行再写逻辑来删除之前上传错的或者需要被覆盖的文件。还有就是如果上传的文件名称是相同的那么你会发现数据库中这个字段的路径后面的文件名称会出现一个乱起八糟的随机字符串,这是因为上传的文件名称冲突了,django为了解决这个冲突,给你改了一下你的文件名称。)

obj = models.Book.objects.get(name='chao2')
obj.img=file_obj
obj.save()

#下面的update方法是不能更新正确更新保存的文件路径的,除非我们自己手动拼接文件路径,然后img=路径来进行update更新
models.Book.objects.filter(name='chao2').update(img=file_obj)

六、查看已经上传了的文件(就需要借助我们上面在settings配置文件中和url中的配置了)

views.py视图函数的写法:

def index(request):
        objs = models.Book.objects.all()
        return render(request,'index.html',{'objs':objs})

index.html文件中的写法:

<div>
    {% for obj in objs %}
        <img src="/media/{{ obj.img }}" alt="">
    {% endfor %}

</div>

 

<div>
    {% for obj in objs %}
        <img src="/media/{{ obj.img }}" alt="">
        <!--<img src="/media/{{ obj.img.name }}" alt="">-->
    {% endfor %}

</div>

上传

一般的, 上传可以分为通过form表单提交和通过ajax提交两种。

form表单上传

来看示例:
前端重要代码。

<div>
    <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <input type="file" name="f1">
        <input type="text" name="user">
        <input type="submit" value="提交">
    </form>
</div>

一般的,在普通的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视图处理:

from django.shortcuts import render, redirect, HttpResponse
from django.db import transaction
def import_case(request, pk):
    """ 导入Excel数据,pk是所属项目的pk """
    if request.method == 'POST':
        try:
            with transaction.atomic():   # 事物
                # project_pk = request.POST.get("project_pk")  # 数据库使用字段
                excel = request.FILES.get('file_obj')
                book = xlrd.open_workbook(filename=None, file_contents=excel.read())
                sheet = book.sheet_by_index(0)
                title = sheet.row_values(0)
                for row in range(1, sheet.nrows):  
                    print(sheet.row_values(row))    # 这里取出来每行的数据,就可以写入到数据库了
                return HttpResponse('OK')
        except Exception as e:
            print(e)
            return render(request, 'import_case.html', {"project_pk": pk, "error": "上传文件类型有误,只支持 xls 和 xlsx 格式的 Excel文档"})

    return render(request, 'import_case.html', {"project_pk": pk, "error": ""})

ajax上传文件

前端文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- ajax上传文件开始 -->
<div>
    {% csrf_token %}
    <input type="file" id="ajaxFile">
    <button id="ajaxBtn">上传</button>
</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','张开');
        $.ajax({
            url: "/upload/",
            type: "POST",
            data: formData,
            processData:false,  // 
            contentType:false,
            success:function (dataMsg) {
                console.log(dataMsg);
            }
        })
    })
</script>
</html>

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

import xlrd
from django.shortcuts import render
from django.http import JsonResponse

def upload(request):
    if request.is_ajax():
        # print(request.META['CONTENT_TYPE'])  # multipart/form-data; boundary=----WebKitFormBoundaryuXDgAwSKKIGnITam
        # print(request.POST)  # <QueryDict: {'csrfmiddlewaretoken': ['mx1EBTtsOb0k96TUUW8XKbCGvK0Co3S6ZMlLvOuZOKAlO9nfhf6zol0V8KxRxbwT'], 'user': ['张开']}>
        # print(request.FILES)  # <MultiValueDict: {'f1': [<InMemoryUploadedFile: 接口测试示例.xlsx (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)>]}>
        f1 = request.FILES.get('f1')
        print(f1)  # 接口测试示例.xlsx
        book = xlrd.open_workbook(filename=None, file_contents=f1.read())
        sheet = book.sheet_by_index(0)
        print(sheet.row_values(1))  # ['cnodejs项目', 'get /topics 主题首页', 'https://cnodejs.org/api/v1/topics', 'get', '', '{"success":true}']
        # 如果还要保存文件的话
        # with open(f1.name, 'wb') as f:
        #     for line in f:
        #         f.write(line)
        return JsonResponse({"message": "upload successful"})
    else:
        return render(request, 'upload.html')

Django之文件下载

在实际的项目中很多时候需要用到下载功能,如导excel、pdf或者文件下载,当然你可以使用web服务自己搭建可以用于下载的资源服务器,如nginx,这里我们主要介绍django中的文件下载。

我们这里介绍三种Django下载文件的简单写法,然后使用第三种方式,完成一个高级一些的文件下载的方法

index.html内容如下

<div>
    <a href="{% url 'download' %}">文件下载</a>
</div>

urls.py文件内容如下:

urlpatterns = [

    url(r'^index/', views.index,name='index'),
    url(r'^download/', views.download,name='download'),

]

view视图函数的写法有一下三种:

方式1:

from django.shortcuts import HttpResponse
def download(request):
  file = open('crm/models.py', 'rb') #打开指定的文件
  response = HttpResponse(file)   #将文件句柄给HttpResponse对象
  response['Content-Type'] = 'application/octet-stream' #设置头信息,告诉浏览器这是个文件
  response['Content-Disposition'] = 'attachment;filename="models.py"' #这是文件的简单描述,注意写法就是这个固定的写法
  return response

  注意:HttpResponse会直接使用迭代器对象,将迭代器对象的内容存储城字符串,然后返回给客户端,同时释放内存。可以当文件变大看出这是一个非常耗费时间和内存的过程。而StreamingHttpResponse是将文件内容进行流式传输,数据量大可以用这个方法

方式2:

from django.http import StreamingHttpResponse #
def download(request):
  file=open('crm/models.py','rb')
  response =StreamingHttpResponse(file)
  response['Content-Type']='application/octet-stream'
  response['Content-Disposition']='attachment;filename="models.py"'
  return response

方式3:

from django.http import FileResponse
def download(request):
  file=open('crm/models.py','rb')
  response =FileResponse(file)
  response['Content-Type']='application/octet-stream'
  response['Content-Disposition']='attachment;filename="models.py"'
  return response

三种http响应对象在django官网都有介绍.入口:https://docs.djangoproject.com/en/1.11/ref/request-response/

推荐使用FileResponse,从源码中可以看出FileResponse是StreamingHttpResponse的子类,内部使用迭代器进行数据流传输。

解决filename不能有中文的问题

如果你细心的尝试,会发现,上面两中下载方式中的filename不能包含中文,那么如何解决呢?来,往下看!

from django.http import FileResponse
from django.utils.encoding import escape_uri_path   # 导入这个家伙
def download(request):
  file=open('crm/models.py','rb')
  response =FileResponse(file)
  response['Content-Type']='application/octet-stream'
  response['Content-Disposition']='attachment;filename="{}.py"'.format(escape_uri_path("我是中文啦"))
  return response

 

django+drf+vue上传excel文件流程梳理

后端实现

1. 后台url.py

urlpatterns = [
      ...
    re_path('^host_excel/', views.HostExcelView.as_view()),
]

2. 后台views.py

from host.utils.read_host_excel import read_host_excel_data
class HostExcelView(APIView):

    # 上传host数据excel文件
    def post(self,request):
        # request.data中能够拿到通过formdata上传的文件和其他键值对数据
        # request.data>>>> <QueryDict: {'default_password': ['123'], 'host_excel': [<InMemoryUploadedFile: 教学日志模版-吴超-2019年11月份.xlsx (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)>]}>
        # from django.core.files.uploadedfile import InMemoryUploadedFile
        print('request.data>>>>', request.data)

        host_excel = request.data.get('host_excel')


        # 后台处理方式1:创建一个excel的临时存储目录(根目录下创建tem_file),先保存excel文件到本地,然后读取数据,保存,最后删除excel文件。
        # file_path = f'{settings.BASE_DIR}/tem_file/{host_excel.name}'
        # with open(file_path, 'wb') as f:
        #     for i in host_excel:
        #        f.write(i)

        # 针对方式1:如果我们只是为了将数据存储到数据库中,为不需要保存excel文件到本地,那么我们需要删除上传上来的excel文件
        # import os
        # if os.path.exists(file_path):  # 如果文件存在
        #     # 删除文件,可使用以下两种方法。
        #     os.remove(file_path)
        #     # os.unlink(path)
        # else:
        #     print('没有该文件:%s' % file_path)  # 则返回文件不存在

                # 后台处理方式2: 如果我们只是为了将上传的excel数据保存到数据库中,那么我们不需要将excel文件保存到服务器本地,所以我们将接收到的数据,保存到内存bytes数据流中
        from io import BytesIO,StringIO
        sio = BytesIO()
        for i in host_excel:
            sio.write(i) #将数据写入io对象

        # 我在这里做了一个excel文件数据读取的函数,在下面的文件中
        # res_data = read_host_excel_data(file_path, default_password) # 方式1:如果我们采用的是后台处理方式1,那么我们传入excel文件路径作为参数
        res_data = read_host_excel_data(sio, default_password)  # 方式2:如果我们采用的是后台处理方式2,那么我们传入io对象
        # 拿到上传之后的数据之后,我们删掉上传上来的临时excel文件


        return Response(res_data)
 

创建一个py文件存放我们上面处理excel的数据的函数

比如我的在host/utils/read_host_excel.py

def read_host_excel_data(recv_data, default_password=''):

    # data = xlrd.open_workbook(recv_data) #如果传入的是文件路径,那么这样传参
    data = xlrd.open_workbook(file_contents=recv_data.getvalue()) #如果传入的是io数据对象,那么别忘了传参的时候要用关键字file_contents=指定一下,下面的处理两种方式都是一样的了

    # 如下excel的处理,参考我的博客
    # 根据索引获取第一个sheet工作簿
    sheet = data.sheet_by_index(0)
    rows_count = sheet.nrows
    # print(sheet.name, sheet.nrows, sheet.ncols) # sheet名称,行数,列数
    default_password = default_password

    # 查询出所有分类数据
    category_list = HostCategory.objects.values_list('id', 'name')
    # print(category_list)

    host_info_list = []
    for row_number in range(1, rows_count):
        one_row_dict = {}
        # print(sheet.cell(row_number, 0))  # 类型:值,  参数:(行号,列号)
        # print(sheet.cell_type(row_number, 0))  # 单元格数据类型
        # print(sheet.cell_value(row_number, 0)) # 单元格的值
        category = sheet.cell_value(row_number, 0)

        # 由于拿到的是分类名称,所以我们要找到对应名称的分类id,才能去数据库里面存储
        for category_data in category_list:
            # print(category_data[1],type(category_data[1]),category,type(category))
            if category_data[1].strip() == category.strip():
                one_row_dict['category'] = category_data[0]
                break

        # 注意:数据列要对应
        one_row_dict['hostname'] = sheet.cell_value(row_number, 1)
        one_row_dict['ip_addr'] = sheet.cell_value(row_number, 2)
        one_row_dict['port'] = sheet.cell_value(row_number, 3)
        one_row_dict['username'] = sheet.cell_value(row_number, 4)

        # 如果该条记录中没有密码数据,那么使用用户填写的默认密码,如果默认密码也没有,那么报错
        # pwd = sheet.cell_value(row_number, 5)
        print(sheet.cell_value(row_number, 5),type(sheet.cell_value(row_number, 5)))
        excel_pwd = sheet.cell_value(row_number, 5)

        try:
            pwd = str(int(excel_pwd))  # 这样强转容易报错,最好捕获一下异常,并记录单元格位置,给用户保存信息时,可以提示用户哪个单元格的数据有问题
        except:
            pwd = default_password
        # 注意:应该提醒用户,密码列应该转换为字符串类型,也就是excel的文本
        if not pwd.strip():
            pwd = default_password
        one_row_dict['password'] = pwd

        one_row_dict['desc'] = sheet.cell_value(row_number, 6)

        host_info_list.append(one_row_dict)

    # 校验主机数据
    # 将做好的主机信息字典数据通过我们添加主机时的序列化器进行校验
    res_data = {}  # 存放上传成功之后需要返回的主机数据和某些错误信息数据
    serializers_host_res_data = []
    res_error_data = []
    for k, host_data in enumerate(host_info_list):
        s_obj = HostModelSerializers(data=host_data)
        # print(s_obj.is_valid())
        if s_obj.is_valid():
            new_host_obj = s_obj.save()

            serializers_host_res_data.append(new_host_obj)
        else:
            # 报错,并且错误信息中应该体验错误的数据位置
            res_error_data.append({'error': f'该{k+1}行数据有误,其他没有问题的数据,已经添加成功了,请求失败数据改完之后,重新上传这个错误数据,成功的数据不需要上传了'})

    # 再次调用序列化器进行数据的序列化,返回给客户端
    s2_obj = HostModelSerializers(instance=serializers_host_res_data, many=True)
    # print(s2_obj.data)
    res_data['data'] = s2_obj.data
    res_data['error'] = res_error_data


    return res_data

前端vue实现

标签:<input type="file" id="file">
        <button @click="upload">上传</button>
{
    ...
    data(){
        return {}
    },
    methods:{
        upload(){
            // 通过axios或者ajax上传文件,我这里是axios的示例,其实ajax和axios的是差不多的
            const formData = new FormData();
            // file是通过js获取的上传文件对象,不管用什么方式获取的,只要拿到文件对象就行,我下面举个例子,针对上面的input标签
            let file_info = document.getElementById('file');
            let file = file_info.files[0];

            formData.append(`host_excel`, file);

            // 通过formdata上传文件数据,那么axios需要加上如下请求头键值对
            this.$axios.post(`${this.$settings.host}/host/host_excel/`, formData, {
                headers: {'Content-Type': 'multipart/form-data'},

            }).then().catch()

        }
    }


}

Django配置vue完成数据库数据的excel形式的文件下载

将数据库中的数据写入excel文件并下载的流程梳理

后端实现

后台url.py

urlpatterns = [
        ....
    re_path('^host/host_excel/', views.HostExcelView.as_view()),
]

后台视图views.py

class HostExcelView(APIView):
    # 下载host数据excel文件
    def get(self,request):
        #1 读取数据库数据
        all_host_data = models.Host.objects.all().values('id', 'category', 'hostname', 'ip_addr', 'port', 'username','desc', 'evrironments')
        print(all_host_data)

        # 2 写入excel并保存
        # 关于excel的操作,参考我的博客
        # 创建excel
        ws = xlwt.Workbook(encoding='utf-8')
        # 创建工作簿
        st = ws.add_sheet('主机数据')

        # 写标题行
        st.write(0, 0, 'id')
        st.write(0, 1, 'category')
        st.write(0, 2, 'hostname')
        st.write(0, 3, 'ip_addr')
        st.write(0, 4, 'port')
        st.write(0, 5, 'username')
        st.write(0, 6, 'desc')
        st.write(0, 7, 'evrironments')

        # 写入数据,从第一行开始
        excel_row = 1
        for host_obj in all_host_data:
            st.write(excel_row, 0, host_obj.get('id'))
            st.write(excel_row, 1, host_obj.get('category'))
            st.write(excel_row, 2, host_obj.get('hostname'))
            st.write(excel_row, 3, host_obj.get('ip_addr'))
            st.write(excel_row, 4, host_obj.get('port'))
            st.write(excel_row, 5, host_obj.get('username'))
            st.write(excel_row, 6, host_obj.get('desc'))
            st.write(excel_row, 7, host_obj.get('evrironments'))
            excel_row += 1

        # sio = BytesIO()
        # 将数据写入io数据流,不用在本地生成excel文件,不然效率就低了
        sio = BytesIO()
        ws.save(sio)
        sio.seek(0)
        # print(sio.getvalue())


        #3 将excel数据响应回客户端
        response = HttpResponse(sio.getvalue(), content_type='application/vnd.ms-excel')
        # response['Content-Disposition'] = 'attachment; filename=xx.xls'

        # 文件名称中文设置
        from django.utils.encoding import escape_uri_path
        response['Content-Disposition'] = 'attachment; filename={}'.format(escape_uri_path('主机列表数据.xls'))
        response.write(sio.getvalue()) #必须要给response写入一下数据,不然不生效

        return response

前端vue实现

其实不管是不是vue,都可以,因为就是一个a标签就能搞定

<a style="color:#fff;" href="http://api.hippo.com:8000/host/host_excel/"> 导出主机列表数据</a>
注意:href属性的值,是你要访问的后台路径

 

posted @ 2019-02-02 02:11  silencio。  阅读(319)  评论(0)    收藏  举报