Python--校園網爬蟲記


 

查成績,算分數,每年的綜合測評都是個固定的過程,作為軟件開發者,這些過程當然可以交給代碼去做,通過腳本進行網絡請求獲取數據,然后直接進行計算得到基礎分直接填表就好了,查成績再手動計算既容易出錯也繁瑣,所以本篇的內容就是開發一個爬蟲腳本取抓取成績表,至於綜合測評計算,這個沒什么意義這里就不說了,分數都有了就都夠了。

我們的目的就是通過編寫腳本,模仿瀏覽器進行請求獲取源碼,再進行解析本地化(或者直接計算)

要抓取到數據,其實方案不止一種,這里會介紹兩種不同的方案,達到同樣的目的:

  • 模仿瀏覽器進行請求(速度快)
  • 操作瀏覽器進行請求(速度慢)

先說第一種,這種方案是普遍的爬蟲技術,因為爬取的內容不多,對速度要求也不夠,所以就是很簡單的一個爬蟲過程:

  1. 分析請求
  2. 模仿請求

對於普通的校園網,一般不做流量限制,所以就算請求頻繁,也基本不用擔心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的操作等等。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM