(一)需求分析&技术实现
(二)初步搭建Django环境
(三)页面布局&Django模板
(四)SQL+Pandas初步处理数据
(五)前端表单交互
(六)Ajax异步传参与加载
(七)前端数据格式的处理
(八)DataTables接管前端表格
(九)Pyecharts实现交互图表
(十)静态图表的展示
(十一)“导出数据至Excel”功能
(十二)添加和配置缓存
(十三)用户登录系统
(十四)部署Django至生产环境
在上一章中,我们已经准备好了前端的交互表单,我们期望提交表单后查询参数传回后台并返回相应的结果在前端展示。
在Django中,这种传参的方式是通过URL Dispatcher进行的,也即是把参数埋在关联后台方法的url里,实际通信时再解析出来作为View中Python方法的参数。我们在上章中其实已经有过定义这种包含参数的动态URL的经验,URL中的parameter(Django文档里称为captured value)用下方这种括号格式表示,括号内冒号前的内容为指定的参数类型转化器,默认为str(不包含'/'的字符串)。
下面的URL即为捕获2个字符串参数column和kw:
urlpatterns = [
...
path(r'search/<str:column>/<str:kw>', views.search, name='search')
]
所以这时我们会朴素地想到,我们建一个长长的URL囊括所有前端参数,不就可以传参了。Submit按钮只需要根据表单选择结果负责跳转到相应的URL就可以了。
这么做确实是可以的,但我们需要知道,避免重定向是提高网页性能和用户体验的非常重要的一条原则,而这里同时还有个同步加载和异步加载的问题。
异步加载的性能优势很多人喜欢用类似下面的示意图解释,最终节省了大量的加载时间。

作为一个应用层面的二手程序员,在本例的场景中我更倾向于把异步加载在传参中的应用主要看成一个前端优化。传统的同步加载一般会根据表单参数返回一个完整的网页,但异步加载仅向服务器发送并取回必须的数据,并在不刷新网页的情况下在局部无缝加载。
其实在上章中我们已经不知不觉完成了一次异步加载,表单下拉框备选项的服务器响应其实就是一次异步加载。但因为封装在Semantic UI的API中,不用我们操心,现在需要我们用jQuery自己实现一次。
本例中我们使用AJAX(异步的JavaScript和XML技术)实现异步传参,实际操作中AJAX的传参有2种请求方式,一般认为GET在涉及传输和缓存的性能上都更具优势:
- GET
- POST
数据也可以有多种形式,如:
- 数组/字典
- Json
- 表单序列化,结果类似Json
jQuery语法上也分为两种写法:
- 通过URL传参(类似Django默认方法)
- 通过data传参
本例中我们使用最通用的通过GET用data参数传递字典的方法。我们在上章中放置了一个名为"AJAX_get"的“筛选”提交按钮,编写这个按钮的click function如下:
<script type="text/javascript">
$("#AJAX_get").click(function (event) {
event.preventDefault(); // 防止表单默认的提交
// 获取单选下拉框的值
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();
}
$.ajax({
// 请求的url
url: '{% url 'chpa:query' %}',
// 请求的type
type: 'GET',
// 发送的数据
data: form_data,
// 回调函数,其中ret是返回的JSON,可以以字典的方式调用
success: function (ret) { //成功执行
},
error: function () { //失败
console.log('失败')
}
});
})
</script>
这里别的部分都一目了然,要特别注意下面这一句,没有此语句Django后台会在每次提交时报错,虽然不影响使用,但是很烦人:
event.preventDefault(); // 防止表单默认的提交
我们需要在url.py编辑上方代码块中$.ajax部分对应的url,新建一个query的URL pattern,并绑定到views.py中的query方法:
urlpatterns = [
...
path(r'query', views.query, name='query'),
]
此时如果前端点击筛选按钮,我们已经可以通过浏览器的扩展工具或者观察后端的request.GET变量看到,表单的选择项已经发送到后端了。

