一步步爬取Coursera課程資源

原文地址javascript

有時候咱們須要把一些經典的東西收藏起來,時時回味,而Coursera上的一些課程無疑就是經典之做。Coursera中的大部分完結課程都提供了完整的配套教學資源,包括ppt,視頻以及字幕等,離線下來後會很是便於學習。很明顯,咱們不會去一個文件一個文件的下載,只有傻子才那麼幹,程序員都是聰明人!html

那咱們聰明人準備怎麼辦呢?固然是寫一個腳原本批量下載了。首先咱們須要分析一下手工下載的流程:登陸本身的Coursera帳戶(有的課程須要咱們登陸並選課後才能看到相應的資源),在課程資源頁面裏,找到相應的文件連接,而後用喜歡的工具下載。java

很簡單是吧?咱們能夠用程序來模仿以上的步驟,這樣就能夠解放雙手了。整個程序分爲三個部分就能夠了:python

  1. 登陸Coursera;git

  2. 在課程資源頁面裏面找到資源連接;程序員

  3. 根據資源連接選擇合適的工具下載資源。github

下面就來具體的實現如下吧!web

登陸

剛開始時本身並無添加登陸模塊,覺得訪客就能夠下載相應的課程資源,後來在測試comnetworks-002這門課程時發現訪客訪問資源頁面時會自動跳轉到登陸界面,下圖是chrome在隱身模式訪問該課程資源頁面時的狀況。正則表達式

未登陸用戶訪問課程頁面資源

要想模擬登陸,咱們先找到登陸的頁面,而後利用google的Developer Tools分析帳號密碼是如何上傳到服務器的。chrome

咱們在登陸頁面的表單中填入帳號密碼,而後點擊登陸。與此同時,咱們須要雙眼緊盯Developer Tools——Network,找到提交帳號信息的url。通常狀況下,若是要向服務器提交信息,通常都用post方法,這裏咱們只須要先找到Method爲post的url。悲劇的是,每次登陸帳號時,Network裏面都找不到提交帳戶信息的地址。猜想登陸成功後,直接跳轉到登陸成功後的頁面,想要找的內容一閃而過了。

因而就隨便輸入了一組帳號密碼,故意登陸失敗,果然找到了post的頁面地址,以下圖:

提交帳戶信息的頁面

地址爲:https://accounts.coursera.org/api/v1/login。爲了知道向服務器提交了哪些內容,進一步觀察post頁面中表單中內容,以下圖:

提交表單內容

咱們看到一共有三個字段:

  • email:帳號的註冊郵箱

  • password:帳號密碼

  • webrequest:附加的字段,值爲true。

接下來就動手寫吧,我選擇用python的Requests庫來模擬登陸,關於Requests官網是這樣介紹的。

Requests is an elegant and simple HTTP library for Python, built for human beings.

事實上requests用起來確實簡單方便,不虧是專門爲人類設計的http庫。requests提供了Session對象,能夠用來在不一樣的請求中傳遞一些相同的數據,好比在每次請求中都攜帶cookie。

初步的代碼以下:

signin_url = "https://accounts.coursera.org/api/v1/login"
logininfo = {"email": "...",
             "password": "...",
             "webrequest": "true"
             }

user_agent = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) "
              "AppleWebKit/537.36 (KHTML, like Gecko) "
              "Chrome/36.0.1985.143 Safari/537.36")

post_headers = {"User-Agent": user_agent,
                "Referer": "https://accounts.coursera.org/signin"
                }
coursera_session = requests.Session()

login_res = coursera_session.post(signin_url,
                                  data=logininfo,
                                  headers=post_headers,
                                  )
if login_res.status_code == 200:
    print "Login Successfully!"
else:
    print login_res.text

將表單中提交的內容存放在字典中,而後做爲data參數傳遞給Session.post函數。通常狀況下,最好是加上請求User-AgentReferer等請求頭部,User-Agent用來模擬瀏覽器請求,Referer用來告訴服務器我是從referer頁面跳轉到請求頁面的,有時候服務器會檢查請求的Referer字段來保證是從固定地址跳到當前請求頁的。

