(一)需求分析&技术实现
(二)初步搭建Django环境
(三)页面布局&Django模板
(四)SQL+Pandas初步处理数据
(五)前端表单交互
(六)Ajax异步传参与加载
(七)前端数据格式的处理
(八)DataTables接管前端表格
(九)Pyecharts实现交互图表
(十)静态图表的展示
(十一)“导出数据至Excel”功能
(十二)添加和配置缓存
(十三)用户登录系统
(十四)部署Django至生产环境
本章实现最后一个必须的功能“导出数据至Excel”,因为目标很明确,实现是比较简单直接的,但也有一些需要注意的地方。
首先,我们需要明确导出数据的response这次不再适合用AJAX回调函数调用了,我们的整个导出功能需要和查询功能query方法并行,也就是需要完全新建一个方法以及相应URL。
为了复用,我们先把之前views.py文件里query方法的前一部分改写成get_df方法:
def get_df(form_dict, is_pivoted=True):
sql = sqlparse(form_dict) # sql拼接
df = pd.read_sql_query(sql, ENGINE) # 将sql语句结果读取至Pandas Dataframe
if is_pivoted is True:
dimension_selected = form_dict['DIMENSION_select'][0]
if dimension_selected[0] == '[':
column = dimension_selected[1:][:-1]
else:
column = dimension_selected
pivoted = pd.pivot_table(df,
values='AMOUNT', # 数据透视汇总值为AMOUNT字段,一般保持不变
index='DATE', # 数据透视行为DATE字段,一般保持不变
columns=column, # 数据透视列为前端选择的分析维度
aggfunc=np.sum) # 数据透视汇总方式为求和,一般保持不变
if pivoted.empty is False:
pivoted.sort_values(by=pivoted.index[-1], axis=1, ascending=False, inplace=True) # 结果按照最后一个DATE表现排序
return pivoted
else:
return df
def query(request):
form_dict = dict(six.iterlists(request.GET))
pivoted = get_df(form_dict)
table = ptable(pivoted)
table = table.to_html(formatters=build_formatters_by_col(table), # 逐列调整表格内数字格式
classes='ui selectable celled table', # 指定表格css class为Semantic UI主题
table_id='ptable' # 指定表格id
)
# Pyecharts交互图表
bar_total_trend = json.loads(prepare_chart(pivoted, 'bar_total_trend', form_dict))
# Matplotlib静态图表
bubble_performance = prepare_chart(pivoted, 'bubble_performance', form_dict)
context = {
'market_size': kpi(pivoted)[0],
'market_gr': kpi(pivoted)[1],
'market_cagr': kpi(pivoted)[2],
'ptable': table,
'bar_total_trend': bar_total_trend,
'bubble_performance': bubble_performance
}
return HttpResponse(json.dumps(context, ensure_ascii=False), content_type="application/json charset=utf-8") # 返回结果必须是json格式
后端实现导出功能新建的export方法也可以和query方法一样在开头调用get_df:
try:
from io import BytesIO as IO # for modern python
except ImportError:
from io import StringIO as IO # for legacy python
import datetime
def export(request, type):
form_dict = dict(six.iterlists(request.GET))
if type == 'pivoted':
df = get_df(form_dict) # 透视后的数据
elif type == 'raw':
df = get_df(form_dict, is_pivoted=False) # 原始数
excel_file = IO()
xlwriter = pd.ExcelWriter(excel_file, engine='xlsxwriter')
df.to_excel(xlwriter, 'data', index=True)
xlwriter.save()
xlwriter.close()
excel_file.seek(0)
# 设置浏览器mime类型
response = HttpResponse(excel_file.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
# 设置文件名
now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") # 当前精确时间不会重复,适合用来命名默认导出文件
response['Content-Disposition'] = 'attachment; filename=' + now + '.xlsx'
return response
实现导出功能的Python写法五花八门,条条大路通罗马。我们这次代码的特点是使用了pandas的df.to_excel方法,需要加载xlsxwriter这个库。有些场景的导出方法则可能需要其他一些处理Excel的库。这段代码最需要注意的是最后设置浏览器mime类型和文件名部分的写法。
相应地,在url.py里再增加一个url pattern:
urlpatterns = [
...
path(r'export/<str:type>', views.export, name='export'),
]
前端因为也不是一个AJAX URL就返回所有数据了,也产生了代码复用的需求,我们把filter.html下面这段获取表单选择结果的JS代码独立出来:
<script>
function getForm(){
// 获取单选下拉框的值
var form_data = {
"DIMENSION_select": $("#DIMENSION_select").val(),
"PERIOD_select": $("#PERIOD_select").val(),
"UNIT_select": $("#UNIT_select").val(),
};
// 获取多选下拉框的值
var dict = {{ mselect_dict|safe }};
for (key in dict) {
var form_name = dict[key]['select'] + "_select";
jquery_selector_id = "[id='" + form_name + "']";//因为我们的部分多选框id有空格,要用这种写法
form_data[form_name] = $(jquery_selector_id).val();
}
return form_data
}
</script>
在display.html里新建两个导出按钮,并写上相应的鼠标点击函数,注意这里的传参用了+ '?' + $.param()的写法:
<div class="ui pointing secondary menu">
...
<a class="item" data-tab="export"><i class="download icon"></i>导出数据</a>
</div>
...
<div class="ui tab segment" data-tab="export">
<div class="ui buttons">
<input class="ui blue button" type='button' id='export_pivot' value="导出整理后时间序列数据"/>
</div>
<div class="ui buttons">
<input class="ui blue button" type='button' id='export_raw' value="导出原始数据"/>
</div>
</div>
<script>
$("#export_pivot").click(function(){
var form_data = getForm();
var downloadUrl = '{% url 'chpa:export' 'pivoted' %}'+ '?' + $.param(form_data, true);
window.location.href = downloadUrl;
});
$("#export_raw").click(function(){
var form_data = getForm();
var downloadUrl = '{% url 'chpa:export' 'raw' %}'+ '?' + $.param(form_data, true);
window.location.href = downloadUrl;
})
</script>
最后需要注意这里的超级大坑,还记得第五章传参时表单多选框[]结尾的问题吗,这里因为不是AJAX传参,这时的多选框又不以[]结尾传参了,这种前后的不一致可能会导致后端混乱。我们需要在后端涉及到的地方顾忌这个问题,比如SQL语句拼接的函数中:
def sqlparse(context):
print(context)
sql = "Select * from %s Where PERIOD = '%s' And UNIT = '%s'" %
(DB_TABLE, context['PERIOD_select'][0], context['UNIT_select'][0]) # 先处理单选部分
# 下面循环处理多选部分
for k, v in context.items():
if k not in ['csrfmiddlewaretoken', 'DIMENSION_select', 'PERIOD_select', 'UNIT_select']:
if k[-2:] == '[]':
field_name = k[:-9] # 如果键以[]结尾,删除_select[]取原字段名
else:
field_name = k[:-7] # 如果键不以[]结尾,删除_select取原字段名
selected = v # 选择项
sql = sql_extent(sql, field_name, selected) #未来可以通过进一步拼接字符串动态扩展sql语句
return sql
完成。