那么在views.py中,我们的query方法要实现以下后续功能:
- 解析前端参数到理想格式
- 根据前端参数数据拼接SQL并用Pandas读取
- Pandas读取数据后,将前端选择的DIMENSION作为pivot_table方法的column参数
- 返回Json格式的结果
步骤1最简单的方法就是用下面的语句把request.GET从QueryDict直接转化为Python字典,这个方法最快速,但得到的字典中即使是单选表单的value也是列表,需要注意。当然也可以使用request.GET.get和request.GET.getlist方法精细处理每个参数。
try:
import six # for modern Django
except ImportError:
from django.utils import six # for legacy Django
def query(request):
...
form_dict = dict(six.iterlists(request.GET))
...
步骤2涉及到参数拼接的逻辑连接,本例中取简单的“AND”关系,但实际中有可能AND关系会不够用。这里未来有2种进步方式,1是在前端准备更为复杂的表单,纳入更多逻辑关系符号(甚至可能还需要包括括号位置),也就是实现一个前端的SQL条件语句编译器;2是偷懒的做法,额外提供一个文本框表单,直接传SQL条件语句到后方。不过这样也跟我们直接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']:
field_name = k[:-9] # 字段名
selected = v # 选择项
sql = sql_extent(sql, field_name, selected) #未来可以通过进一步拼接字符串动态扩展sql语句
return sql
def sql_extent(sql, field_name, selected, operator=" AND "):
if selected is not None:
statement = ''
for data in selected:
statement = statement + "'" + data + "', "
statement = statement[:-2]
if statement != '':
sql = sql + operator + field_name + " in (" + statement + ")"
return sql
步骤3就是把之前第四章处理数据的方法拿过来,参数变成动态的。
步骤4修改return语句返回的类型为Json,不渲染具体页面。
整个query方法最后差不多应该长这样:
def query(request):
form_dict = dict(six.iterlists(request.GET))
sql = sqlparse(form_dict) # sql拼接
print(sql)
df = pd.read_sql_query(sql, ENGINE) # 将sql语句结果读取至Pandas Dataframe
dimension_selected = form_dict['DIMENSION_select'][0]
# 如果字段名有空格为了SQL语句在预设字典中加了中括号的,这里要去除
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表现排序
context = {
'market_size': kpi(pivoted)[0],
'market_gr': kpi(pivoted)[1],
'market_cagr': kpi(pivoted)[2],
'ptable': ptable(pivoted).to_html(),
}
return HttpResponse(json.dumps(context, ensure_ascii=False), content_type="application/json charset=utf-8") # 返回结果必须是json格式
而index方法被解放了,只保留初始化表单备选项的功能:
def index(request):
mselect_dict = {}
for key, value in D_MULTI_SELECT.items():
mselect_dict[key] = {}
mselect_dict[key]['select'] = value
context = {
'mselect_dict': mselect_dict
}
return render(request, 'display.html', context)
我们此时已经可以测试这个异步传参的结果了,还记得在第四章我们实现的高血压药ARB的查询实例吗,它是静态的,通过后台人工输入的字符串作为参数查询。现在我们已经可以通过http://127.0.0.1:8088/chpa/query?动态查询结果了。

最后一个简单的步骤,在filter.html的JS代码里修改$.ajax部分的success参数,决定返回的结果怎么处理。我们选择把结果更新到之前模板预留id的DOM元素中。
success: function (ret) { //成功执行
// 更新单位标签
$("#label_size_unit").html("最新"+form_data['PERIOD_select']+ " " +form_data['UNIT_select']);
// 把查询结果输出到网页上预留id的DOM元素中
$("#value_size").html(ret["market_size"].toLocaleString());
$("#value_gr").html(ret["market_gr"].toLocaleString());
$("#value_cagr").html(ret["market_cagr"].toLocaleString());
$("#result_table").html(ret['ptable']);
},
这个项目的核心——网页前后端交互查询出数,可以说基本功能层面至此已经完成了~
最后还有些有关用户体验的细节问题,我们希望查询出数后有个loading界面,这在数据量大时很有用。可以使用Semantic UI的Dimmer(遮罩)组件,在Display.html的最前部分加入下面的语句:
{% extends "chpa_data/analysis.html" %}
{% block display %}
<!-- 数据处理时的loading遮罩 -->
<div class="ui active dimmer" id="dimmer">
<div class="ui text" style="color: #FFFFFF">请使用左侧筛选框选择分析维度和定义市场</div>
</div>
...
然后在JS部分添加按钮点击过和AJAX成功回传过以后修改dimmer的语句:
<script type="text/javascript">
$("#AJAX_get").click(function (event) {
...
var dimmer = $("#dimmer");
dimmer.attr('class', 'ui active dimmer'); // 点击筛选按钮后dimmer变成active
dimmer.children('div').remove(); // 删除初始化文字
dimmer.append('<div class="ui text loader">数据加载中……</div>'); // 增加loading效果和文字
...
$.ajax({
...
success: function (ret) { //成功执行
// 去除加载遮罩(去掉active)
dimmer.attr('class', 'ui dimmer');
...
},
error: function () { //失败
console.log('失败');
dimmer.children('div').text('有错误发生,无法完成查询'); // AJAX回调失败则报错
}
});
})
</script>
Loading界面就完成了:
