天天看點

[技術部落格] 課程中心爬蟲教程

q2l

目錄

  • CHAP 1 基礎知識
    • 1.1 Session & Cookie
    • 1.2 Single Sign On
  • CHAP 2 實際應用
    • 2.1 Session & Cookie 的Python實作
    • 2.2 SSO 破解登陸難題
    • 具體實作
  • CHAP 3 困難和坑
    • 3.1 目前站點url被隐藏
    • 3.2 課程站點内部顯示使用iframe架構
    • 3.3 資源界面内檔案夾無Url連結
    • 3.4 作業連結送出前後不一緻
    • 3.5 通知中心通知顯示不全
    • 3.6 通知詳情内容無結構
  • Reference

  1. 由于HTTP協定是無狀态的協定,是以服務端需要記錄使用者的狀态時,就需要用某種機制來識具體的使用者,這個機制就是Session。

    典型的場景比如購物車,當你點選下單按鈕時,由于HTTP協定無狀态,是以并不知道是哪個使用者操作的,是以服務端要為特定的使用者建立了特定的Session,用用于辨別這個使用者,并且跟蹤使用者,這樣才知道購物車裡面有幾件物品。這個Session是儲存在服務端的,有一個唯一辨別。在服務端儲存Session的方法很多,記憶體、資料庫、檔案、叢集等。

  2. 服務端如何識别特定的客戶?第一次建立Session的時候,服務端會在HTTP協定中告訴用戶端,需要在 Cookie 裡面記錄一個Session ID,以後每次請求把這個會話ID發送到伺服器,就可以依據此來識别不同用戶端了。

    如果用戶端的浏覽器禁用了 Cookie 怎麼辦?一般這種情況下,會使用一種叫做URL重寫的技術來進行會話跟蹤,即每次HTTP互動,URL後面都會被附加上一個諸如 sid=xxxxx 這樣的參數,服務端據此來識别使用者。

總結:

  1. Session是在服務端儲存的一個資料結構,用來跟蹤使用者的狀态,這個資料可以儲存在叢集、資料庫、檔案中;
  2. Cookie是用戶端儲存使用者資訊的一種機制,用來記錄使用者的一些資訊,也是實作Session的一種方式。

​ 單點登入英文全稱Single Sign On,簡稱就是SSO。它的解釋是:在多個應用系統中,隻需要登入一次,就可以通路其他互相信任的應用系統。

[技術部落格] 課程中心爬蟲教程

​ 如圖所示,圖中有4個系統,分别是Application1、Application2、Application3、和SSO。Application1、Application2、Application3沒有登入子產品,而SSO隻有登入子產品,沒有其他的業務子產品,當Application1、Application2、Application3需要登入時,将跳到SSO系統,SSO系統完成登入,其他的應用系統也就随之登入了。這完全符合我們對單點登入(SSO)的定義。

[技術部落格] 課程中心爬蟲教程

上圖是CAS官網上的标準流程,具體流程如下:

  1. 使用者通路app系統,app系統是需要登入的,但使用者現在沒有登入。
  2. 跳轉到CAS server,即SSO登入系統,以後圖中的CAS Server我們統一叫做SSO系統。 SSO系統也沒有登入,彈出使用者登入頁。
  3. 使用者填寫使用者名、密碼,SSO系統進行認證後,将登入狀态寫入SSO的session,浏覽器(Browser)中寫入SSO域下的Cookie。
  4. SSO系統登入完成後會生成一個ST(Service Ticket),然後跳轉到app系統,同時将ST作為參數傳遞給app系統。
  5. app系統拿到ST後,從背景向SSO發送請求,驗證ST是否有效。
  6. 驗證通過後,app系統将登入狀态寫入session并設定app域下的Cookie。

至此,跨域單點登入就完成了。以後我們再通路app系統時,app就是登入的。接下來,我們再看看通路app2系統時的流程。

  1. 使用者通路app2系統,app2系統沒有登入,跳轉到SSO。
  2. 由于SSO已經登入了,不需要重新登入認證。
  3. SSO生成ST,浏覽器跳轉到app2系統,并将ST作為參數傳遞給app2。
  4. app2拿到ST,背景通路SSO,驗證ST是否有效。
  5. 驗證成功後,app2将登入狀态寫入session,并在app2域下寫入Cookie。

這樣,app2系統不需要走登入流程,就已經是登入了。SSO,app和app2在不同的域,它們之間的session不共享也是沒問題的。

