在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那就这样。
本文原创自博客园文章,想了解python相关技巧,欢迎到我的博客踩踩~
地址:http://www.cnblogs.com/anpengapple/