上面片斷的運行結果很奇怪,顯示以下信息:Invalid CSRF Token。後來在github上面搜索到一個Coursera的批量下載腳本,發現人家發送頁面請求時headers多了XCSRF2Cookie, XCSRF2Token, XCSRFToken, cookie4個字段。因而又從新看了一下post頁面的請求頭部,發現確實有這幾個字段,估計是服務器端用來作一些限制的。

用瀏覽器登陸了幾回,發現XCSRF2Token, XCSRFToken是長度爲24的隨機字符串,XCSRF2Cookie爲"csrf2_token_"加上長度爲8的隨機字符串。不過一直沒搞明白Cookie是怎麼求出來的,不過看github上面代碼,Cookie彷佛只是"csrftoken"和其餘三個的組合,試了一下居然能夠。

在原來的代碼上添加如下部分就足夠了。

def randomString(length):
    return ''.join(random.choice(string.letters + string.digits) for i in xrange(length))

XCSRF2Cookie = 'csrf2_token_%s' % ''.join(randomString(8))
XCSRF2Token = ''.join(randomString(24))
XCSRFToken = ''.join(randomString(24))
cookie = "csrftoken=%s; %s=%s" % (XCSRFToken, XCSRF2Cookie, XCSRF2Token)

post_headers = {"User-Agent": user_agent,
                "Referer": "https://accounts.coursera.org/signin",
                "X-Requested-With": "XMLHttpRequest",
                "X-CSRF2-Cookie": XCSRF2Cookie,
                "X-CSRF2-Token": XCSRF2Token,
                "X-CSRFToken": XCSRFToken,
                "Cookie": cookie
                }

至此登陸功能初步實現。

分析資源連接

登陸成功後,咱們只須要get到資源頁面的內容,而後過濾出本身須要的資源連接就好了。資源頁面的地址很簡單,爲https://class.coursera.org/name/lecture,其中name爲課程名稱。好比對於課程comnetworks-002,資源頁面地址爲https://class.coursera.org/comnetworks-002/lecture

抓取到頁面資源後,咱們須要分析html文件,這裏選擇使用BeautifulSoup。BeautifulSoup是一個能夠從HTML或XML文件中提取數據的Python庫,至關強大。具體使用官網上有很詳細的文檔,這裏再也不贅述。在使用BeautifulSoup前,咱們還得找出資源連接的規律,方便咱們過濾。

其中課程每週的總題目爲class=course-item-list-header的div標籤下,每週的課程均在class=course-item-list-section-list的ul標籤下,每節課程在一個li標籤中,課程資源則在li標籤中的div標籤中。

查看了幾門課程以後,發現過濾資源連接的方法很簡單,以下:

  1. ppt和ppt資源:用正則表達式匹配連接;

  2. 字幕資源:找到title="Subtitles (srt)"的標籤,取其href屬性;

  3. 視頻資源:找到title="Video (MP4)"的標籤,取其href屬性便可。

字幕和視頻也能夠用正則表達式過濾,不過用BeautifulSoup根據title屬性來匹配,有更好的易讀性。而ppt和pdf資源,沒有固定的title屬性,只好利用正則表達式來匹配。

具體代碼以下:

soup = BeautifulSoup(content)
chapter_list = soup.find_all("div", class_="course-item-list-header")
lecture_resource_list = soup.find_all("ul", class_="course-item-list-section-list")

ppt_pattern = re.compile(r'https://[^"]*\.ppt[x]?')
pdf_pattern = re.compile(r'https://[^"]*\.pdf')
for lecture_item, chapter_item in zip(lecture_resource_list, chapter_list):
    # weekly title
    chapter = chapter_item.h3.text.lstrip()

    for lecture in lecture_item:
        lecture_name = lecture.a.string.lstrip()

        # get resource link
        ppt_tag = lecture.find(href=ppt_pattern)
        pdf_tag = lecture.find(href=pdf_pattern)
        srt_tag = lecture.find(title="Subtitles (srt)")
        mp4_tag = lecture.find(title="Video (MP4)")
        print ppt_tag["href"], pdf_tag["href"]
        print srt_tag["href"], mp4_tag["href"]

下載資源

