利用 python 實現自動搶報告
上了研究生才知道北化的研究生每年需要聽 15 個報告,而且最重要的是這些報告都是算分 數的,而且更重要的是你的分數是和最後的獎學金評定掛鉤的。這也就決定了大家爲了那麼 點報告分數擠教務網都擠破了頭(北化的服務器大家都懂),雖然場面不如開學搶課那麼火 爆,卻需要長時間掛着教務網瘋狂刷新,爲了交換個講座也需要大半夜起來,生怕被別人截 胡。爲了解放生產力,同時熟悉爬蟲技術、神經網絡技術、裝飾器等進階內容,寫了這個爬 蟲來練練手。
1. 聲明
該爬蟲是本人 (Cycoe) 練習 python 編程技巧和神經網絡所編寫,使用造成的任何責任與 本人無關
2. 問題分解
想要順利的拿到搶講座的 session,需要如下的步驟:
- 處理登陸問題,包括處理驗證碼、頁面表單和 cookies
- 獲取報告列表
- 獲取搶報告的地址
- 提交表單數據
3. 框架設計
好的框架應具有良好的可維護性和擴展性,目前正朝着這個方向努力
- 採用交互式的命令行設計,分離 login, robSpeech 等方法
- 將 login, robSpeech, robClass 等方法封裝成 Robber 對象
- 將底層的 requests 封裝成Spider 對象
4. 逐步解決
4.1. 构造 Headers
通過 firefox 或 chrome 的 debug 模式可以查看在訪問網頁時的 request 和 response。 仿照瀏覽器在訪問教務網時提交的 headers 構造如下字典
class Spider(object): @staticmethod def formatHeaders(referer=None, contentLength=None, originHost=None): """ 封裝請求的 headers :param referer: 跳轉標記,告訴 web 服務器自己是從哪個頁面跳轉過來的 :param contentLength: 作用未知 :param originHost: 原始主機地址 :returns: headers 字典 """ headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7', 'Cache-Control': 'max-age=0', 'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded', 'DNT': '1', 'Host': 'graduate.buct.edu.cn:8080', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (X11;Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36', 'Referer': referer, 'Content-Length': contentLength, 'Origin': originHost, }
4.2. 构造请求对象
封裝 request 請求需要的 prepareBody 對象, response = session.send(prepareBody)
,response 就是我們拿到的服務器的響應對象,可通過 response.text
得到網頁內容,
response.status_code
得到狀態碼, response.url
得到響應的地址。
def prepare(self, referer=None, originHost=None, method='GET', url=None, data=None, params=None): """ 生成用於請求的 prepare :param referer: 跳轉標記,告訴 web 服務器自己是從哪個頁面跳轉過來的 :param originHost: 原始主機地址 :param method: 請求方法 in ['GET', 'POST'] :param url: 請求的 url 地址 :param data: 封裝的 post 數據 :param params: post 參數 :return: prepare 對象 """ headers = self.formatHeaders(referer=referer, originHost=originHost) req = Request(method, url, headers=headers, data=data, params=params) return self.session.prepare_request(req) prepareBody = prepare( referer=None, originHost=None, method='GET', url=UrlBean.jwglLoginUrl, data=None, params=None ) response = session.send(prepareBody)
4.3. 請求登錄
請求登錄網址,提交表單數據。根據 response 返回的響應體內容判斷是否登陸成功。
def getVIEWSTATE(self): """ 正則獲取頁面的 __VIEWSTATE :returns: 頁面的 __VIEWSTATE """ VIEWSTATE = re.findall('<.*name="__VIEWSTATE".*value="(.*)?".*/>', self.response.text) if len(VIEWSTATE) > 0: return VIEWSTATE else: return None def getEVENTVALIDATION(self): """ 正則獲取頁面的 __EVENTVALIDATION :returns: 頁面的 __EVENTVALIDATION """ EVENTVALIDATION = re.findall('<.*name="__EVENTVALIDATION".*value="(.*)?".*/>', self.response.text) if len(EVENTVALIDATION) > 0: return EVENTVALIDATION else: return None def login(self): """ 登錄教務網 """ # 在登錄前請求一次登錄頁面,獲取網頁的隱藏表單數據 prepareBody = self.prepare(referer=None, originHost=None, method='GET', url=UrlBean.jwglLoginUrl, data=None, params=None) # 登陸主循環 while True: self.response = self.session.send(prepareBody) self.VIEWSTATE = self.getVIEWSTATE() self.EVENTVALIDATION = self.getEVENTVALIDATION() if self.VIEWSTATE is not None and self.EVENTVALIDATION is not None: break Logger.log("Retrying fetching login page viewState...", level=Logger.warning) reInput = True # 是否需要重新輸入用戶名和密碼 while True: # 輸入用戶名和密碼 if reInput: if Config.checkUserFile(): Config.readUserInfo() else: Config.userName = input("> UserName: ") Config.password = input("> Password: ") reInput = False prepareBody = self.prepare(referer=UrlBean.jwglLoginUrl, originHost=None, method='GET', url=UrlBean.verifyCodeUrl, data=None, params=None) while True: codeImg = self.session.send(prepareBody) # 獲取驗證碼圖片 if codeImg.status_code == 200: break else: Logger.log("retrying fetching vertify code...", level=Logger.warning) with open('check.gif', 'wb') as fr: # 保存驗證碼圖片 for chunk in codeImg: fr.write(chunk) print_vertify_code() verCode = input("input verify code:") # verCode = self.classifier.recognizer("check.gif") # 識別驗證碼 # 構造登陸表單 postData = { '__VIEWSTATE': self.VIEWSTATE, '__EVENTVALIDATION': self.EVENTVALIDATION, '_ctl0:txtusername': Config.userName, '_ctl0:txtpassword': Config.password, '_ctl0:txtyzm': verCode, '_ctl0:ImageButton1.x': '43', '_ctl0:ImageButton1.y': '21', } prepareBody = self.prepare(referer=UrlBean.jwglLoginUrl, originHost=UrlBean.jwglOriginUrl, method='POST', url=UrlBean.jwglLoginUrl, data=postData, params=None) # 獲取登陸 response while True: self.response = self.session.send(prepareBody) if self.response.status_code == 200: break # 根據返回的 html 判斷是否登錄成功 if re.search('用戶名不存在', self.response.text): Logger.log('No such a user!', ['Cleaning password file'], level=Logger.error) print(OutputFormater.table([['No such a user!'], ['Cleaning password file']], padding=2)) Config.cleanUserInfo() reInput = True elif re.search('密碼錯誤', self.response.text): Logger.log('Wrong password!', ['Cleaning password file'], level=Logger.error) print(OutputFormater.table([['Wrong password!'], ['Cleaning password file']], padding=2)) Config.cleanUserInfo() reInput = True elif re.search('請輸入驗證碼', self.response.text): Logger.log('Please input vertify code!', ['Retrying...'], level=Logger.error) print(OutputFormater.table([['Please input vertify code!'], ['Retrying...']], padding=2)) elif re.search('驗證碼錯誤', self.response.text): Logger.log('Wrong vertify code!', ['Retrying...'], level=Logger.error) print(OutputFormater.table([['Wrong vertify code!'], ['Retrying...']], padding=2)) else: Logger.log('Login successfully!', ['UserName: ' + Config.userName, 'Password: ' + Config.password], level=Logger.warning) print(OutputFormater.table([['Login successfully!']], padding=2)) Config.dumpUserInfo() break
4.4. 拿到 session
拿到已登陸的 session 後,搶課和搶報告都是非常方便的,只要按照瀏覽器提交的數據構 造 headers 和表單數據後就可以獲得正常的 response
5. 暗坑總結
- 剛開始抓到的網頁內容中文都是亂碼,後來 google 解決,發現是 python 的編碼和 asp 框架的編碼問題造成的,python 中的編碼問題真的是讓人頭大
- 由於網站的防爬蟲設計,會在 html 源碼中插入很多隱藏的表單數據,如此處的
__VIEWSTATE
和__EVENTVALIDATION
,這兩個是非常重要的參數。否則無法成功登陸 - 兩次訪問之間要有一定的時間間隔,如此處用了一個隨機函數的閉包來獲得隨機時間的 間隔
- 使用裝飾器解決了在訪問搶課網頁前判斷登錄的問題
- 接下來將循環封裝成函數,加入最大循環次數和超時
- 完善邊界檢查和異常處理