實作Python爬蟲的方式有二:

  1. selenium

    通過利用 chromedriver 等自動化工具實作模拟浏覽器的操作,官網上最大的智語:

    [技術部落格] 課程中心爬蟲教程

    優點:實作簡單,調試簡單,和使用者正常使用浏覽器的操作一樣

    缺點:記憶體資源消耗大,需要安裝自動化驅動的支援

  2. requests

    模拟浏覽器的請求,可以攜帶Cookie以保持Keep-alive的連接配接,官網的介紹:

    Requests allows you to send HTTP/1.1 requests extremely easily. There’s no need to manually add query strings to your URLs, or to form-encode your POST data. Keep-alive and HTTP connection pooling are 100% automatic, thanks to urllib3.

    優點:記憶體占用小,通路速度快

    缺點:調試困難,不能對無url的按鈕(如: javascript(0))進行模拟點選,需要手動模拟請求

考慮到我們的使用者量規模在150人以上,而且每個人在登陸的時候都會進行課程同步,伺服器是2C4G的剛好夠用配置,是以我們使用的技術是Python的Requests子產品。

​ 在具體實作的時候,剛開始是想對 課程中心網站 直接進行登陸,但是直接通路登陸會跳轉到 SSO 登陸界面,輸入使用者名密碼之後再跳轉回課程中心,中間跳轉的過程太多,嘗試實作未果。

​ 在一次嘗試使用财務系統的時候,從網頁VPN進入,看到第三個應用就是課程中心,意識到通過網頁VPN直接登陸可能是使用了單點登入的技術,是以嘗試先成功登陸網頁VPN,然後通過Session攜帶的資訊再通路課程中心網站,果然,成功了。

s = requests.session() # 建立一個Session,下面使用該Session進行網頁請求
           

檢視Session中儲存的Cookie:

[技術部落格] 課程中心爬蟲教程

和網站上直接登陸儲存的Cookie進行比對:

[技術部落格] 課程中心爬蟲教程

可以看到Session可以保持所有的浏覽器中的資訊,是以可以直接登陸到課程中心了。

在上面的Selenium和Requests對比中,最明顯的一點就是:

Selenium 可以直接模拟滑鼠的點選

Requests 需要通過模拟 HTTP/1.1 的模拟實作

然而課程中心中的大部分都是沒有直達Url的,大多用iframe架構加上POST查詢實作頁面的重新整理和跳轉,這給爬蟲帶來很大的難題。

下面講一下遇到的困難和坑和解決方法:

[技術部落格] 課程中心爬蟲教程

雖然在一般情況下點進課程的第一個是 首頁 ,但是存在某些情況下,某些老師的設定是隻有資源和作業,是以點進第一個就是資源,這時候直接擷取href是會NoneType錯誤,這時候直接儲存目前課程站點的Url即可。

[技術部落格] 課程中心爬蟲教程

内容存在于iframe架構内時,無法擷取架構内的Url,而selenium可以通過切換如iframe内模拟點選,requests則需要進入iframe架構單獨的頁面。是以先擷取src内的網址,重新GET請求。

[技術部落格] 課程中心爬蟲教程

對于資源界面中的檔案夾,可以發現點選并不會跳轉網頁,網頁連結不變,通過JavaScript操作擷取到檔案夾内的資源并渲染顯示,并且我們大概可以看到JavaScript執行的參數。這說明網頁在使用者看不見的地方進行了加載。

為了看到這種加載,我們開啟Chrome的F12調試者模式,在Network下,開啟Preserve Log,篩選All,保留網站内跳轉産生的日志,再點選檔案夾,可以看到儲存了幾個Log:

[技術部落格] 課程中心爬蟲教程

可以發現其中有一個status code為302的狀态碼,對應的是:

302 Found:臨時重定向。

這個狀态碼是指用戶端要請求的資源臨時放到一個新地方了。同樣,應答中也包含了資源的新位址。

檢視該記錄中的詳細資訊:

[技術部落格] 課程中心爬蟲教程

和上面審查元素中的onclick中的元素有強大的相關性,通過嘗試可以發現點選檔案夾深入的操作的collectionId是組成為:

/group/該課程下的一串字元/檔案夾名稱

,sakai_action為

doNavigate

,sakai_csrf_token應該是csrf相關的Token,通常藏匿于網站的源碼内,作為有效請求的辨別符。

