前兩天面試時(shí)被問到績點(diǎn)是多少,但學(xué)校教務(wù)系統(tǒng)不提供績點(diǎn)查詢的功能,那么能不能寫一個(gè)爬蟲程序并計(jì)算出績點(diǎn)呢?答案是肯定的!
1. 準(zhǔn)備
HttpFox插件,是一款http協(xié)議分析插件,分析頁面請(qǐng)求和響應(yīng)的時(shí)間、內(nèi)容、以及瀏覽器用到的COOKIE等。是火狐瀏覽器的插件。谷歌瀏覽器和Safari都有自帶的分析工具,可是感覺太復(fù)雜,沒有這款好用。不過火狐瀏覽器對(duì)學(xué)校的教務(wù)系統(tǒng)兼容性不是很好,我還下載了IE tab插件。
可以非常直觀的查看相應(yīng)的信息。
點(diǎn)擊start是開始檢測(cè),點(diǎn)擊stop暫停檢測(cè),點(diǎn)擊clear清除內(nèi)容。
2. 探究過程
下面就去山東建筑大學(xué)官網(wǎng)登錄到數(shù)字校園綜合信息門戶,看一看在登錄的時(shí)候,到底發(fā)送了那些信息。
先來到登錄頁面,把httpfox打開,clear之后,點(diǎn)擊start開啟檢測(cè):
輸入完賬號(hào)密碼,確保httpfox處于開啟狀態(tài),然后點(diǎn)擊登錄。
這個(gè)時(shí)候可以看到,httpfox檢測(cè)到了好多信息:
那么我們來分析一下這些數(shù)據(jù)
看起來紅框里的兩條數(shù)據(jù)比較有意思,先看看這個(gè)post
PostData中我們看到了比較熟悉的詞,username和password,學(xué)過java web的我們很清楚這段數(shù)據(jù)的含義,點(diǎn)擊登錄后將這用戶名和你們提交到服務(wù)器比對(duì)。
可以看到這里使用get的方式在鏈接上以?的方式顯示的加上了參數(shù),跳轉(zhuǎn)到信息門戶。
我們的post的數(shù)據(jù)就發(fā)送到了這個(gè)地址
http://urpe.sdjzu.edu.cn/loginPortalUrlForIndexLogin.portal
需要的post數(shù)據(jù)是用戶名密碼,也就是說我們需要輸入這兩種數(shù)據(jù)來模擬登錄過程。
進(jìn)入教務(wù)系統(tǒng)后點(diǎn)擊成績查詢,我們看到請(qǐng)求的地址為
http://jwfw1.sdjzu.edu.cn/ssfw/jwnavmenu.do?menuItemWid=1E057E24ABAB4CAFE0540010E0235690
我們整理一下整個(gè)過程的思路。
- POST學(xué)號(hào)和密碼--->然后返回cookie的值
- 發(fā)送cookie給服務(wù)器--->返回頁面信息。
- 獲取到成績頁面的數(shù)據(jù),用正則表達(dá)式將成績和學(xué)分單獨(dú)取出并計(jì)算加權(quán)平均數(shù)。
ok,理順?biāo)悸泛笫O碌木椭挥芯幋a問題了。
3. 實(shí)驗(yàn)
我們先來實(shí)驗(yàn)下是否能夠獲得查詢成績界面的源碼
我們先準(zhǔn)備一個(gè)POST的數(shù)據(jù),再準(zhǔn)備一個(gè)cookie的接收,然后寫出源碼如下:
# coding=utf-8
import urllib
import urllib2
import cookielib
cookie = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
# 需要的POST數(shù)據(jù)
postdata = urllib.urlencode({
'userName': '20140216064',
'password': '*********'
})
# 自定義一個(gè)請(qǐng)求
req1 = urllib2.Request(
url='http://urpe.sdjzu.edu.cn/loginPortalUrlForIndexLogin.portal',
data=postdata
)
req2 = urllib2.Request(
url='http://jwfw1.sdjzu.edu.cn/ssfw/jwnavmenu.do?menuItemWid=1E057E24ABAB4CAFE0540010E0235690'
)
# 訪問登錄鏈接
opener.open(req1)
result = opener.open(req2)
# 打印返回的內(nèi)容
print result.read()
很棒呦,看來跟我們預(yù)期的一樣。
4. 整理數(shù)據(jù)
獲得了成績查詢界面的源碼后我們需要將數(shù)據(jù)進(jìn)行整理,獲得我們想要的數(shù)據(jù),課程名稱,學(xué)分,成績。
將網(wǎng)頁源碼貼到Sublime Text中,方便我們查看源碼
通過查看源碼我們看到從這個(gè)<div>開始到3640行都是關(guān)于成績的代碼,而成績是存放在這個(gè)table標(biāo)簽下。
紅色框中分別為課程名,學(xué)分,以及成績。這些是我們需要抽取出來的數(shù)據(jù)。
看到這里竟然有意外收獲!注意黃色框被注釋的部分,看來學(xué)校的教務(wù)系統(tǒng)有計(jì)算績點(diǎn)的功能的,不知由于何種原因不用呢?
這里是這段程序中最困難的部分,我也踩了很多坑。我參照的博客是使用正則表達(dá)式來抽取想要的信息的,但我對(duì)正則表達(dá)式掌握的并不好,弄了好久也沒寫出合適的表達(dá)式,于是我果斷放棄了使用正則表達(dá)式,采用BeautifulSoup來進(jìn)行信息的篩選。
BeautifulSoup用法參考
# 將內(nèi)容從頁面源碼中提取出來
def deal_data(self, myPage):
soup = BeautifulSoup(myPage)
# 從title屬性為有效成績的標(biāo)簽中獲取所有class屬性為t_con的TAG(tr標(biāo)簽)
trs = soup.find(attrs={"title": "有效成績"}).findAll(attrs={"class": "t_con"})
# 從tr標(biāo)簽中的td標(biāo)簽中獲取需要的信息。下標(biāo)為3,7,8的分別為課程名,學(xué)分,成績
for tr in trs:
for index, td in enumerate(tr.findAll('td')): # enumerate能在for循環(huán)中使用下標(biāo)
if index == 3:
print td.text
elif index == 7:
self.weights.append(td.text.encode('utf8'))
print td.text
elif index == 8:
self.points.append(td.text.encode('utf8'))
print td.text
print
整個(gè)邏輯我簡單說一下,我覺得還可以改進(jìn)。
首先查找title屬性為”有效成績“的標(biāo)簽,通過上文的截圖我們可以知道這是那個(gè)div標(biāo)簽,之后在該div標(biāo)簽中定位class為t_con的tr標(biāo)簽。你也許會(huì)問為什么不直接定位到tr標(biāo)簽,因?yàn)楹竺娴木W(wǎng)頁代碼中還存在class 為t_con的tr標(biāo)簽,但不是我們需要的成績。然后在每個(gè)tr標(biāo)簽下抽取下標(biāo)為3,7,8的標(biāo)簽,這里我是把它存到數(shù)組里了。
接下來就清晰了,先打印成績信息,然后計(jì)算績點(diǎn)。
學(xué)渣一個(gè),績點(diǎn)低請(qǐng)忽略。
源碼
# encoding=utf8
import urllib
import urllib2
import cookielib
import re
import string
from BeautifulSoup import BeautifulSoup
import sys
reload(sys)
sys.setdefaultencoding('utf8')
class SDJZU_Crawler:
# 聲明相關(guān)的屬性
def __init__(self):
self.loginUrl = 'http://urpe.sdjzu.edu.cn/loginPortalUrlForIndexLogin.portal' # 登錄的url
self.resultUrl = 'http://jwfw1.sdjzu.edu.cn/ssfw/jwnavmenu.do?menuItemWid=1E057E24ABAB4CAFE0540010E0235690' # 查詢成績的url
self.cookieJar = cookielib.CookieJar() # 初始化一個(gè)CookieJar來處理Cookie的信息
self.postdata = urllib.urlencode({'userName': '', 'password': ''}) # 登錄需要POST的數(shù)據(jù)
self.weights = [] # 存儲(chǔ)權(quán)重,也就是學(xué)分
self.points = [] # 存儲(chǔ)分?jǐn)?shù),也就是成績
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar))
def sdjzu_init(self):
username = raw_input('請(qǐng)輸入學(xué)號(hào):') # 這里不要用input,二者區(qū)別請(qǐng)自行查詢
password = raw_input('請(qǐng)輸入密碼:')
self.postdata = urllib.urlencode({'userName': username, 'password': password}) # 將用戶名密碼加入到POST中
# 初始化鏈接并且獲取cookie
myRequest = urllib2.Request(url=self.loginUrl, data=self.postdata) # 自定義一個(gè)請(qǐng)求
result = self.opener.open(myRequest) # 訪問登錄頁面,獲取到必須的cookie的值
result = self.opener.open(self.resultUrl) # 訪問成績頁面,獲得成績的數(shù)據(jù)
self.deal_data(result.read())
self.calculate_gpa()
# 將內(nèi)容從頁面源碼中提取出來
def deal_data(self, myPage):
soup = BeautifulSoup(myPage)
# 從title屬性為有效成績的標(biāo)簽中獲取所有class屬性為t_con的TAG(tr標(biāo)簽)
trs = soup.find(attrs={"title": "有效成績"}).findAll(attrs={"class": "t_con"})
# 從tr標(biāo)簽中的td標(biāo)簽中獲取需要的信息。下標(biāo)為3,7,8的分別為課程名,學(xué)分,成績
for tr in trs:
for index, td in enumerate(tr.findAll('td')): # enumerate能在for循環(huán)中使用下標(biāo)
if index == 3:
print td.text
elif index == 7:
self.weights.append(td.text.encode('utf8'))
print td.text
elif index == 8:
self.points.append(td.text.encode('utf8'))
print td.text
print
# 計(jì)算績點(diǎn),如果成績還沒出來,就不算該成績,
def calculate_gpa(self):
point = 0.0 # 成績
weight = 0.0 # 學(xué)分
for i in range(len(self.points)):
if self.points[i].isdigit() and (self.weights[i] != 0):
point += string.atof(self.points[i]) * string.atof(self.weights[i]) # 成績*學(xué)分累加求和
weight += string.atof(self.weights[i]) # 學(xué)分累加求和
print "績點(diǎn)為:"
print point / weight # 輸出績點(diǎn) 值成績*學(xué)分累加求和 / 學(xué)分累加求和
# 調(diào)用
mySpider = SDJZU_Crawler()
mySpider.sdjzu_init()