python刷課腳本(正方教務系統)

最近在學python,正好做一個刷課腳本來練習一下。熟悉一下python的語法。過程中也遇到不少問題。下面就把整個過程記錄下來。


python刷課腳本實際上是模擬瀏覽器的訪問過程,因爲選課需要不斷的刷新,所以這裏可以利用腳本自動提交表單,解放雙手

一,對教務系統進行分析

這裏用chrome的調試功能f12


首先是模擬登錄。這裏我們看到有驗證碼,對於驗證碼,我試過python的文字識別,但是不理想。所以只能手工輸入。

1.驗證碼處理

觀察源碼

<img id="icode" src="CheckCode.aspx" onclick="reloadcode();" alt="看不清,換一張" title="看不清,換一張" border="0" style="POSITION:absolute;TOP:5px;LEFT:130px">

這個我們可以看出驗證碼是通過CheckCode.aspx顯示的。那麼我們就把驗證碼下載來,保存爲圖片就行。

2.cookie處理

這個是比較重要的,因爲我們登錄上去需要保持登錄狀態。我們先分析一下登錄的過程

當我們訪問教務系統網站的時候,我們看到的網址是http://www.scut.edu.cn/jw2005/

實際上,真實的網址是http://222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/default2.aspx

觀察登錄的post請求


可以看到,服務器沒有存cookie到本地。那麼服務器如何保存登錄狀態呢。於是我根據服務器使用的asp,去查找相應的資料。果然,asp使用了不使用 Cookie 的 ASP.NET 會話管理,它利用url:http://222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/default2.aspx中的(julnrg55o3ukpeqcudqgbl45)來保持登錄狀態。也就是說我們只要使用這個url來訪問服務器,就可以保持同一登錄狀態。


3.字符編碼處理
我們看到上面的表單中有
RadioButtonList1:%D1%A7%C9%FA
觀察url編碼方式,可以看出是gb2312的字符進行了url編碼。由於python的默認編碼的是ascii編碼,那麼如果我們直接對其進行url編碼,那麼得到的字符是錯誤的。
我自己的python設置了utf-8編碼,utf-8用3個字符代表一個漢字,所以url編碼之後的結果是:
>>> urllib2.quote('有')
'%E6%9C%89'
所以我們需要把中文編碼爲gbk,然後再進行url編碼
>>> urllib2.quote('有'.decode('utf8').encode('gbk'))
'%D3%D0'
先把‘有’以utf8進行解碼,然後編碼成gbk,最後再進行url編碼。
如果默認編碼方式不是utf8,那麼可以這樣寫:str.decode(sys.stdin.encoding).encode('gbk')
sys.stdin.encoding表示輸出的默認編碼
4.獲取選課鏈接
觀察源碼,我們可以發現選課的鏈接,但是裏面有個xm參數,用的是學生的名字,而且是中文,在url出現的中文字符都會被url編碼,那麼我們要記得進行轉碼。由於不同用戶的選課鏈接是不一樣的,在這裏我們要使用正則表達式在page裏面提取出選課的鏈接。
 href=re.findall(r'xf_xsqxxxk.+?N121103',self.getPage())
其中的學生姓名要提取出來,進行轉碼
word=re.findall(u"[\u4e00-\u9fa5]+",str)#用於提取字符串中的中文
url=re.sub(u"[\u4e00-\u9fa5]+",urllib2.quote(self.encodeGBK(chinese)),href[0].decode('utf8'))#替換url中的中文


4.接下來獲取所有課程
\
<input name="dpDataGrid2:txtPageSize" type="text" value="6" onchange="__doPostBack('dpDataGrid2$txtPageSize','')" language="javascript" id="dpDataGrid2_txtPageSize" class="width30">
觀察源碼,監聽到輸入框的數據有變化就會刷新頁面。
以下是提交的數據表單,

      '__EVENTTARGET':'dpkcmcGrid:txtPageSize',
            '__EVENTARGUMENT':'',
            'ddl_ywyl':'',
            '__VIEWSTATE':'',#這個viewstate簡直無力吐槽,起碼有上萬個字符,編輯器都裝不下,最後我用正則在網頁源碼裏面提取,添加進請求裏面。
            'ddl_kcxz':'',
            'ddl_kcgs':'',
            'ddl_xqbs':'2',
            'ddl_sksj':'',
            'TextBox1':'',
            'dpkcmcGrid:txtChoosePage':'1',
            'dpkcmcGrid:txtPageSize':'120'

5,獲取到所有課程列表之後
我們先觀察提交選課的數據表單
    '__EVENTTARGET':'',
            '__EVENTARGUMENT':'',
            'ddl_ywyl':'',
            '__VIEWSTATE':self.getViewState(page),
            'ddl_kcxz':'',
            'ddl_kcgs':'',
            'ddl_xqbs':'2',
            'ddl_sksj':'',
            'TextBox1':'',
            'dpkcmcGrid:txtChoosePage':'1',
            'dpkcmcGrid:txtPageSize':'120',
            code:'on',#這裏的code是課程代碼,每門課都不一樣
            'Button1':self.encodeGBK('  提交  '),
            'dpDataGrid2:txtChoosePage':'1',
            'dpDataGrid2:txtPageSize':'120'
