事情緣由還得從那天下午的課說起严卖。當時大家都在認真聽課迎变。突然,旁邊一哥們說他搶到了“高級數(shù)理邏輯”了,what滑黔?脱柱?猿推?“高級數(shù)理邏輯”秘症?就是那門課水易過的神課?可是明明選課系統(tǒng)剛開始1分鐘不到就沒了呀蔑赘。于是狸驳,就問了他是怎么搞到的。他說是運行了幾行JavaScript
腳本缩赛,自動刷課的耙箍。我恍然大悟,原來你們都是這么選課的八肘伞辩昆!于是就考慮要不自己也搞個腳本?事不宜遲旨袒,課后就開搞汁针!
具體怎么操作呢?我最開始的想法是調(diào)出Chrome控制臺砚尽,寫好JavaScript代碼施无,然后準備循環(huán)刷新運行。但老是報錯“no such element”
必孤,以前沒怎么用過JavaScript猾骡,以為跳轉(zhuǎn)到不同的頁面之后,原頁面上的代碼就不能用了,所以會出現(xiàn)找不到元素的錯誤⌒讼耄現(xiàn)在回過頭來看幢哨,原來是由于該元素在另外一個frame中里面,必須先移動到另外一個frame嫂便,才能找到對應的元素捞镰,所以才會報這個錯。有時間搞個JS版的腳本毙替。
不管怎么樣岸售,直接在控制臺執(zhí)行JavaScript的想法在當時看來是不行了。這時我想到了假期實習時曾用python selenium庫試著爬取微博用戶的頭像蔚龙,這個庫能實現(xiàn)摸擬瀏覽器運行,不需要分析各種表單提交參數(shù)映胁,就能讀到動態(tài)網(wǎng)頁的所有信息木羹,實在是爬動態(tài)網(wǎng)頁的首選,缺點是速度比較慢解孙。后來坑填,因為新浪PC站的反爬蟲相對嚴格,最終還是用了requests庫加上cookie參數(shù)爬微博移動站弛姜。如果要爬取社交網(wǎng)站的數(shù)據(jù)的話脐瑰,其對應的靜態(tài)的移動站是比較靠譜的選擇。
最終決定選擇用python廷臼,結(jié)合selenium庫實現(xiàn)自動選課苍在。
正式進入今天的主題。
環(huán)境配置
- 安裝python3荠商,再安裝selenium庫寂恬,直接
pip install selenium
就行。 - 下載chromedriver驅(qū)動莱没,也可以選擇沒有界面的phantomJS瀏覽器初肉,為了方便調(diào)試,也不追求速度饰躲,我選擇了有界面的chrome瀏覽器牙咏。
- 引入Chrome瀏覽器
chromedriver = "E:\LabProjects\crwalChinaZ\chromedriver"
os.environ['webdriver.chrome.driver'] = chromedriver
driver = webdriver.Chrome(chromedriver)
用上面這種方式啟動Chrome不用設(shè)置環(huán)境變量,只需要給出chromedriver的本地文件路徑即可嘹裂。然后程序就會打開不帶任何配置的純凈的chrome瀏覽器(可以給webdriver.Chrome()
函數(shù)傳入配置參數(shù)妄壶,比如插件,這樣瀏覽器就會帶上相應的插件)寄狼。
執(zhí)行 driver.get('http://yjxt.bupt.edu.cn/')
打開選課網(wǎng)站盯拱,此時運行效果如下
填充表單
現(xiàn)在已經(jīng)成功打開了教務(wù)處的網(wǎng)站,下一步輸入賬戶密碼,實現(xiàn)登錄狡逢。
首先定位賬戶密碼表單的位置宁舰,傳入自己的賬號和密碼。driver.get(url)
用于打開一個網(wǎng)頁奢浑,但由于現(xiàn)在的大多數(shù)的Web應用程序使用Ajax技術(shù)蛮艰,當一個頁面被加載到瀏覽器時,該頁面內(nèi)的元素可以在不同的時間點被加載雀彼。而driver.get(url)
并不保證web頁面所有元素加載完成后再返回壤蚜。對于這樣的情況,官網(wǎng)給的建議是顯式或隱式地等待一段時間徊哑。用driver.implicitly_wait(seconds)
實現(xiàn)隱式等待袜刷,WebDriverWait()
(下面會提到)實現(xiàn)隱式等待。根據(jù)函數(shù)單詞意思莺丑,“隱式等待”很容易理解著蟹,就相當于sleep
一段時間,那顯式等待WebDriverWait()
怎么理解呢梢莽?我們先看該函數(shù)的一個使用示例:
try:
CourseManagement = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.ID, 'menu')))
except Exception as e:
print(e)
以上代碼表示最多等待瀏覽器20秒萧豆,或直到ID為“menu”
的節(jié)點出現(xiàn)為止,如果元素出現(xiàn)昏名,則將這個節(jié)點賦給了CourseManagement
涮雷,如果超時了則報錯。結(jié)合函數(shù)的字面意思也很好理解轻局。具體各個參數(shù)的意義詳見selenium中文文檔
待頁面元素都加載完成后洪鸭,需在網(wǎng)頁的源碼中找到賬戶密碼表單元素的位置。注意仑扑,必須通過“更多工具-》開發(fā)者平臺”或直接“右鍵-》檢查”卿嘲,而不能通過“右鍵-》查看網(wǎng)頁源代碼”來獲得查看頁面的源代碼,這兩者的內(nèi)容是不同的夫壁,前者包含了靜態(tài)和動態(tài)加載的源碼拾枣,后者只有靜態(tài)的源碼,沒有我們所需要的表單元素盒让。
selenium提供了很多定位元素的方法梅肤,常用的有find_element_by_id
,find_element_by_name
邑茄,find_element_by_xpath
姨蝴。官網(wǎng)提供了更多定位元素的方法,詳見selenium中文文檔肺缕。如何確定元素的xpath路徑左医,一直是件讓人頭疼的事授帕。有個小技巧很有用,在開發(fā)者平臺上找到要找的頁面元素浮梢,然后“右鍵-》copy-》copy xpath”跛十,這樣該元素的xpath路勁就復制到粘貼板上了,直接粘貼即可秕硝,非常好用芥映!找到表單的代碼如下:
driver.implicitly_wait(3)
# driver.maximize_window()
account = driver.find_element_by_id("username")
passwd = driver.find_element_by_id('password')
確定表單之后,需要填充表單远豺,這里使用send_keys方法奈偏,分別傳入你的賬戶和密碼填充表單。
account.send_keys(config.account)
passwd.send_keys(config.password)
提交表單
表單填好后躯护,當然是提交表單惊来。在selenium中有幾種方法能提交表單。
- 在頁面中觀察對應的提交按鈕棺滞,找到這個元素裁蚁,然后執(zhí)行該元素的
click()
方法,實現(xiàn)表單提交检眯。在這個頁面中厘擂,“提交”按鈕當然是“立即登錄”按鈕了昆淡,找到這個元素再執(zhí)行click()
方法即可锰瘸。這種方法雖然通用,但必須找到登錄元素所在的位置昂灵,比較麻煩避凝; - 直接執(zhí)行
account.submit()
方法,也能提交表單眨补。當調(diào)用元素的submit()
方法時管削,selenium會尋找離該元素最近的可提交的元素,具體是有type="submit"
屬性的元素撑螺,并提交含思。這里離account
最近的滿足該條件的元素當然就是“立即登錄”按鈕啊,所以也能達到提交表單的效果甘晤。當然含潘,按照這個原理,也可以通過密碼框元素的submit()
方法即passwd.submit()
實現(xiàn)同樣的效果线婚,非常方便遏弱,推薦使用這種方法; - 最后一種方法是模擬鍵盤的操作塞弊。很多網(wǎng)站登錄頁面的實現(xiàn)邏輯都是賬戶和密碼填好后漱逸,直接按回車就可以提交表單泪姨,實現(xiàn)登錄。selenium提供了模擬鍵盤的方法饰抒,如
elem.send_keys(Keys.RETURN)
肮砾,這相當于“點擊”了回車鍵,實現(xiàn)同樣的效果循集。
綜合來說唇敞,個人覺得第二種方法更加直觀好用,第三種模擬鍵盤的方法需要考慮網(wǎng)站的鍵位順序咒彤,可能會出現(xiàn)一些問題疆柔。所以直接執(zhí)行account.submit()
,進入選課系統(tǒng)镶柱,現(xiàn)在頁面如下:
![選課系統(tǒng)界面1]
](http://upload-images.jianshu.io/upload_images/3029393-2a0d92550e561530.JPG?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
進入選課頁面
現(xiàn)已成功登錄系統(tǒng)旷档,按照選課流程,需要先點擊左下角“課務(wù)管理”歇拆,然后再點擊“課務(wù)管理”下面的“課程網(wǎng)上選課管理”鞋屈,此時右邊彈出的界面即為選課頁面。所以故觅,現(xiàn)階段的任務(wù)是找到“課務(wù)管理”和“課程網(wǎng)上選課管理”兩個元素厂庇,分別執(zhí)行click
事件,進入選課界面输吏。
首先找到“課程管理”位置权旷,執(zhí)行click事件
CourseManagement = driver.find_element_by_xpath('//div[@id="menu"]/div[2]')
CourseManagement.click()
可運行時卻提示“no such element”
錯,這令人很郁悶贯溅,代碼中明明有menu
這個id
的拄氯,為什么會報錯呢?這個問題糾結(jié)了好久它浅,selenium文檔上也沒有這個問題译柏,最后費了好大的力氣,終于在stackoverflow上找到了答案姐霍。有的頁面由幾個frame組成鄙麦,如果要訪問的元素不在當前的frame中,那么必須先切換到該元素所在的frame镊折,才能進一步選定元素胯府。那frame又是什么呢?我查了下腌乡,找到了下面這段簡要描述:
框架是網(wǎng)頁中常用的技術(shù)盟劫,可以讓多個URL的內(nèi)容顯示在一個頁面中。常用標簽FRAMESET与纽,F(xiàn)RAME實現(xiàn)侣签。FRAMESET是用以劃分框窗塘装,每一框窗由一個FRAME標記所標示,F(xiàn)RAME必須在FRAMESET范圍中使用影所。iframe在frame的基礎(chǔ)上提供了更多好用的特性蹦肴。
仔細一看,左邊導航欄果然在一個在一個id為MenuFrame的iframe中猴娩,而剛才相當于在默認的frame中阴幌,當然找不到這個元素,所以現(xiàn)在的任務(wù)是轉(zhuǎn)到相應的frame卷中,再執(zhí)行操作矛双。【4.jpg】
了解原因后蟆豫,查了文檔议忽,發(fā)現(xiàn)switch_to_frame()
可以轉(zhuǎn)到指定的frame,代碼段如下:
frame = driver.find_element_by_id("MenuFrame")
driver.switch_to_frame(frame)
進入正確的frame之后十减,下面的代碼就能正確執(zhí)行了
CourseManagement = driver.find_element_by_xpath('//div[@id="menu"]/div[2]')
CourseManagement.click()
下一步是點擊“課程網(wǎng)上選課管理”栈幸。于是,按上面的套路帮辟,我寫了類似的代碼
driver.find_element_by_id('tree1_2_a').click()
代碼執(zhí)行后速址,點擊事件能觸發(fā),但是右邊彈出的頁面卻并不是預想的選課頁面由驹。仔細一看芍锚,原來是錯誤地“點擊”了“學期課表信息查詢”按鈕,導致右邊界面不對荔棉。再次確認元素的id
沒問題后闹炉,接著又執(zhí)行了幾次蒿赢,每次結(jié)果都不太一樣润樱,有時候“點擊”上面的按鈕,有的時候“點擊”下面的按鈕羡棵。程序員的都知道壹若,這種不按套路跑的程序是最讓人頭疼的,代碼明明是對的皂冰,但為什么每次結(jié)果都不一樣呢店展?難道還是代碼的問題?代碼肯定沒錯秃流,應該是環(huán)境的問題......
ActionChains類
這一通無意義的想法下來赂蕴,我還是乖乖谷歌吧。用中文搜了好久也找不到對應的問題舶胀,最后還是用了英文關(guān)鍵字才找到了問題的所在概说。這種問題主要是由于模擬瀏覽器的指針定位錯誤引起的碧注,就相當于鼠標的坐標計算錯了,所以導致點擊了錯誤的位置糖赔。有人提出了可以用ActionChains
類來實現(xiàn)點擊事件萍丐,以下是ActionChains
實現(xiàn)示例:
menu = driver.find_element_by_css_selector(".nav")
hidden_submenu = driver.find_element_by_css_selector(".nav #submenu1")
ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()
最后一行是一個動作鏈的實現(xiàn),首先移動到menu
元素放典,然后點擊hidden_submenu
元素逝变,最后的perform()
表示立即執(zhí)行該動作鏈。ActionChains
實現(xiàn)機制類似于真實的鼠標操作奋构,容易理解壳影。但代碼改用ActionChains
實現(xiàn)鼠標點擊事件后,錯誤仍然存在弥臼,真是讓人奇怪态贤,難不成確實是環(huán)境的問題?看來還得找另外的方法醋火。
嵌入JavaScript代碼
stackoverflow上有人提到悠汽,selenium有直接執(zhí)行JavaScript代碼的接口。selenium本身就是一個JS模擬器芥驳,用原生的JavaScript實現(xiàn)點擊事件肯定沒問題柿冲。貌似有點道理,先試一試再說兆旬。于是我嵌入了一行簡單的JavaScript代碼
driver.execute_script('document.getElementById("tree1_2_a").click()')
再次運行假抄,bug解決!
一路隨著bug狂奔之后丽猬,最終的選課界面終于出現(xiàn)了宿饱,下一步就是就是找到要選的課的位置,循環(huán)判斷能否選課脚祟,再傳遞click事件谬以,完成選課!
返回默認frame
然而由桌,還是太年輕为黎,高興得太早了。接著行您,先找到課的位置铭乾,再執(zhí)行簡單的點擊事件(PS. 下面的xpath路勁是直接在控制臺復制的,方法見上娃循,簡單快速?婚荨)
driver.find_element_by_xpath('//*[@id="contentParent_dgData"]/tbody/tr[44]/td[8]')
但是又提示“no such element”
錯誤。又是這個錯誤捌斧!仔細一想笛质,難道選課頁面在另外一個frame里吹泡?仔細一看,還真是经瓷。所以必須先轉(zhuǎn)到選課頁面所在的frame爆哑,然后才能進行操作。于是又有了下面代碼
Courseframe = driver.find_element_by_id("PageFrame")
driver.switch_to_frame(Courseframe)
又是“no such element”
錯誤舆吮!為什么呢揭朝?原來兩個frame間的關(guān)系是平行的,在其中一個frame是看不到另一個frame的元素的色冀,必須先進入主frame潭袱,即相當于這兩個frame的父frame,然后才能進入另外一個frame锋恬。查看官方文檔后屯换,發(fā)現(xiàn)switch_to_default_content()
函數(shù)能切換到默認的frame。執(zhí)行這個函數(shù)后与学,上面的代碼就能正確地執(zhí)行了彤悔。
到此,下面的邏輯就很簡單了索守。先循環(huán)判斷要選的課是否處于可選狀態(tài)晕窑,可以的話直接執(zhí)行click
事件。
由于網(wǎng)站的frame用得比較多卵佛,需要特別注意frame間的轉(zhuǎn)換杨赤。
多說一句
最近阿里月餅事件鬧得沸沸揚揚,我也不是受這件事的啟發(fā)才寫腳本的截汪,純粹是感興趣疾牲。
任務(wù)自動化本來就是程序員的一大樂趣,無關(guān)價值觀衙解。
附. 完整代碼:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
import os
import io
import sys
import config
import time
# 將wanted_course_num改為想選的課的順序
# 有效溝通技巧是0阳柔,寬帶通信網(wǎng)是1,以此類推
wanted_course_num = 42
wanted_course_string = '//*[@id="contentParent_dgData_hykFull_'
wanted_course = wanted_course_string + str(wanted_course_num) + '"]'
wanted_course2 = 'contentParent_dgData_hykSelkc_' + str(wanted_course_num)
print(wanted_course)
# 下載chromedriver丢郊,我這里是放在了
# E:\LabProjects\crwalChinaZ\chromedriver
# 更改為你放置的位置
chromedriver = "E:\LabProjects\crwalChinaZ\chromedriver"
os.environ['webdriver.chrome.driver'] = chromedriver
driver = webdriver.Chrome(chromedriver)
# driver = webdriver.PhantomJS()
driver.get('http://yjxt.bupt.edu.cn/')
driver.implicitly_wait(3)
# driver.maximize_window()
account = driver.find_element_by_id("username")
passwd = driver.find_element_by_id('password')
account.send_keys(config.account)
passwd.send_keys(config.password)
account.submit()
# try:
# CourseManagement = WebDriverWait(driver, 20).until(
# EC.presence_of_element_located((By.ID, 'menu')))
# except Exception as e:
# print(e)
driver.implicitly_wait(10)
while 1:
frame = driver.find_element_by_id("MenuFrame")
driver.switch_to_frame(frame)
CourseManagement = driver.find_element_by_xpath('//div[@id="menu"]/div[2]')
CourseManagement.click()
driver.execute_script('document.getElementById("tree1_2_a").click()')
# driver.find_element_by_id('tree1_2_a').click()
driver.implicitly_wait(5)
driver.switch_to_default_content()
Courseframe = driver.find_element_by_id("PageFrame")
driver.switch_to_frame(Courseframe)
logic_button = driver.find_element_by_xpath(wanted_course).text
if u'班級已全選滿' in logic_button:
print('wait 10 seconds!')
else:
# button = driver.find_element_by_xpath('//*[@id="contentParent_dgData"]/tbody/tr[44]/td[8]')
# button.click()
string = 'document.getElementById("{}").click()'.format(wanted_course2)
# print(string)
# driver.execute_script('document.getElementById(%s).click()' %(wanted_course2))
driver.execute_script(string)
driver.implicitly_wait(2)
driver.switch_to_default_content()
driver.switch_to_frame(driver.find_element_by_xpath("http://iframe[@name='selClass']"))
driver.execute_script('document.getElementById("contentParent_dgData_ImageButton1_0").click()')
break;
driver.switch_to_default_content()
# driver.refresh()
time.sleep(10)