在Flask/Django中增加下载Excel的功能

又好久没写博客了,因为公司在做的东西涉及到业务方面的比较多,没法写。
最近在做下载功能,在网上能找到很多例子,但是都不太好用,自己半研究半照抄,终于搞出来了能用的东西。所以觉得应该记录一下。
 
下载什么呢?下载Excel。我所维护的几个系统里,有一些数据,需要在页面上导出。以前的做法,我都是用定时任务提前把要下载的Excel生成好,保存在服务器的某个位置,但是这样做似乎太傻了。
于是我现在用的是这样一种方法:在生成Excel的时候,最后保存为字节流,而不是一个文件;然后,在框架的response中,设置header,使返回的数据直接是下载类型的。
这样,前端只要直接调用这个接口,就能返回一个可以下载的字节流数据,而下载完成后,保存下来的就是一个Excel了。
 
那么具体要怎么做呢?
首先还是生成Excel。这里我使用的是openpyxl。我之前试了一下用xlwt,其实使用起来也是蛮方便的,并不比openpyxl难用,但是有个致命的弱点:生成的xls文件,最大支持的行数为65535行。而我业务上要生成的Excel,动辄就是十万行起(摊手。xlsx格式的Excel是能够支持到一百万行的,所以也就没什么好说的了。
 
下面我就分成Flask和Django两个版本来介绍一下,下载功能应该怎么做。
 
 
1、Flask版本
 
生成Excel的部分:
 
wb = Workbook()
ws = wb.active
# 首行列名写入excel
for i, t in enumerate(title):
    ws.cell(row=1, column=(i + 1)).value = t[1]
# 数据部分写入excel
title_fields = [t[0] for t in title]
for i, _data in enumerate(data):
    one_row = [_data[t] for t in title_fields]
    for j, d in enumerate(one_row):
        ws.cell(row=(i + 2), column=(j + 1)).value = d

 

这里我是做成了一个通用的写Excel的方法:
data是一个字典,格式  {"key1": "value1", "key2": "value2", ...}
title是一个二维数组,格式  [("key1": "第一列"), ("key2": "第二列"), ...]
这样能保证title写入excel的时候保证跟想要的顺序一致。
 
接下来就到了重点了:一般在保存Excel的时候,我们会用
 
wb.save(filename)

 

而这里我不是这样用的,我是将它保存为一个字节流:
 
sio = BytesIO()
wb.save(sio)

 

接下来就是将这个流返回到浏览器端下载:
 
response = Response()
response.headers.add("Content-Type", "application/vnd.ms-excel")
response.headers.add('Content-Disposition', 'attachment', filename=filename.encode("utf-8").decode("latin1"))
sio.seek(0)
response.data = sio.getvalue()
return response

 

要注意的是,这里面的filename有一个小小的尬点:在flask框架中,header里面的文件名会用latin1编码(flask框架的代码是这么写的):
 
 
这就有点尴尬,我正常的filename如果有中文,并且不经编码,在这里就会报错——没错,就算是python3,中文编码一样能恶心你。
而解决方法,就像上面写的那样,先encode成UTF-8编码,然后再decode成latin1。后面的事情就让框架去做吧。
这样就可以了。当我们去下载的时候,在前端调用这个链接,就能自动下载Excel了,而我们的服务器上,也用不着傻傻地保存一份。
 
 
完整的代码是这样的:
 
app.py
from flask import Flask
from excel import generate_excel

app = Flask(__name__)


@app.route('/')
def hello_world():
    return 'Hello World!'


@app.route('/excel')
def download():
    data = [
        {"key1": 1, "key2": 2, "key3": 3},
        {"key1": 11, "key2": 22, "key3": 33},
        {"key1": 111, "key2": 222, "key3": 333},
        {"key1": 1111, "key2": 2222, "key3": 3333},
        {"key1": 11111, "key2": 22222, "key3": 33333},
    ]
    title = [("key1", "第一列"), ("key2", "第二列"), ("key3", "第三列")]
    filename = "测试Excel.xlsx"
    return generate_excel(title, data, filename)


if __name__ == '__main__':
    app.run()

 

excel.py
from io import BytesIO
from openpyxl import Workbook
from flask import Response


def generate_excel(title, data, filename):
    wb = Workbook()
    ws = wb.active

    # 首行列名写入excel
    for i, t in enumerate(title):
        ws.cell(row=1, column=(i + 1)).value = t[1]
    # 数据部分写入excel
    title_fields = [t[0] for t in title]
    for i, _data in enumerate(data):
        one_row = [_data[t] for t in title_fields]
        for j, d in enumerate(one_row):
            ws.cell(row=(i + 2), column=(j + 1)).value = d

    # 传给save函数的不是保存文件名,而是BytesIO流
    sio = BytesIO()
    wb.save(sio)

    response = Response()
    response.headers.add("Content-Type", "application/vnd.ms-excel")
    response.headers.add('Content-Disposition', 'attachment', filename=filename.encode("utf-8").decode("latin1"))
    sio.seek(0)
    response.data = sio.getvalue()
    return response

 

 

2、Django版本

 
如果用django的话,思路是一致的,只不过在实现方面有点出入。在返回流到浏览器下载这部分,django的写法是这样的:
 
from django.http import HttpResponse
from django.utils.encoding import escape_uri_path
 
# ...从生成excel到wb.save(sio)都是一样的
 
response = HttpResponse()
response["Content-Type"] = "application/vnd.ms-excel"
response["Content-Disposition"] = "attachment; filename*=UTF-8''%s" % escape_uri_path(filename)
# 保存流
sio.seek(0)
response.write(sio.getvalue())
return response

 

其他部分就不贴了。django代码比较繁琐,全贴出来也没什么意义。
使用django的话,其实还有专门的StreamingHttpResponse和FileResponse模块(FileResponse还是从StreamingHttpResponse继承来的),理论上来说应该能更方便,不过我没有尝试。毕竟我懒。
 
OK那就这样。
posted @ 2020-06-26 18:15  _小苹果  阅读(144)  评论(0编辑  收藏