作者:朱桃
刚来公司,接手的第一个任务是,开发网站项目的一个功能模块,需要用到python、js、html,在这之前,python还算比较熟悉,js、html完全没使用过,项目基于Django,也是没有用过。因此整个开发过程比较坎坷,边学边用,踩过了很多坑之后,才基本上手了。
比较好的是项目的大框架已经有了,有很多代码可以借鉴和学习,因此降低了入门的难度。从7月20号到8月10号历时二十来天,整个功能初步完成,现在总结下一些关键内容和踩过的坑。
在视频点播业务中,视频的资源分布在全国各地的cdn机房中,机房的磁盘有SSD和SATA两种类型,我们需要尽量将用户请求的视频资源保存在SSD磁盘。主要原因有:
因此,对于机房来说,最主要的优化之一就是提高SSD的命中率。我这里做的事情就是,汇总所有机房的SSD命中率,然后在页面上进行展示,以方便观察各种优化措施是否有效。
这里对SSD命中率,有两种计算方式得到的结果:
具体展示需求有:
下面将对功能模块中主要的部分进行介绍。
上面提到的计算方式一,需要从CGI接口拉取数据,数据接口示例:
http:xxxx/getStructedFeatureData.cgi?data=my_data&user_id=my_id&user_rtx=my_rtx
data中包含了请求的各种参数,示例如下:
data= [{"business_name":"下载","days":1,"date":"2017-07-31","query":[{"module":"oc_http", "target": "ssd_ratio", "type":"server","dimens":{"room":"东莞电信大朗OC3-30G-V"}} ]}]
一开始没考虑太多,想到的是利用jquery的Ajax直接请求数据接口,获取数据展示出来,处理代码全部用js完成。请求和处理代码大概是这样:
function query_download_handler_record(query_arg){
//reate_query_url是根据接口文档构造的url字符串
my_url = create_query_url(query_arg);
$.ajax({
url: my_url,
type: "GET",
contentType : "application/json",
dataType: 'json',
cache: false,
async: true,
}).then(function(result) {
//handler code 省略
return true;
},
function(result) {
alert("查询出错");
return false;
});
return true;
}
问题:
访问拉取数据接口,Ajax请求出现以下错误:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
无法跨域,按照网上建议,将dataType=”json”
改为了”jsonp”
,解决了上述错误,但是得到请求后出现:'Uncaught SyntaxError: Unexpected token :'
原因是返回的是json格式,json和jsonp格式不匹配。
不使用ajax直接跨域请求数据接口,改用python请求数据接口获取数据,处理后返回数据到JS页面中。Python中获取数据接口的数据很简单,直接用requests包就可以了。这时对应的ajax请求部分代码改为:
function query_download_handler_record(query_arg){
$.ajax({
url: "ftp_dispatch_download_hander",
type: "GET",
data: query_arg, //传递的参数
contentType : "application/json",
dataType: 'json',
cache: false,
async: true,
}).then(function(result) {
//handler code
return true;
},
function(result) {
alert("查询出错");
return false;
});
return true;
}
Python部分代码:
def ftp_dispatch_download_hander(request):
total_data = []
query_dimens = []
if request.is_ajax() and 'GET' == request.method:
errno = 0
error = "成功"
oc_list = request.GET.get("oc_list", "")
isp_list = request.GET.get("isp_list", "")
date = request.GET.get("start_date", "")
if oc_list == "" or isp_list == "" or date == "":
errno = -100
return HttpResponse(json.dumps({"errno":errno}), content_type="application/json")
else:
url = create_query_url(oc_list, isp_list, date) //根据参数构造url,和前面类似
page = requests.get(url).content #得到url的内容
//handler code: 最好添加对page的处理
return HttpResponse(page, content_type="application/json")
else:
return HttpResponse(json.dumps ({"errno":-1}), content_type="application/json")
这样的话,在python后端处理代码,还可以做很多处理工作,直接返回js需要的内容即可。代码中间省略了一些处理,这里只是说明大概的处理流程。
在开始进一步设计前端展示界面和编写后端代码时,考虑到数据的本地缓存,主要有以下两个原因:
基于以上两个原因,分别对这两种方式的数据进行汇总缓存,考虑用python脚本,每天定时获取前一天所有机房的数据,汇总保存到一个表中。定时任务用crontab命令,设定每天定时运行一次。
对于数据接口的数据,获取所有机房列表,然后构造对应的请求url,请求数据,得到的数据是每分钟的,进行汇总为每十分钟的,和另一种计算方式结果保持一致。主要的流程代码如下:
def ftp_download_real_ratio_by_group(date):
#数据库连接
dbconn, dbcur = getDb()
table_name = "process_ssd_" + date.replace("-", "")
#创建表
create_table(dbconn, dbcur, table_name)
#数据接口得到所有的机房名称
room_list = ftp_get_room_list()
room_index = 0
for room in room_list:
room_index += 1
#根据机房名称确定运营商isp
isp = get_isp_by_room(room)
#命中率和流量是两个不同的接口,因此需要分别拉取
#根据参数构造请求命中率url,和前面类似, 加了一个获取的目标
url = create_query_url('ssd_ratio', room, isp, date)
content = requests.get(url).content
ssd_ratio_data = json.loads(content)
#根据参数构造请求流量url,和前面类似, 加了一个获取的目标
flow_url = create_query_url('flow', room, isp, date)
content = requests.get(flow_url).content
flow_data = json.loads(content)
#根据每分钟的流量和命中率,计算每十分钟的流量和命中率
dbarray = ftp_download_real_ratio_by_ten_minutes(room, isp, ssd_ratio_data, flow_data)
#保存到数据库
ftp_download_real_ratio_save(dbconn, dbcur, table_name, dbarray)
if __name__ == "__main__":
now = datetime.datetime.now()
yesterday = now - datetime.timedelta(days = 1)
date = yesterday.strftime("%Y-%m-%d") #昨天的日期
ftp_download_real_ratio_by_group(date)
对于流水日志导出的数据,因为已经是存到数据库表中了,只需要将多个表进行汇总就行了,比较简单:
def ftp_get_origin_and_merge(date):
#数据库连接
dbconn, dbcur = get_db()
#得到一天中,每隔十分钟的时间序列,从0000、0010...到2350
time_list = get_record_time_list()
prefix = "t_ssd_hit_ratio_" + date
for time in time_list:
table_name = prefix + time
#得到这个时刻表中所有的记录
results = get_data(dbconn, dbcur, table_name)
array.append(results)
out_table = "flow_ssd_" + date
save_to_table(dbconn, dbcur, out_table, array)
if __name__ == "__main__":
now = datetime.datetime.now()
yesterday = now - datetime.timedelta(days = 1)
date = yesterday.strftime("%Y%m%d") #昨天的日期
ftp_get_origin_and_merge(date)
保存数组到数据库时,执行以下代码时:
sql = "insert into " + out_table + " values(%s, %s, %s, %s, %s, %s, %s, %s)"
try:
dbcur.executemany(sql, array)
dbconn.commit()
except Exception as ex:
dbconn.rollback()
print ex
出现错误:
OperationalError (2006, 'MySQL server has gone away')
插入的array数组太大,分为多次插入即可。
try:
length = len(array)
for i in range(0, length, 1000):
dbcur.executemany(sql, array[i: i + 1000])
dbconn.commit()
except Exception as ex:
dbconn.rollback()
print ex
开发的功能是嵌入到之前的一个项目中,展示的下拉选项框组件为了一致,直接和前面一样,用的bootstrap-multiselect.js这个组件。但是在使用时,发现这个组件有一个问题。
bootstrap-multiselect.js组件设置了includeSelectAllOption为true,即打开了全选选项,如图所示的“select all”:
在点击select all的时候,所有选项都会被选中:
再次点击时,所有选项都会被取消,看似没有问题。但是官网上说明,点击和取消时,分别会调用函数onSelectAll和onDeselectAll,为了针对处理,这两个函数都写了,但在使用时,发现onDeselectAll函数没有被调用。
这是前期bootstrap-multiselect.js自身的bug,点击查看Stack overflow同样问题的回答。项目中使用的版本比较老,是还没有修正的,去下载最新版进行测试,发现onDeselectAll调用没有问题。但是刚把新版的放到项目中,发现其它页面的显示严重错误,猜测可能是还有其他地方做了修改。为了不对之前的页面产生影响,放弃使用新版bootstrap-multiselect.js组件。
最后使用了最麻烦的方法,直接自己添加一个“全部”选项,在onChange方法中,进行判断,如果为“全部”选项选中,则在参数列表加入其他所有选项,如果为取消,则将所有选项从参数列表中去除掉。具体实现中multiselect初始化代码:
$('#download_handler_query_oc_select').multiselect({
maxHeight: 200,
buttonClass: 'btn btn-primary',
nonSelectedText: '请选择一个机房',
numberDisplayed: 1,
enableFiltering: true,
onChange: function(element, checked) {
merge_node = {id:-1, name:"汇总"}
select_all_node = {id:0, name:"全部"};
component = '#download_handler_query_oc_select';
//multiselect_handler 函数处理点击后的各种情况,汇总、全部和其它选项
res = multiselect_handler(element, checked, component, oc_select_option, merge_node, select_all_node,
oc_select_all_flag, oc_merge_flag, download_query_oc_list, 'oc');
oc_select_all_flag = res[0];
oc_merge_flag = res[1];
download_query_oc_list = res[2];
return;
}
});
这里除了全选,还加上了汇总选项,上面调用的multiselect_handler函数代码包含了对下拉框的汇总、全部等选项的所有处理过程,因为机房、机型、运营商下拉选项框都有类似的处理,因此进行了提取,代码流程如下:
function multiselect_handler(element, checked, component, select_option, merge_node, select_all_node, select_all_flag, merge_flag, download_query_list, multiselect_name){
if (checked == true) {
node = {};
node.id = element.val();
node.name = element.text();
if (node.id == select_all_node.id){
//选中全部时 handler code
} else if (node.id == merge_node.id){
//选中汇总时 handler code
} else {
//选择其它选项时,这里得判断汇总、全部是否被选择,如果是则取消
if (select_all_flag == 1){//取消选择全选
$(component).multiselect('deselect', select_all_node.id);
//handler code
}
if (merge_flag == 1) { //取消选择汇总
$(component).multiselect('deselect', merge_node.id);
//handler code
}
//选中其他时 handler code
}
//这里处理三个下拉选择框的联动刷新,改变选择框的选项
select_items_refresh(download_query_list, multiselect_name);
} else if (checked == false) {
var val = element.val();
if (val == select_all_node.id) {
//再次点击,取消全部选项 handler code
} else if(val == merge_node.id){
//再次点击,取消汇总选项 handler code
} else {
//再次点击,取消其他选项 handler code
}
//这里处理三个下拉选择框的联动刷新,改变选择框的选项
select_items_refresh(download_query_list, multiselect_name);
}
return [select_all_flag, merge_flag, download_query_list];
}
上述中,调用的函数select_items_refresh,处理三个下拉框之间的联动刷新,因为对每个不同的组件,刷新有很大的差别,将在这个函数里做区分处理。联动刷新的三个选择框如下:
Js中普通的省市区三级联动代码网上很多,因为省市区是固定顺序刷新的,选择省->刷新市->选择市->刷新区,并且数据固定也不是很多,对应关系可以保存在数组里,比较简单一点。这里不同的是,需要任意点击一个下拉框选项,其余两个都会刷新,机房数量400+并且会变化,机型6种,运营商数量10+,因此只能动态的根据选项变化获取其余两个选项框应该展示的选项框。处理代码比较细节和繁琐,但都是判断和逻辑层面的,这里就不进行说明了。
需要做出的效果类似下图:
才用js不久,总想着用现成的组件,结果发现没有类似的。在查询资料后,发现翻页组件可以用jquery的pagination,每个图表的显示可以用echarts,多个图表的处理,只能自己写函数动态的处理。
自己编写的代码处理流程是:
1、先获取数据,项目中是从数据库查询的数据,这里做demo测试时,直接构造的数据。 2、传入需要显示的页码,根据每页图表数和图表总数,计算总页数->刷新翻页组件,翻页组件中点击某个页码之后会调用callback进行处理,这里callback直接跳回到步骤2. 3、计算当前页需要显示的图表起止索引->绘制图表曲线。
以下是做demo测试的主流程代码:
var figure_numbers_per_page = 3;//每页的图表数量
var data = [];
var fig_numbers = 50;
data = get_data(fig_numbers); //构造数据
get_page(1, fig_numbers, figure_numbers_per_page);
//第几页,总图数,每页的图表数量
function get_page(page_index, figure_count, figure_numbers_per_page){
var page_count = Math.ceil(figure_count / figure_numbers_per_page);//总页数
var ul=$("#figures_div");
ul.empty();
paint_page(page_index, page_count, figure_count);
var figure_start = figure_numbers_per_page * (page_index - 1);
var figure_end = get_figure_end(figure_count, figure_numbers_per_page, page_index, page_count);
//当前页需要展示的最后一个图表的索引
for (var j = figure_start; j < figure_end; j++) {
var e = "<div id = \"figures_"+ j + "\" style=\"width:400px;height:300px;display:none;float:left\"> </div>";
ul.append(e);
}
plot_figures(figure_start, figure_end);
}
//刷新翻页组件,在点击某页之后会调用callback函数进行处理
function paint_page(page_index, page_count, figure_count){
var page_div=$("#pagination_div");
page_div.empty();
var ul_detail="";
$("#pagination_div").pagination({
currentPage: page_index,
totalPage: page_count,
callback: function(current) {
get_page(current, figure_count, figure_numbers_per_page);
}
});
}
//绘制图表曲线
function plot_figures(figure_start, figure_end) {
var option = [];
for (var i = figure_start; i < figure_end; i++){
figure_id = "figures_" + i;
$('#' + figure_id).show();
var chart = echarts.init(document.getElementById(figure_id));
//echarts的配置(表面、曲线名、x轴、y轴数据)
option = create_echart_option(data[i]["title"], data[i]["name"], data[i]["xdata"], data[i]["ydatas"]);
chart.setOption(option);
}
}
保存的表是按照天进行存放的,查询时间段的功能可以选择开始日期和结束日期,查询多天的汇总结果和实时曲线结果。因此需要多表查询:
目前的处理办法是:直接按照每天进行查询,最后将结果进行拼接汇总起来,比较简单。
缺点:多表查询会比较慢,特别是在时间段跨度稍大一点的时候。
优化思路:每天的记录大概是12万,一个月下来是360万,可以加索引优化的字段是时间和机房名称,这个数量级的情况下做好优化,myql还是挺快的。因此后面会考虑将数据库缓存改为按月存放,测试优化前后的速度对比。
问题:Python脚本运行出现语法错误:IndentationError: unindent does not match any outer indentation level,但是文档中每一行都严格对齐了。
原因:代码中存在TAB键和空格混用的情况,Python代码不支持代码对齐中,混用TAB和空格。
解决办法:使用notepad++,打开文档,依次视图->显示符号->显示空格与制表符,可以发现混用的地方。建议python代码统一用空格对齐,tab在不同环境下缩进空格数不一样。
Notepad++设置tab替换为空格:设置(T) ⇒ 首选项... ⇒ 语言 ⇒ 标签设置,勾选 "替换为空格"
Vim设置tab替换为空格:
:set ts=4
:set expandtab
:%retab!
问题:xshell经过跳板机连接测试机,要从本机传文件到测试机,执行rz文件失败,文件大小10M左右,在上传一部分后停止并退出显示一行乱码,执行多次,仍然无法成功。
解决办法:中间有控制字符的原因,加参数-e忽略控制字符,rz -e。常用命令方式:rz -bye。
问题:python2.7代码中用MySQLdb了解数据库进行操作,出现以下错误:
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 24: ordinal not in range(128)
原因:编码冲突
解决办法:以下两步:
1、
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
2、数据库连接时:
dbconn = MySQLdb.connect(host=xx,user=xx,passwd=xx,db =xx,charset="utf8")
dbcur = dbconn.cursor()
dbcur.execute("set names utf8")
这篇文章主要介绍了在功能模块中的一些关键处理思路和流程,以及一些比较典型的问题,都是比较基础的东西。其中的内容,相信各位大牛还有许多更好的处理方式。水平有限,总结的内容可能存在不足,欢迎大家指正!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。