读了这么多年书,头一次看到大家抢着做作业,做迟了就白做了。真心佩服fm的机制设计。有人说,刷fm的网站,要像刷微博一样勤快,否则迟了就没有机会了。对于这种机械性操作,作为CS的人,自然想让它自动化。于是就有了下面的设计。
总体设计:
整体结构:
核心流程图:
总体设计考虑:
- 运行平台,希望它哪个平台都能跑,最重要的还是能在树莓派这个linux的系统上跑。因为,我不想7*24开着笔记本,但是,树莓派嘛,随便它开着吧。
- 计算机语言的选择:C,C++是我常用的,但是,我得找好多库,否则这事没法干。C#,java,C#没用过,而且又不能在linux上跑。java用过一些,但是知道不多。因为,要在树莓派上跑,java太重量级了。这件事情可能还是脚本做比较好。python,pearl,ruby,之前只用过一段时间的python,还是相当不错的。而且各种库,干什么事都省力了。
- 之前想过,如果出现了新题目,用其他方式来提醒。比如:在PC端写一个软件,让树莓派来触发这个软件,然后,在桌面上,给我提示。又或者是通过发送飞信的方式来提醒。但是,他们都不如发邮件来得省力。
- 除了能够收到邮件提醒之外,我还希望能够将提交数目可视化出来,从而得到一些有趣的结论。也就是这里的Construct web&graphs.
模块设计:
Web Login&Downloader 模块:
实现方法:
因为,此处需要用到http协议,所以,寻找相应的库来完成是最方便的。我使用的是httplib2,一个号称要取代python的标准库httplib。不管是POST还是GET,亦或是加上cookie机制,都是简单好用的,下面的代码就是Login的代码。
这样就得到了login之后的cookie了,以后只要将这个cookie传给web server,它就知道你是登录了的那个了。然后,就可以去下载网页了。def Login(self): #Login. url = self.baseUrl+'login.php' body = {'passwd':'***********','user':'***********'}#****处需要填入相应的用户名和密码。 headers = {'Content-type':'application/x-www-form-urlencoded'} response,content=self.http.request(url,'POST',headers=headers,body=urllib.urlencode(body)) self.cookie=response['set-cookie']
def _GetOnePage(self,url): headers = {'Content-type':'application/x-www-form-urlencoded'} headers['Cookie'] = self.cookie response, content = self.http.request(url, 'GET', headers=headers)
Html Parser 模块:
设计考虑:
- 不得不说,这是有重新造轮子的嫌疑。在我写这个模块之前,就认为一定会有这样一个库非常完美的完成了这一切。后来,我发现python标准库里面就有。不过,我这个为的不是实用,更多的是训练。从实现效率考量,我不应该用python去实现,应该用C或者C++去实现,更加不应该实用费时的正则表达式去识别标签以及其他东西。但是,从训练考量,因为我一直没用过正则表达式,这次是一次尝试。所以也就有了以下代码。
实现方法:
实现思路也很简单,先用正则表达式去识别出start tag 和 end tag,然后将它们看成左右括号,用堆栈的方法进行括号匹配。这里有点小麻烦是,有些tag它只有开始没有结束比如:<br>。一种方案是说,那就将这些特殊的记住,然后特殊处理。另一种方案是说,如果,我在匹配end tag时出现错误(也就是当前栈顶的那个tag,不是end tag对应的start tag),那就认为当前栈顶的tag是一个特殊tag只有开始没有结束。如果,网页是完全按照规范来做的话,这样就够了。但是,现实世界比较复杂。就我parse的那个网页,它就不符合规范。平白无故多出了几个end tag。然后,我再进行了一些错误处理。总共代码也就150+行,代码写得也就一般般,没做太多测试,也没做错误处理。
import re class HtmlParser: def __init__(self): self.rootDocObject=None self.html=None def Parse(self,html): self.html=html self.rootDocObject=ParseHtml(self.html); return self.rootDocObject def GetRootDocObject(self): return self.rootDocObject def ShowHtml(self): return self._ShowHtml(self.rootDocObject,0) def _ShowHtml(self,docObject,level): parentTag = None if(docObject.GetParent() is not None): parentTag = docObject.GetParent().GetTag() else: parentTag = "I'm the root!" print(level*"\t"+"TagName:"+docObject.GetTag()+";Attr:"+str(docObject.GetAttr()) \ +";ParentName:"+parentTag) for i in docObject.GetBody(): if(isinstance(i,basestring)): print((level+1)*"\t"+i) else: self._ShowHtml(i,level+1) return None class DocObject: def __init__(self,tag=None,attr=None,parent=None): self.tag=tag self.attr=attr #attr should be a dict. self.parent=parent #parent should be a DocObject. self.body=[] #should be a list.element type is either string or DocObject. def GetTag(self): return self.tag def GetAttr(self): return self.attr def GetParent(self): return self.parent def GetBody(self): return self.body; #def FindText(self,text,pos): # for i in self.body[pos:]: # if isinstance(i,basestring) && i==text: # return i # return None def FindTag(self,text,pos): index=pos for i in self.GetBody()[pos:]: if (not isinstance(i,basestring)) and i.GetTag()==text: print(text) return (i,index) index+=1 return None def ParseAttr(str): """The str should be striped the tags""" reModule = re.compile(r"((?P<key1>\w+) *= *['\"](?P<value1>[-=&_%@!#,$+?:.; \w/]*)['\"])|((?P<key2>\w+) *= *(?P<value2>[-=&_%@!#,$+?:.;\w/]+))") iter=reModule.finditer(str) #print(str) attrDict = {} for i in iter: if i.group('key1') is not None: attrDict[i.group('key1')]=i.group('value1') else: attrDict[i.group('key2')]=i.group('value2') return attrDict def ParseHtml(html): #stack stack=[] #The root DocObject rootDocObject = DocObject("root",None,None) stack.append(rootDocObject) #regular expression reTag = re.compile(r"(?P<all>(<(?P<startTagName>\w+)(?P<attr>.*?)>)|(</(?P<endTag>\w+)>)|(<!--(?P<comment>[\s\S]*?)-->))") normalTextStart = 0 pos = 0 reMatch = reTag.search(html,pos) while(reMatch is not None): #append the text. normalTextEnd = reMatch.start('all') text = html[normalTextStart:normalTextEnd] if(len(str.strip(text))!=0): #check whether text is consist of space. stack[len(stack)-1].GetBody().append(text); normalTextStart = reMatch.end('all') #deal with the tag. tagName = None attr = None if(reMatch.group('startTagName') is not None): #It is a start tag. tagName = reMatch.group('startTagName') attr = ParseAttr(reMatch.group('attr')) docObject = DocObject(tagName,attr,stack[len(stack)-1]) stack[len(stack)-1].GetBody().append(docObject); stack.append(docObject) elif(reMatch.group('endTag') is not None): #It is a end tag.Go to the stack,and find the start tag. tagName = reMatch.group('endTag') findIt = False for i in stack: if(i.GetTag()==tagName): findIt=True break if findIt: if len(stack)==1: print("Error:Pos:"+str(pos)+";tagName:"+tagName) return None #Error here. docObject=stack.pop() while(docObject.GetTag()!=tagName): docObjectParent=stack[len(stack)-1] docObjectBodyLen = len(docObject.GetBody()) docObjectParentLen = len(docObjectParent.GetBody()) docObjectParent.GetBody()[docObjectParentLen:docObjectParentLen+docObjectBodyLen]=\ docObject.GetBody() del docObject.GetBody()[0:docObjectBodyLen] if len(stack)==1: print("Error:Pos:"+str(pos)+";tagName:"+tagName) return None #Error here. docObject=stack.pop() else: #find a little error. print("A little Error:Find the end tag but without start one." \ +"Pos:"+str(pos)+";tagName:"+tagName) elif(reMatch.group('comment') is not None): #It is comment tag. tagName = "comment" #reMatch.group('comment') attr = None #ParseAttr(reMatch.group('attr')) docObject = DocObject(tagName,attr,stack[len(stack)-1]) stack[len(stack)-1].GetBody().append(docObject) docObject.GetBody().append(reMatch.group('comment')) else: #it must be a bug. return None pos = reMatch.end('all') reMatch = reTag.search(html,pos) return rootDocObject
Send Email模块:
实现方法:
这次是使用python里的标准库,也是十分简单,就可以完成发邮件的功能。此处碰到了一个怪事,如果我不用ssl的方法发送邮件,速度会很慢10s左右,使用了ssl,就1s差不多了。
def mail(content): import smtplib from email.mime.text import MIMEText fromAddr = "zju3100102665@126.com" toAddr = "786752376@qq.com" msg = MIMEText(content,'html') msg['Subject']="ES Assignments Report" msg['From']=fromAddr msg['To']=toAddr smtp = smtplib.SMTP_SSL("smtp.126.com") smtp.set_debuglevel(4) smtp.login('zju3100102665','*********')#***为密码 smtp.sendmail(fromAddr,toAddr,msg.as_string()) smtp.quit()
Store Difference 模块:
设计考虑:
- 在存储从网站上抓取下来的数据时,我并没有将所有数据都存储下来,那样子太大了,而且大部分都是无用数据。我只在网站上数据更新后,在本地保存一份。
实现方法:
基本就是文件和字符串处理的事情。文件的结构如下图所见。
Construct Web&Graphs 模块:
设计考虑:
- 对于构建web 网页,那只有一种办法也是最简单的就是直接打开一个文件然后写进去就可以了。但是,如何创建一个graph呢?这个至少有两种方案。第一种是找一个图形库,然后直接用textout,drawline之类的方法画出来。还有一种方案是生成一个脚本,然后让这个脚本去画出图形来。从效率上来说,一般是第一个高,同样这也是最常见的方法。但是,第二个的好处是灵活。能够完成第二种功能的,有gnuplot,maple,matlab等,如果,是最终要在网页上显示的话,javascript也算一个。它们一般需要数据文件,也需要脚本文件。类似的还有graphviz,只是它只要数据,不需要脚本,但是只能画图论中的图。此处,我选择的是gnuplot。显然,matlab和map等数学软件都太重量级了。javascript也是一种比较好的选择,使用d3库的话,图形结果应该是比较漂亮而且自由度更高。但是,程序复杂度提高了好些,为了不这么麻烦。最终选择了gnuplot。gnuplot这个东西是在1986年出现的,非常老的一个工具,主要流行在科学工作者之间。他可以将结果输出到屏幕,png,svg,convas等上面,所以灵活度大大增加。此处,我是将结果输出到了svg文件中,方便在浏览器中显示。
实现方法:
实现上,我需要输出html文档,将待会生成的图片嵌入到里面去。还有就是生成gnuplot脚本以及相应的数据。最后执行脚本。下面给一个数据和脚本的样例吧。
118.dat
118.gp2013-03-15#14:24 10 0 2013-03-15#15:31 10 0 2013-03-15#16:01 10 1 2013-03-15#16:21 10 3 2013-03-15#16:42 10 5 2013-03-15#16:52 10 6 2013-03-15#17:52 10 8 2013-03-15#18:02 10 9 2013-03-15#18:42 10 10
结果:set terminal svg set output "118.svg" set title "Code:118" set timefmt "%Y-%m-%d#%H:%M" set format x "%m-%d\n%H:%M" set xdata time set xlabel "Date" set ylabel "Times" plot "118.dat" using 1:2 with linespoints title "submits","118.dat" using 1:3 with linespoints title "submited" lt 4
系统结果:
总体效果:
效果十分不错,这个系统是在2周前开始运行的,期间还是帮助我抢到了3个左右的实验。同时也搜集到了一些有趣的数据,接下来我们会看到。
邮箱部分效果:
网页部分效果:
数据分析:
- 介绍一下这个图怎么看。首先,X轴是时间,横轴上的标签比如03-15\n15:30指的是3月15日,15点30分。纵轴提交的此处。红色的折线表示总的提交数的变化。天蓝色的折线表示现在提交数目的变化。图中只是画出了,从题目出现到它提交满并且不再变化的时候。
- 完成速度最快的一次记录:1:40分钟完成。
- 完成的最曲折的一次:fm or ta 很认真啊,半夜在看报告,更有同学半夜在改报告。
- 之后若是有好玩的图,则持续更新。如果,你和我在同一层楼的话,你应该可以打开这个http://222.205.46.240/es/来看看其他图。
后记:
前后断断续续写了好几天,直到近日才将其写完。总共代码只有700行,但是写了总共5天差不多。也就是差不多在50小时左右。程序说难也不难,主要是对python以及它的库,并不熟悉,经历了比较长的学习时间,也是出了很多难缠的bug。尤其是在对象引用这块。
通过这样一次训练,还是学习到了很多东西。比如:python,正则表达式,网络编程,略略的使用了一下gnuplot。
代码是写的比较糟糕的,但是如果你希望得到源代码参考一下的话,那么可以给我发邮件,邮箱应该在上面的代码中有。
备注:
此为浙江大学计算机学院嵌入式课程实验报告。