現在我們要拿到課程代碼,同樣是用正則表達式
我的思路是先得到所有課程列表,然後在匹配課程,得到課程代碼

二,總結
整個寫的過程,遇到的主要是兩個問題,一個是編碼問題,另一個是正則表達式的使用。
從理論上看這個腳本比較簡單,但實際做起來會遇到不少問題。
補充,在研究如何自動識別驗證碼的時候,發現正方這個驗證碼不好處理,後來發現原來可以跳過驗證碼登錄,原理是如果你不請求驗證碼就不用在表單中輸入,後臺就不驗證,看起來是程序員偷懶了,以爲瀏覽器一定會請求驗證碼,但是如果不是瀏覽器呢,一個小小的腳本就輕鬆繞過了驗證碼,再次印證安全領域的一句老話:永遠不要相信用戶的輸入。這也是一個比較嚴重的漏洞,因爲繞過就可以暴力破解賬號密碼了。
關於機器識別圖像,有精力再去研究。現在先投機取巧。
最後放上源碼
#!/usr/bin/env python
# coding=utf-8
__metaclass__=type
import urllib2 
import urllib
import time
import sys
import re


reload(sys)
sys.setdefaultencoding('utf-8')
class Spider:
    def __init__(self):
 
        self.baseUrl='http://222.201.132.117/'
        self.CheckCodeUrl='CheckCode.aspx'
        self.PostUrl='default2.aspx'
        self.Agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/40.0.2214.111 Chrome/40.0.2214.111 Safari/537.36'
        #開啓debug
        httpHandler = urllib2.HTTPHandler(debuglevel=0)
        self.opener = urllib2.build_opener(httpHandler)
        self.attempt=0 
        self.setRealUrl()




    def setRealUrl(self):
        '獲取真實網站,包含cookie'
        request = urllib2.Request(self.baseUrl)
        request.add_header('User-Agent', self.Agent)
        urlList=self.opener.open(request).geturl().split('/')
        urlList.pop()
        self.baseUrl='/'.join(urlList)+'/'
      
    def getCheckCode(self):
        '獲取驗證碼'
        #設置agent
        request = urllib2.Request(self.baseUrl+self.CheckCodeUrl)
        request.add_header('User-Agent', self.Agent)
        #下載驗證碼
        file=self.opener.open(request)
        pic=file.read()
        path='code.gif'
        CheckCode=open(path,'wb')
        CheckCode.write(pic)
        CheckCode.close()
      
    def setInfo(self):
        '設置身份信息'
        #學號
        self.uid=raw_input('輸入學號:')
        #密碼  
        self.pwd=raw_input('輸入密碼:')
        #驗證碼
        #self.getCheckCode()
        #self.code=raw_input('輸入驗證碼:(驗證碼在本軟件所在文件夾)')
        self.code=''
    def getPage(self):
        '登錄教務系統,並且獲取網頁'
        postdata=urllib.urlencode({    
            '__VIEWSTATE':'dDwyODE2NTM0OTg7Oz5VGnjXV87Z19Dm3QbgRgvcptEYyA==',    
            'txtUserName':self.uid,
            'TextBox2': self.pwd,
            'txtSecretCode':self.code,
            'RadioButtonList1':self.encodeGBK('學生'),#用gbk編碼
            'Button1':'',
            'lbLanguage':'',
            'hidPdrs':'',
            'hidsc':''
        })  
        headers={
            'User-Agent':self.Agent,
            'Referer':self.baseUrl
        }


        req = urllib2.Request(  
            url = self.baseUrl+self.PostUrl,
            data = postdata,  
            headers = headers  
        )  
        try:    
            response = urllib2.urlopen(req)   
            page=response.read().decode('gbk').encode('utf-8')#對gbk編碼的數據進行轉碼
            return page 
        except urllib2.URLError, e:    
            self.run()
            if hasattr(e, 'code'):    
                print 'The server couldn\'t fulfill the request.'    
                print 'Error code: ', e.code    
            elif hasattr(e, 'reason'):    
                print 'We failed to reach a server.'    
                print 'Reason: ', e.reason    
        else:    
            print 'No exception was raised.'    
            # everything is fine    


    def encodeGBK(self,str):
        '對字符串用gbk進行編碼'
        return str.decode('utf-8').encode('gbk')


    def getSelect(self):
        '獲取選課的網址'
        href=re.findall(r'xf_xsqxxxk.+?N121103',self.getPage())
        chinese=self.getChinese(href[0].decode('utf8'))
        url=re.sub(u"[\u4e00-\u9fa5]+",urllib2.quote(self.encodeGBK(chinese)),href[0].decode('utf8'))
        return url


    def getChinese(self,str):
        '從字符串獲取中文'
        word=re.findall(u"[\u4e00-\u9fa5]+",str)
        return word[0]


    def getViewState(self,page):
        '從網頁中獲取viewstate'
        return re.search('<input type="hidden" name="__VIEWSTATE" value="(.+?)" />',page).group(1)


    def getList(self):
        '獲取全部課程列表'
        url=self.baseUrl+self.getSelect()
        request=urllib2.Request(url)
        request.add_header('User-Agent', self.Agent)
        request.add_header('Referer', self.baseUrl+'/xs_main.aspx?xh='+self.uid)
        response = self.opener.open(request)   
        page=response.read().decode('gbk').encode('utf-8')#對gbk編碼的數據進行轉碼
        postdata=urllib.urlencode({
            '__EVENTTARGET':'dpkcmcGrid:txtPageSize',
            '__EVENTARGUMENT':'',
            'ddl_ywyl':'',
            '__VIEWSTATE':self.getViewState(page),
            'ddl_kcxz':'',
            'ddl_kcgs':'',
            'ddl_xqbs':'2',
            'ddl_sksj':'',
            'TextBox1':'',
            'dpkcmcGrid:txtChoosePage':'1',
            'dpkcmcGrid:txtPageSize':'120'
        })
        headers={
            'User-Agent':self.Agent,
            'Referer':url
        }


        request = urllib2.Request(  
            url = url,
            data = postdata,  
            headers = headers  
        )  
        try:


            response = self.opener.open(request)   
            page=response.read().decode('gbk').encode('utf-8')#對gbk編碼的數據進行轉碼
            return page,url
        except urllib2.URLError:
            self.run()


    def postCourse(self,course,teacher,date):
        '獲取課程信息,並且提交'
        data=self.getList()
        url=data[1]
        page=data[0]
        pat='<tr.+?</tr>'
        pat_code='<input id=".+?" type="checkbox" name="(.+?)" />'


        pattern=re.compile(pat,re.S)#開啓dotall模式
        codes=re.findall(pattern,page)#找出課程代碼
        code=''
        flag=1
        for c in codes:
            if course in c and teacher in c and date in c:
                l=re.findall(pat_code,c)
                code=l[0]
                flag=0
                break
        if(flag):
            print '未找到相應課程,請重新輸入'
            exit()
        print code
        self.submit(code,url,page,date)
        if(self.checkSelect(course,page,code,url,date)):
            return True
        else:
            return False
