一、业务场景
利用pandas进行数据的分析和存取是非常方便的,如果前端需要获取后台分析后的数据,一个很自然的想法是后台通过pandas分析数据,利用pandas.to_excel()生成excel文件,再提供给前端进行下载。然而该方法存在的问题是,随着时间的推移,服务器势必会产生越来越多的临时文件,有没有什么方法能够不产生临时文件而直接将文件输出到前台进行下载呢?
二、解决方法
查看pandas.to_excel()方法可知,该方法不仅可以将数据输出到文件中,也可以输出到一个ExcelWriter对象中,因此我们可以通过BytesIO生成一个ExcelWriter对象,将excel数据输出到该对象中,再通过flask的send_file或make_response提供前端下载。
1.send_file方法
out = io.BytesIO()
writer = pd.ExcelWriter(out, engine='xlsxwriter')
df.to_excel(excel_writer=writer, sheet_name='Sheet1', index=False)
writer.save()
out.seek(0)
#文件名中文支持
file_name = quote('xxx.xlsx')
response = send_file(out, as_attachment=True, attachment_filename=file_name)
response.headers['Content-Disposition'] += "; filename*=utf-8''{}".format(file_name)
return response
这里有几点需要说明(1) engine='xlsxwriter',该参数使用xlsxwriter来生成excel,默认使用openpysx生成的excel打开时会报存在不可读取的内容的警告。(2)out.seek(0)不能遗漏,否则数据无法输出到前端。(3)此方法无需显式调用writer.close()。
2.make_response方法
out = io.BytesIO()
writer = pd.ExcelWriter(out, engine='xlsxwriter')
df.to_excel(excel_writer=writer, sheet_name='Sheet1', index=False)
writer.save()
writer.close()
file_name = quote('xxx.xlsx')
response = make_response(out.getvalue())
response.headers["Content-Disposition"] = "attachment; filename*=utf-8''{}".format(file_name)
response.headers["Content-type"] = "application/x-xlsx"
使用make_response方法时需要显示调用writer.close(),out.seek(0)无需显式调用。
三、利用AJAX POST下载Excel文件
由于后台处理数据有时会比较耗时,因此下载excel时可能会需要等待,这时在前台进行提示是一个比较友好的做法。一个很自然的想法是通过javascript操作页面元素并通过ajax获取文件。
javascript代码--async异步fetch
async function download(){
$("#loading").show(); //显示一个loading图片
report = $("#report").val() // 需要传到后台的数据
const res = await fetch("{{ url_for('main.index')}}",{
method: "POST",
body: JSON.stringify({'report':report}),
headers: {"Content-Type": "application/json"},
responseType: 'blob'
})
if(res.ok){
$("#loading").hide(); //数据获取后再隐藏loading图片
const blData = await res.blob()
const urlObjData = window.URL.createObjectURL(new Blob([blData]))
const fileName = decodeURI(res.headers.get('filename'))
const link = document.createElement('a')
link.href = urlObjData
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(urlObjData)
}
}
loading图片
<div id="loading" style="display: none">
<img src="{{ url_for('static', filename='loading.gif') }}">
</div>
后台不一样的地方,文件名通过headers传到前端
file_name = 'xxx.xlsx'
response = make_response(out.getvalue())
response.headers['filename'] = quote(file_name.encode('utf-8'))
response.headers["Content-type"] = "application/x-xlsx"
关于ajax post下载flask文件可参考ajax post下载flask文件流以及中文文件名。该方法也可用于下载文件后同时刷新页面或者重定向。