檢視網頁源代碼,搜尋課程有關的字元串,可以從幾個地方發現:

  • 網頁頭
[技術部落格] 課程中心爬蟲教程
  • 課程站點頭
[技術部落格] 課程中心爬蟲教程

直接通過第一個位置進行擷取:

def getCollectionIdPrefix(rr):
    # rr = beautifulSoup(session.get(assignment_iframe_url, cookies=cookie).text, 'html.parser')
    for script in rr.findAll(attrs={'type':"text/javascript"}):
        # print(str(script))
        if 'collectionId' in str(script):
            content = str(script)
            break
    # print(content)
    pattern = re.compile('collectionId = (\S+);')
    prefix = pattern.findall(content)[0][1:-1]
    return prefix
           

然後搜尋sakai_csrf_token:

[技術部落格] 課程中心爬蟲教程

隻找到一條記錄,存在于課程body的尾部,可以通過唯一的name進行擷取:

def getCsrfToken(rr):
    # rr = beautifulSoup(session.get(assignment_iframe_url, cookies=cookie).text, 'html.parser')
    return rr.find('input',{'name':'sakai_csrf_token'}).attrs['value']
           

找到這幾個就可以進行POST請求的僞造了:

def enterFolder(collectionId, ass_iframe_url, s, cookie, token):
    # enter is bidirectional, both go deepin and return 
    ## mock navigate data
    data = {
        'source': 0,
        'collectionId': collectionId,
        'navRoot': collectionId,
        'criteria': 'title',
        'sakai_action': 'doNavigate',
        'sakai_csrf_token': token
    }
    ## go to next folder
    s.post(ass_iframe_url, cookies=cookie, data=data)
    return
           

在進行POST請求後,目前Session中的Cookie已經改變,但是還沒有獲得資源,細看Preserve Log可以發現在POST後面會跟上一個非常相似的GET,裡面沒有攜帶任何内容,依舊請求資源站點的Url就可以了。

至于傳回上一級檔案夾,可以點選上面的面包屑導航,一樣的方式檢視Preserve Log僞造POST請求:

[技術部落格] 課程中心爬蟲教程
[技術部落格] 課程中心爬蟲教程

可以發現多了一個navRoot字段,與collectionId相同,其餘一樣,先POST再GET。

  • 對于已經送出的作業,其Url構成為:
    href="https://course.e2.buaa.edu.cn/portal/tool/a5695950-ebed-96d8-78e8b58ab8?submissionId=/assignment/s/0d40488a-ec2c-aef5-87b531b4/d600add7-1b92-a428-7057935e3fe7/243fafd9-7ae9-9836-279f9502&panel=Main&sakai_action=doView_grade"
    
    作業進入的連結 + '?submissionId=/assigment/s/' + ... + '&panel=Main&sakai_action=doView_grade'.
               
  • 對于未送出的作業,其Url構成為:
    href="https://course.e2.buaa.edu.cn/portal/tool/a5695950-4b82-96d8-78e8b8ab8?assignmentReference=/assignment/a/0d404c2c-4650-aef5-87b431b4/2b33659dc-4769-99b7-5d0a8269&panel=Main&sakai_action=doView_submission"
    
    作業進入的連結 + '?assignmentReference=/assignment/a/' + ... + '&panel=Main&sakai_action=doView_submission'.
               

由于後端的存儲中,對作業的唯一性的判定采用的是Url判斷,是以會出現送出作業前拉去課程中心和送出作業後拉取課程中心造成一份作業出現兩次的Bug。

這時隻能通過判斷其作業進傳入連結接是否相同來判斷是否一緻:

homework_url.split('?')[0]
           

[技術部落格] 課程中心爬蟲教程

需要先選擇顯示200項然後再GET所有即可:

token = getCsrfToken(note_ss)
selectData = {
  'eventSubmit_doChange_pagesize': 'changepagesize',
  'selectPageSize': '200',
  'sakai_csrf_token': token
}
s.post(note_url, cookies=cookie, data=selectData)
           

[技術部落格] 課程中心爬蟲教程

需要使用next_siblings進行周遊,需要注意的是next_siblings是屬性而不是方法。

  1. Zhihu | COOKIE和SESSION有什麼差別?
  2. JianShu | 單點登入 (SSO) 看這一篇就夠了
  3. 免費的分布式的自動化測試工具
  4. [Requests: HTTP for Humans™](

繼續閱讀