<span style="white-space:pre">	</span>
      
    def submit(self,code,url,page,date):
        '提交選課表單'
        postdata=urllib.urlencode({
            '__EVENTTARGET':'',
            '__EVENTARGUMENT':'',
            'ddl_ywyl':'',
            '__VIEWSTATE':self.getViewState(page),
            'ddl_kcxz':'',
            'ddl_kcgs':'',
            'ddl_xqbs':'2',
            'ddl_sksj':'',
            'TextBox1':'',
            'dpkcmcGrid:txtChoosePage':'1',
            'dpkcmcGrid:txtPageSize':'120',
            code:'on',
            'Button1':self.encodeGBK('  提交  '),
            'dpDataGrid2:txtChoosePage':'1',
            'dpDataGrid2:txtPageSize':'120'
        })
        headers={
            'User-Agent':self.Agent,
            'Referer':url
        }


        request = urllib2.Request(  
            url = url,
            data = postdata,  
            headers = headers  
        )  
        try:
            response = self.opener.open(request)   
            page=response.read().decode('gbk').encode('utf-8')#對gbk編碼的數據進行轉碼
        except urllib2.URLError:
            self.run(course,teacher,date)
    
    def checkSelect(self,course,page,code,url,date):
        '判斷選課是否成功'
        pat='<legend>已選課程</legend><table.+?'+course+'.+?</table>'
        pattern=re.compile(pat,re.S)
        lenth=len(re.findall(pattern,page))
        if(lenth==1):
            print '選課成功'
            return False
        else:
            time.sleep(1)#等待一秒
            self.attempt += 1
            print '嘗試選課'+str(self.attempt)
            self.submit(code,url,page,date)
            return True


    def run(self,course,teacher,date):
        self.setInfo()


        while(self.postCourse(course,teacher,date)):
            pass




spider=Spider()
course=raw_input("輸入課程:")
teacher=raw_input("輸入教師:")
date=raw_input("輸入週一~週五")
spider.run(course,teacher,date)










Name
Path
 

   
default2.aspx
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)
 
xs_main.aspx?xh=201330613452
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)
 
jw.css
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/style/base
 
base.css
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/style/base
 
module.css
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/style/base
 
forms.css
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/style/base
 
page.css
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/style/base
 
jw.css
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/style/standard
 
base.css
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/style/standard
 
module.css
222.201.132.117/(julnrg55o3ukpeqcudqgbl45)/style/standard
 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章