基于 python 、js 的一个网页模块开发流程总结

作者:朱桃

导语

刚来公司,接手的第一个任务是,开发网站项目的一个功能模块,需要用到python、js、html,在这之前,python还算比较熟悉,js、html完全没使用过,项目基于Django,也是没有用过。因此整个开发过程比较坎坷,边学边用,踩过了很多坑之后,才基本上手了。

比较好的是项目的大框架已经有了,有很多代码可以借鉴和学习,因此降低了入门的难度。从7月20号到8月10号历时二十来天,整个功能初步完成,现在总结下一些关键内容和踩过的坑。

1、功能模块背景和需求

在视频点播业务中,视频的资源分布在全国各地的cdn机房中,机房的磁盘有SSD和SATA两种类型,我们需要尽量将用户请求的视频资源保存在SSD磁盘。主要原因有:

  • SSD读取速度快,SATA读取速度慢,在播放高码率的视频文件时,有可能会出现SATA读取速度跟不上,导致视频播放出现卡顿;
  • SATA读取速度慢,在有大量请求时,系统不能及时处理,导致系统负载增大,以至于最后可能挂掉;
  • SSD比SATA更贵,控制成本的原因只有部分为SSD,提高SSD命中率使得现有资源可以同时处理的请求更多。

因此,对于机房来说,最主要的优化之一就是提高SSD的命中率。我这里做的事情就是,汇总所有机房的SSD命中率,然后在页面上进行展示,以方便观察各种优化措施是否有效。

这里对SSD命中率,有两种计算方式得到的结果:

  • 计算方式一:根据机房的进程数据计算,结果以CGI接口提供,可以按照机房名称、时间等信息去拉取数据,数据按照一分钟进行计算的,拉取时需要分别拉取机房的SSD命中率和总流量(两个接口)。
  • 计算方式二:根据访问的流水日志进行计算,保存的日志文件是十分钟一个,数据组用Spark平台计算出这十分钟日志里面,每个机房的SSD流量、SATA流量,将每十分钟的数据导出到Mysql数据库的表中,机房数量大概是400+。

具体展示需求有:

  • 可以对比两种计算方式的命中率汇总结果和实时曲线。
  • 查询特定机房的命中率实时曲线。
  • 查询特定机型的命中率汇总结果和实时曲线。
  • 查询特定运营商的SSD命中率汇总结果和实时曲线。
  • 查询时间段可选。

下面将对功能模块中主要的部分进行介绍。

2、拉取数据接口数据

上面提到的计算方式一,需要从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需要的内容即可。代码中间省略了一些处理,这里只是说明大概的处理流程。

3、数据本地缓存

在开始进一步设计前端展示界面和编写后端代码时,考虑到数据的本地缓存,主要有以下两个原因:

  • 当需要获取任意多个机房数据时或者汇总数据时,需要在url中加入一个特别长的请求参数,可能会出现url参数超过长度限制,如果拆分成多次url请求,会花费大量时间。
  • 另一种计算方式的结果,是每十分钟一个表存到数据库中的,每张表的数据记录是900多条(机房数量(400+) * 2,2是因为机房里面还分UGC、影视),但是大多数的查询是按天查询,因此需要多表查询,比较耗时。

基于以上两个原因,分别对这两种方式的数据进行汇总缓存,考虑用python脚本,每天定时获取前一天所有机房的数据,汇总保存到一个表中。定时任务用crontab命令,设定每天定时运行一次。

3.1数据接口数据缓存

对于数据接口的数据,获取所有机房列表,然后构造对应的请求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)

3.2 流水日志数据缓存

对于流水日志导出的数据,因为已经是存到数据库表中了,只需要将多个表进行汇总就行了,比较简单:

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

4、下拉选项框处理

开发的功能是嵌入到之前的一个项目中,展示的下拉选项框组件为了一致,直接和前面一样,用的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+,因此只能动态的根据选项变化获取其余两个选项框应该展示的选项框。处理代码比较细节和繁琐,但都是判断和逻辑层面的,这里就不进行说明了。

5、可翻页的曲线图表集合效果

需要做出的效果类似下图:

才用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);    
        }
    }

6、时间段查询功能

保存的表是按照天进行存放的,查询时间段的功能可以选择开始日期和结束日期,查询多天的汇总结果和实时曲线结果。因此需要多表查询:

目前的处理办法是:直接按照每天进行查询,最后将结果进行拼接汇总起来,比较简单。

缺点:多表查询会比较慢,特别是在时间段跨度稍大一点的时候。

优化思路:每天的记录大概是12万,一个月下来是360万,可以加索引优化的字段是时间和机房名称,这个数量级的情况下做好优化,myql还是挺快的。因此后面会考虑将数据库缓存改为按月存放,测试优化前后的速度对比。

7、各种问题汇总

7.1

问题: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!

7.2

问题:xshell经过跳板机连接测试机,要从本机传文件到测试机,执行rz文件失败,文件大小10M左右,在上传一部分后停止并退出显示一行乱码,执行多次,仍然无法成功。

解决办法:中间有控制字符的原因,加参数-e忽略控制字符,rz -e。常用命令方式:rz -bye。

7.3

问题: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")

8、结语

这篇文章主要介绍了在功能模块中的一些关键处理思路和流程,以及一些比较典型的问题,都是比较基础的东西。其中的内容,相信各位大牛还有许多更好的处理方式。水平有限,总结的内容可能存在不足,欢迎大家指正!

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Golang测试技术

本篇文章内容来源于Golang核心开发组成员Andrew Gerrand在Google I/O 2014的一次主题分享“Testing Techniques”,...

3386
来自专栏葡萄城控件技术团队

Asp.Net MVC4入门指南(5):从控制器访问数据模型

在本节中,您将创建一个新的MoviesController类,并在这个Controller类里编写代码来取得电影数据,并使用视图模板将数据展示在浏览器里。 在开...

1775
来自专栏阮一峰的网络日志

浏览器同源政策及其规避方法

浏览器安全的基石是"同源政策"(same-origin policy)。很多开发者都知道这一点,但了解得不全面。 本文详细介绍"同源政策"的各个方面,以及如何规...

4066
来自专栏编程

如何构建你的第一个 Vue.js 组件

协作翻译 原文:How to build your first Vue.js component 链接:https://medium.freecodecamp....

2195
来自专栏软件测试经验与教训

SoapUI测试WS接口实战

3729
来自专栏小瞳的专栏

Python学习之爬虫入门

爬虫是一种用来自动浏览万维网的网络机器人(英语:Internet bot)。其目的一般为编纂网络索引(英语:Web indexing)。网络搜索引擎等站点通过爬...

1532
来自专栏大数据

Python自学笔记——多线程微信文章爬取

# -*- coding: utf-8 -*- """ Created on Tue Dec 26 10:34:09 2017 @author: Andrew ...

1787
来自专栏Gcaufy的专栏

打造小程序组件化开发框架

这篇主要介绍在使用小程序数月之后,结合自己的开发习惯,总结出一套支持组件化的开发框架。希望对大家使用 WePY 有所帮助。

3.3K1
来自专栏SDNLAB

Neutron集成ONOS源码分析

OpenStack Neutron集成ONOS的项目,名为“networking-onos”。目前,在Neutron项目中现已开始从源码中移除诸如SDN Plu...

3306
来自专栏Crossin的编程教室

Python 实战(4):搜一下

一个内容型网站如果不能进行站内搜索,那是会令人抓狂的。都说知乎的搜索不好使,可人家好歹也是有的。所以我们的电影网站至少也得有个搜素框。 那么要如何做呢?HTML...

3209

扫码关注云+社区