既然已經獲得了資源連接,下載部分就很容易了,這裏我選擇使用curl來下載。具體思路很簡單,就是輸出curl resource_link -o file_name到一個種子文件中去,好比到feed.sh中。這樣只須要給種子文件執行權限,而後運行種子文件便可。

爲了便於歸類課程資源,能夠爲課程每週的標題創建一個文件夾,以後該周的全部課程均下載在該目錄下。爲了方便咱們快速定位到每節課的全部資源,能夠把一節課的全部資源文件均命名爲課名.文件類型。具體的實現比較簡單,這裏再也不給出具體程序了。能夠看一下一個測試例子中的feed.sh文件,部份內容以下:

mkdir 'Week 1: Introduction, Protocols, and Layering'
cd 'Week 1: Introduction, Protocols, and Layering'
curl https://d396qusza40orc.cloudfront.net/comnetworks/lect/1-readings.pdf -o '1-1 Goals and Motivation (15:46).pdf'
curl https://class.coursera.org/comnetworks-002/lecture/subtitles?q=25_en&format=srt -o '1-1 Goals and Motivation (15:46).srt'
curl https://class.coursera.org/comnetworks-002/lecture/download.mp4?lecture_id=25 -o '1-1 Goals and Motivation (15:46).mp4'
curl https://d396qusza40orc.cloudfront.net/comnetworks/lect/1-readings.pdf -o '1-2 Uses of Networks (17:12).pdf'
curl https://class.coursera.org/comnetworks-002/lecture/subtitles?q=11_en&format=srt -o '1-2 Uses of Networks (17:12).srt'
curl https://class.coursera.org/comnetworks-002/lecture/download.mp4?lecture_id=11 -o '1-2 Uses of Networks (17:12).mp4'

到這裏爲止,咱們已經成功完成爬取Coursera課程資源的目標,具體的代碼放在gist上。使用時,咱們只須要運行程序,並把課程名稱做爲參數傳遞給程序就能夠了(這裏的課程名稱並非整個課程的完整名字,而是在課程介紹頁面地址中的縮略名字,好比Computer Networks這門課,課程名稱是comnetworks-002)。

其實,這個程序能夠看作一個簡單的小爬蟲程序了,下面粗略介紹下爬蟲的概念。

一點都不簡單的爬蟲

關於什麼是爬蟲,wiki上是這樣說的

A Web crawler is an Internet bot that systematically browses the World Wide Web, typically for the purpose of Web indexing.

爬蟲的整體架構圖以下(圖片來自wiki):

爬蟲的整體架構圖

簡單來講,爬蟲從Scheduler中獲取初始的urls,下載相應的頁面,存儲有用的數據,同時分析該頁面中的連接,若是已經訪問就pass,沒訪問的話加入到Scheduler中等待抓取頁面。

固然有一些協議來約束爬蟲的行爲規範,好比許多網站都有一個robots.txt文件來規定網站哪些內容能夠被爬取,哪些不能夠。

每一個搜索引擎背後都有一個強大的爬蟲程序,把觸角伸到網絡中的全部角落,不斷去收集有用信息,並創建索引。這種搜索引擎級別的爬蟲實現起來很是複雜,由於網絡上的頁面數量太過龐大,只是遍歷他們就已經很困難了,更不要說去分析頁面信息,並創建索引了。

實際應用中,咱們只須要爬取特定站點,抓取少許的資源,這樣實現起來簡單不少。不過仍然有許多讓人頭疼的問題,好比許多頁面元素是javascript生成的,這時候咱們須要一個javascript引擎,渲染出整個頁面,再加以過濾。

更糟糕的是,許多站點都會用一些措施來阻止爬蟲爬取資源,好比限定同一IP一段時間的訪問次數,或者是限制兩次操做的時間間隔,加入驗證碼等等。絕大多數狀況下,咱們不知道服務器端是如何防止爬蟲的,因此要想讓爬蟲工做起來確實挺難的。

參考:
github:coursera-dl/coursera
github:coursera-downloader
python爬取頁面元素失敗
Wiki: Web crawler
Python 爬蟲如何入門學習?

相關文章
相關標籤/搜索