2015新年伊始,校园网开始收费。明光村职业学院免费上网的岁月一去不返。我等广大群众感慨之际,突然发现流量用得如此之快。身边的朋友也开始渐渐对看视频心有余悸,对实时流量(此处仅指收费的v4校外流量)的关注也日渐升级。校园网流量计的想法便这样出现了。
p.s. 鉴于开发过程中,有队友的加入,所以下文按照时间顺序 而非严格的逻辑顺序 进行,望读者谅解。
背景话毕,如何分解我们的需求呢?要想让流量数据可视化,当然又必不可少的两点:a)数据 b)可视化。具体说来便是:
这么一想,解决方案就出来了。
最初设计 考虑到用户对流量实时性了解的需要,决定用动态的折线图呈现最终效果。折线图能显示最近半小时内校园网流量的变化,每约30s采集一次数据。
爬虫程序 既然要收费,学校必定会给学生查看流量的渠道。netaccount.bupt.edu.cn 需要离线后才能查看,上网注销窗 是最好的选择,每次刷新即可显示当前上线时间,使用流量,剩余网费,很直观;最重要的是每次刷新都能得到实时数据。就爬它了!
写爬虫程序的语言可以有很多,从Java到python到php到js甚至windows shell都可以轻松完成这个简易的爬虫任务:爬取html页面,匹配流量字段,存储。
在爬之前,我们需要知道流量值藏在html页面代码的什么地方。打开开发者工具,代码一览无余。
从flow的大小来看,存储的单位是KB,没关系/1024/1024就行了。准备工作做完后,可以开始码代码了。python比较好上手,我们暂时选择python写爬虫。不说废话,直接上干货。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import urllib2import cookielibimport reimport timeimport jsonclass GATE_Spider : def __init__ (self ): self.loginUrl = 'http://10.3.8.211' self.cookieJar = cookielib.CookieJar() self.flow = [] self.flow_json = [] self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar)) def flow_init (self ): for x in range (3 ): myRequest = urllib2.Request(url = self.loginUrl) result = self.opener.open (myRequest) self.deal_data(result.read().decode('gbk' )) self.flow_json = json.dumps(self.flow,indent=2 ,separators=(',' ,':' )) self.cal_data(self.flow); time.sleep(5 ) def deal_data (self,myPage ): myFlow = re.search(r';flow=\'\d+' ,myPage) myFlow = re.search(r'\d+' ,myFlow.group(0 )) self.flow.append({'flow' :float (myFlow.group(0 ).encode('gbk' ))/1024 ,'time' :time.time()}) def cal_data (self,items ): with open ('./new.dat' ,'w' ) as f: f.write(self.flow_json) mySpider = GATE_Spider() mySpider.flow_init()
思路很清晰,因为登录工作在程序运行前已经完成,cookie已经存在本地,每次刷新只是简单地GET请求而已。所以在初始化后,只需模拟GET请求,拿到回复后,匹配第一次出现的flow和fee即可,以标准的json格式存储在.dat文件里,每过30s循环一次。
p.s.有意思的是,上网注销窗那里的MB竟然是字符串拼接拼出来的,直接/1024就可以得到的结果却都两个大圈子才拼出来,不知目的在哪里。。。
好了,测试之后,文件里已经把流量,网费,时间都按顺序储存好了。怎么和前端数据可视化互动呢?最好的方式当然是选择js,用网页的形式呈现。不过,为了保证实时性,js需要不间断地向后台发起请求。整个流程应该是由js端驱动的。而我对python作网站后台并不熟悉,无奈之下,只能暂时放弃python爬虫这个主意,选择php爬虫。不过,思路上大同小异,没过多久,php的爬虫后台端就搞定了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 <?php header ("Content-type: text/json" );class Spider { public $flow ; public $fee ; public $time function __construct ( ) { $this ->flow = 0 ; $this ->fee = 0 ; $this ->time = 0 ; } function getURLcontent ($url ) { $data = "" ; $curl = curl_init (); curl_setopt ($curl , CURLOPT_URL, $url ); curl_setopt ($curl , CURLOPT_HEADER, 0 ); curl_setopt ($curl , CURLOPT_RETURNTRANSFER, 1 ); $data = curl_exec ($curl ); $this ->time = time () * 1000 +3600 *8000 ; curl_close ($curl ); return $data ; } function simplifyURLcontent ($content ) { $flow = 0 ; $fee = 0 ; preg_match ('/flow=\'([\S\s]*)\';fsele=\d+;fee=([\S\s]*)\';xsele/' , $content , $matches ); if (!$matches ) return false ; else { $this ->flow = intval ($matches [1 ])/1024 /1024 ; $this ->fee = intval ($matches [2 ]); return true ; } } function deliverFlow ( ) { $ret = array ($this ->time, $this ->fee, $this ->flow); echo json_encode ($ret ); } function workFlow ( ) { $ret = $this ->getURLcontent ("http://10.3.8.211" ); $isGet = $this ->simplifyURLcontent ($ret ); if ($isGet ) $this ->deliverFlow (); return false ; } } $gate = new Spider (); $gate ->workFlow (); ?>
这样,js每次发送.ajax请求时,php就可以返回前端需要的数据了。
这么来看,后台爬虫端应该就这么多了。
数据可视化 好的,现在我们已经可以拿到所需的数据了。关键是怎么去呈现它。最好能有现成的工具来好好利用收集到的数据。前人怎么会没考虑到这点呢,js charts用SVG等方法渲染出来的图表足够用了。这里,我选择了Highcharts。Highcharts的文档很丰富,API文档也很充分,所以有一定web基础的人很容易上手。
Highcharts提供了很多种图表类型,考虑到实时性的需要,我们选择最基础的line类型。这里不再介绍Highcharts的基本信息,感兴趣的可以去它的官网 查看。既然后台php已经把json标准化的数据拿过来了,就看Highcharts这边怎么用了。
由于这里各点数据是动态更新的。这里Highcharts的Chart对象里,series需要动态更新。这里写个简单的ajax,向后台index.php请求数据,根据上面的php程序,将得到一个[time,fee,flow]的json类型数据point,第一个自然是x轴数据,第二个是第二条y轴数据,第三个是第一条y轴数据。这里我们用series的addPoint方法把数据新增进去即可。由于图只显示近半小时数据,我们姑且视作50个点,因此这里还要判断点数是否超过50,然,则平移。因此addPoint的第二个第三个参数分别是TRUE和判断是否平移的Boolean值。在得到数据后,setTimeout来实现每约30s请求一次数据。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $.ajax ({ url : 'index.php' , success : function (point ) { var series = chart.series [0 ], series_f = chart.series [1 ], shift = series.data .length > 50 , shift_f = series_f.data .length > 50 ; chart.series [0 ].addPoint ([point[0 ],point[2 ]], true , shift); chart.series [1 ].addPoint ([point[0 ],point[1 ]], true , shift_f); setTimeout (requestData, 20000 ); }, cache : false });
之后的就比较简单了。设置好前端表格的格式就可以看到炫酷的效果了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 $(document ).ready (function ( ) { chart = new Highcharts .Chart ({ chart : { renderTo : 'container' , defaultSeriesType : 'spline' , events : { load : requestData } }, title : { text : 'Campus Network Live Flow Data(ipv4 only)' }, credits :{ enabled :false }, xAxis : { type : 'datetime' , tickPixelInterval : 40 , maxZoom : 50 * 1000 }, yAxis : [{ minPadding : 0.2 , maxPadding : 0.2 , title : { text : 'Flow (GB)' , margin : 30 } },{ opposite :true , minPadding : 0.2 , maxPadding : 0.2 , title : { text : 'Balance(yuan)' , margin : 30 } }], series : [{ name : 'FLow data' , data : [] },{ yAxis : 1 , name : 'Balance data' , ` data: [] }] }); });
嗯~ 看上去还不错。
p.s. 经测试,在后台取的时间不经处理会早8个小时,这是由于UTC和时区的原因,在后台额外加上整8个小时即可。
历史数据 在实时数据做完后,有朋友参与了进来。经过讨论,我们认为做一个历史性数据展示窗口也是必要的。毕竟,不是所有人都愿意开着网页等着数据更新。后台悄悄地采集数据会更友好。于是,下一步开始了。
有了前面的基础后,后面的开发要快许多。不过我们遇到了一个现实的问题。既然我们的小工具是面向不一定懂编程的用户的,那么php做后台就变得有些不现实。所以这里我们决定用windows的shell来写,并推广到不同操作系统。由于篇幅限制,我们将在下篇日志里介绍这种方法。这里仅对前段部分进行讲解。
思路还是类似的,后台采到的数据用csv格式存在.dat或诸如此类的文件里。Highcharts需要读入其中所有的数据,并存入到series的data域里,一次请求,一次性完成,这是不同于上个功能的。
依旧,在Highcharts的chart对象实体化前,得对变量赋值。重点当然在对csv格式的文件读取中。这里用jQuery的get方法,虽然js跨域访问陌生网站很困难,访问本地资源还是没问题的。读入数据后,以’\n’为分隔得到每个点,这里要初始化一个series对象,预存两条数据线的name,yAxis属性。然后.each函数里,split数据by ‘ , ‘, push到series的data域里即可(注意,这里最好对值parseFLoat处理一下较好)。最后将两组series push到chart的options里(注意,需要push两次,因为是两组数据)。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $.get ('data.csv' , function (data ) { var lines = data.split ('\n' ); var series = [{ name : 'Flow Data' , data : [] },{ name : 'Balance Data' , yAxis : 1 , data : [] }]; $.each (lines, function (lineNo,line ) { var items = line.split (',' ); series[0 ].data .push ([parseFloat (items[0 ]),parseFloat (items[2 ])]); series[1 ].data .push ([parseFloat (items[0 ]),parseFloat (items[1 ])]); }); options.series .push (series[0 ]); options.series .push (series[1 ]); var chart = new Highcharts .Chart (options); });
配合上预先设定好的Highcharts其他属性,综合成一个options初始化一个Highcharts.chart对象即可完成任务。再稍微处理一下前端,将两个功能组合在一起,加上css样式和一个简单的收起js,得到了半成品.
整体代码js如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 function requestData ( ) { $.ajax ({ url : 'index.php' , success : function (point ) { var series = chart.series [0 ], series_f = chart.series [1 ], shift = series.data .length > 50 , shift_f = series_f.data .length > 50 ; chart.series [0 ].addPoint ([point[0 ],point[2 ]], true , shift); chart.series [1 ].addPoint ([point[0 ],point[1 ]], true , shift_f); setTimeout (requestData, 20000 ); }, cache : false }); } var options = { chart : { renderTo : 'container_h' , defaultSeriesType : 'spline' , zoomType : 'x' }, title : { text : 'Campus Network History Flow Data(ipv4 only)' }, credits :{ enabled :false }, xAxis : { type :'datetime' , minRange : 150000 }, yAxis : [{ minPadding : 0.2 , maxPadding : 0.2 , title : { text : 'Flow (GB)' , margin : 30 } },{ opposite :true , minPadding : 0.2 , maxPadding : 0.2 , title : { text : 'Balance (yuan)' , margin : 30 } }], series : [] }; $.get ('data.csv' , function (data ) { var lines = data.split ('\n' ); var series = [{ name : 'Flow Data' , data : [] },{ name : 'Balance Data' , yAxis : 1 , data : [] }]; $.each (lines, function (lineNo,line ) { var items = line.split (',' ); series[0 ].data .push ([parseFloat (items[0 ]),parseFloat (items[2 ])]); series[1 ].data .push ([parseFloat (items[0 ]),parseFloat (items[1 ])]); }); options.series .push (series[0 ]); options.series .push (series[1 ]); var chart = new Highcharts .Chart (options); }); $(document ).ready (function ( ) { chart = new Highcharts .Chart ({ chart : { renderTo : 'container' , defaultSeriesType : 'spline' , events : { load : requestData } }, title : { text : 'Campus Network Live Flow Data(ipv4 only)' }, credits :{ enabled :false }, xAxis : { type : 'datetime' , tickPixelInterval : 40 , maxZoom : 50 * 1000 }, yAxis : [{ minPadding : 0.2 , maxPadding : 0.2 , title : { text : 'Flow (GB)' , margin : 30 } },{ opposite :true , minPadding : 0.2 , maxPadding : 0.2 , title : { text : 'Balance(yuan)' , margin : 30 } }], series : [{ name : 'FLow data' , data : [] },{ yAxis : 1 , name : 'Balance data' , data : [] }] }); });
剩下的部分,我们在下篇文章里继续。下期见 ~