讀了這麼多年書,頭一次看到大家搶着做作業,做遲了就白做了。真心佩服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 = "[email protected]" toAddr = "[email protected]" 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。
代碼是寫的比較糟糕的,但是如果你希望得到源代碼參考一下的話,那麼可以給我發郵件,郵箱應該在上面的代碼中有。
備註:
此爲浙江大學計算機學院嵌入式課程實驗報告。