又好久沒寫博客了,因為公司在做的東西涉及到業務方面的比較多,沒法寫。
最近在做下載功能,在網上能找到很多例子,但是都不太好用,自己半研究半照抄,終於搞出來了能用的東西。所以覺得應該記錄一下。
下載什么呢?下載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那就這樣。