查成績,算分數,每年的綜合測評都是個固定的過程,作為軟件開發者,這些過程當然可以交給代碼去做,通過腳本進行網絡請求獲取數據,然后直接進行計算得到基礎分直接填表就好了,查成績再手動計算既容易出錯也繁瑣,所以本篇的內容就是開發一個爬蟲腳本取抓取成績表,至於綜合測評計算,這個沒什么意義這里就不說了,分數都有了就都夠了。
我們的目的就是通過編寫腳本,模仿瀏覽器進行請求獲取源碼,再進行解析本地化(或者直接計算)
要抓取到數據,其實方案不止一種,這里會介紹兩種不同的方案,達到同樣的目的:
- 模仿瀏覽器進行請求(速度快)
- 操作瀏覽器進行請求(速度慢)
先說第一種,這種方案是普遍的爬蟲技術,因為爬取的內容不多,對速度要求也不夠,所以就是很簡單的一個爬蟲過程:
- 分析請求
- 模仿請求
對於普通的校園網,一般不做流量限制,所以就算請求頻繁,也基本不用擔心IP被封禁,所以編寫爬蟲代碼可以不用太過擔心。先說我所在學校的校園網,是杭州方正軟件公司開發的。
① 分析請求
分析請求很簡單,就是使用瀏覽器進行請求,然后分析每個請求所發送和接收的信息,這里最簡單應該是使用chrome的開發者模式(F12打開)
輸入用戶名和密碼,勾選已認真閱讀,接着點擊登陸,這樣右邊的網絡窗口中會檢查到所有的網絡請求,我們只需要找到對應登陸的一個(這里會帶有表單):
這個時候,我們可以通過一些測試工具,嘗試進行請求對應的這個地址,並且把表單提交上去試試登陸能否成功,如果成功的話,腳本也就可以模擬這個請求,這里用的是chrome商店的一個工具Postman,用法很簡單:
登陸成功之后,我們再進行查詢成績:
這里可以看到這次得到了兩個新的請求(上圖紅框的前兩個)
仔細觀察會發現,第一個請求頭中的Referer指向的是第二個請求的地址,所以可以知道,第二個請求是先於第一個請求發送的。其次,我們發現這個請求中也有表單。
再看第二個請求:
它的Referer指向第三個請求,而這個第三個請求實際上登陸成功之后,就已經存在了,它就是請求到主界面的,而這個請求的類型是Get,所以也表明,第三個請求沒有傳遞任何信息給這個請求。
整理可以知道,流程是這樣的:
登陸成功后跳轉:http://202.192.72.4/xs_main.aspx?xh=2013034743130
點擊查詢成績按鈕請求:http://202.192.72.4/xscj_gc.aspx?xh=2013034743130&xm=%B3%C2%D6%BE%B7%AB&gnmkdm=N121605 (Get)
點擊查詢在校成績請求:http://202.192.72.4/xscj_gc.aspx?xh=2013034743130&xm=%u9648%u5fd7%u5e06&gnmkdm=N121605 (Post)
所以,我們先來模擬第二個,這個請求是Get類型,所以直接請求即可,但是會發現請求會失敗,原因是服務器不能知道我們已經進行登陸了:
所以最先想到的辦法是帶上第一個請求得到的Cookie,但是也是不行,這個時候要用到上面說的Referer標識,這個標識會告訴服務器請求來源,因為登陸成功會在服務器進行登記,這個標記會讓服務器知道請求來源於登陸成功的賬號:
此時請求返回正常,我們在源碼中可以發現有兩個隱藏的<input>標簽:
這兩個標簽傳遞的,其實是第三個請求的參數,這個時候,模擬第三個請求,並且添加對應的Referer(第二個請求的URL),會發現請求也成功了:
這個請求中的url中的一個參數xm被我更改為1了,原本使用的是一種unicode加密編碼,把用戶名編碼過去了,但是實際上這個參數並沒有實際意義,%u的格式會破壞Python程序,所以這里直接改成1了。
② 模仿請求
請求分析完畢,就可以開始寫代碼了:
用到的包:
1 import requests, xlwt, os 2 from bs4 import BeautifulSoup
登錄:
1 def login(s, number, password): 2 print '正在登錄賬號:'+number 3 url = 'http://202.192.72.4/default_gdsf.aspx' 4 data = {'__EVENTTARGET': 'btnDL', 5 'TextBox1': number, 6 'TextBox2': password, 7 '__VIEWSTATE': '/wEPDwULLTExNzc4NDAyMTBkGAEFHl9fQ29udHJvbHNSZXF1aXJlUG9zdEJhY2tLZXlfXxYBBQVjaGtZRIgvS19wi/UKxQv2qDEuCtWOjJdl', 8 'chkYD': 'on', 9 '__EVENTVALIDATION': '/wEWCgKFvrvOBQLs0bLrBgLs0fbZDAK/wuqQDgKAqenNDQLN7c0VAuaMg+INAveMotMNAuSStccFAvrX56AClqUwdU9ySl1Lo85TvdUwz0GrJgI='} 10 s.post(url, data) 11 return s.cookies
登錄操作沒有給后面的請求傳遞任何參數,這里的Cookies不是必須的,但是登錄是必須的,這樣告訴服務器我們后面的請求才是合法的。
點擊查詢成績按鈕:
1 def get_data_for_grade(s, number, password): 2 url = 'http://202.192.72.4/xscj_gc.aspx?xh=' + number + '&xm=%B3%C2%D6%BE%B7%AB&gnmkdm=N121605' 3 referer = 'http://202.192.72.4/xs_main.aspx?xh=' + number 4 cookies = login(s, number, password) 5 response = s.get(url=url, headers={'Referer': referer}, allow_redirects=False, cookies=cookies) 6 source = response.text 7 soup = BeautifulSoup(source, 'html.parser') 8 view_state = soup.find('input', attrs={'id': '__VIEWSTATE'})['value'] 9 event_validation = soup.find('input', attrs={'id': '__EVENTVALIDATION'})['value'] 10 states = {'view_state': view_state, 'event_validation': event_validation, 'cookies': cookies, 'origin': url} 11 return states
第五行隊請求設置Referer,接着通過BeautifulSoup解析源碼得到兩個隱藏的<input>標簽里面value值,第三個請求要用到。
查詢所有成績請求:
1 def check_info(s, number, password): 2 url = 'http://202.192.72.4/xscj_gc.aspx?xh=' + number + '&xm=1&gnmkdm=N121605' 3 states = get_data_for_grade(s, number, password) 4 print '登錄成功,正在拉取成績' 5 data = { 6 '__VIEWSTATE': states['view_state'], 7 'ddlXN': '', 8 'ddlXQ': '', 9 'Button2': '', 10 '__EVENTVALIDATION': states['event_validation'] 11 } 12 response = s.post(url, data=data, cookies=states['cookies'], headers={'Referer': states['origin']}, 13 allow_redirects=False) 14 return response.text
得到成績單源碼之后,就可以進行解析了,這里解析存放到xls表格中:
1 def writeToFile(source): 2 print '正在寫入文檔' 3 wb = xlwt.Workbook(encoding='utf-8', style_compression=0) 4 soup = BeautifulSoup(source, "html.parser") 5 span = soup.find('span', attrs={'id': 'Label5'}) 6 sheet = wb.add_sheet('成績單', cell_overwrite_ok=True) 7 table = soup.find(attrs={'id': 'Datagrid1'}) 8 lines = table.find_all('tr') 9 for i in range(len(lines)): 10 tds = lines[i].find_all('td') 11 for j in range(len(tds)): 12 sheet.write(i, j, tds[j].text) 13 try: 14 os.remove(span.text + '.xls') 15 except: 16 pass 17 wb.save(span.text + '.xls')
最后遍歷學號進行爬取,這里只爬取默認賬號密碼的成績:
1 for i in range(1, 55): 2 num = '2013034743001' 3 s = requests.session() 4 try: 5 if i <= 9: 6 writeToFile(check_info(s, num[:12] + str(i), num[:12] + str(i))) 7 else: 8 writeToFile(check_info(s, num[:11] + str(i), num[:11] + str(i))) 9 except: 10 pass 11 s.close()
第二種方案,是通過模擬瀏覽器來進行登錄,點擊按鈕等操作獲取成績,這里用到的是自動化測試框架Selenium。
這種方案的優點是我們不需要像第一種那樣要去分析請求,只需要告訴瀏覽器要怎么做就行了,但是缺點是速度慢。
1 # -*- coding: utf-8 -*- 2 from selenium import webdriver 3 from selenium.webdriver.common.by import By 4 from selenium.webdriver.support.wait import WebDriverWait 5 from selenium.webdriver.common.action_chains import ActionChains 6 from selenium.webdriver.support import expected_conditions as EC 7 from selenium.common.exceptions import NoSuchElementException 8 from selenium.common.exceptions import NoAlertPresentException 9 from bs4 import BeautifulSoup 10 import xlwt 11 import os 12 13 14 class Script(): 15 def setUp(self): 16 self.driver = webdriver.Chrome() 17 self.driver.implicitly_wait(10) 18 # self.driver.maximize_window() 19 self.base_url = "http://202.192.72.4/" 20 self.verificationErrors = [] 21 self.accept_next_alert = True 22 23 def test_jb(self, num): 24 driver = self.driver 25 driver.get(self.base_url + "/default_gdsf.aspx") 26 driver.find_element_by_id("TextBox1").clear() 27 driver.find_element_by_id("TextBox1").send_keys(num) 28 driver.find_element_by_id("TextBox2").clear() 29 driver.find_element_by_id("TextBox2").send_keys(num) 30 driver.find_element_by_id("chkYD").click() 31 driver.find_element_by_id("btnDL").click() 32 WebDriverWait(driver, 5).until(EC.alert_is_present()).accept() 33 self.open_and_click_menu(driver) 34 retry = 0 35 while retry <= 2: 36 try: 37 driver.switch_to.frame(driver.find_element_by_id('iframeautoheight')) 38 WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.XPATH, "//input[@id='Button2']"))) 39 break 40 except: 41 print '重新點擊按鈕' 42 driver.switch_to.parent_frame() 43 self.open_and_click_menu(driver) 44 retry += 1 45 else: 46 print '重試失敗' 47 48 source = driver.page_source 49 driver.find_element_by_xpath("//input[@id='Button2']").click() 50 51 def source_change(driver): 52 if source == driver.page_source: 53 return False 54 else: 55 return driver.page_source 56 57 self.writeToFile(WebDriverWait(driver, 10).until(source_change)) 58 driver.quit() 59 60 def open_and_click_menu(self, driver): 61 menu1 = WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.XPATH, "//ul[@class='nav']/li[5]"))) 62 menu2 = driver.find_element_by_xpath("//ul[@class='nav']/li[5]/ul/li[3]") 63 ActionChains(driver).move_to_element(menu1).move_to_element(menu2).click(menu2).perform() 64 65 def is_element_present(self, how, what): 66 try: 67 self.driver.find_element(by=how, value=what) 68 except NoSuchElementException as e: 69 return False 70 return True 71 72 def is_alert_present(self): 73 try: 74 self.driver.switch_to_alert() 75 except NoAlertPresentException as e: 76 return False 77 return True 78 79 def close_alert_and_get_its_text(self): 80 try: 81 alert = self.driver.switch_to_alert() 82 alert_text = alert.text 83 if self.accept_next_alert: 84 alert.accept() 85 else: 86 alert.dismiss() 87 return alert_text 88 finally: 89 self.accept_next_alert = True 90 91 def tearDown(self): 92 self.driver.quit() 93 self.assertEqual([], self.verificationErrors) 94 95 @staticmethod 96 def writeToFile(source): 97 wb = xlwt.Workbook(encoding='utf-8', style_compression=0) 98 soup = BeautifulSoup(source, "html.parser") 99 span = soup.find('span', attrs={'id': 'Label5'}) 100 sheet = wb.add_sheet('成績單', cell_overwrite_ok=True) 101 table = soup.find(attrs={'id': 'Datagrid1'}) 102 lines = table.find_all('tr') 103 for i in range(len(lines)): 104 tds = lines[i].find_all('td') 105 for j in range(len(tds)): 106 sheet.write(i, j, tds[j].text) 107 try: 108 os.remove(span.text + '.xls') 109 except: 110 pass 111 wb.save(span.text + '.xls') 112 113 114 if __name__ == "__main__": 115 # unittest.main() 116 s = Script() 117 118 for i in range(1, 50): 119 num = '2013034743101' 120 s.setUp() 121 try: 122 if i <= 9: 123 s.test_jb(num[:12] + str(i)) 124 else: 125 s.test_jb(num[:11] + str(i)) 126 except: 127 pass
這種方法的意義只是熟悉一下自動化測試框架,因為速度實在太慢了,也就不詳細介紹了,這里粗略說一下,其實原理就是通過查到網頁中對應的控件,進行點擊或者懸浮於上面等等的操作,一步一步的到達最后的成績單,要做的是控制整個流程,明確在什么時候應該停一下等控件出現,什么時候要去點擊。
而且到目前為止,這個框架還是有一些Bug的,比如火狐瀏覽器的驅動無法實現在一個按鈕上Hover的操作等等。