pyspider 實戰專案之爬取去哪兒

痴海發表於2018-07-07

閱讀文字大概需要 13 分鐘。

通過之前的文章介紹,你現在應該對 pyspider 有了一定的認識。如果你還不清楚的話,可以再回顧下之前的文章「高效率爬蟲框架之 pyspider」。務必要對 pysdpier 有個整體認知,這樣你的學習效率才會高。
現在我們用一個實戰專案,來進一步掌握 pyspider 框架的使用。此次的專案爬取的目標是「去哪兒網」,我要將所有攻略的作者、標題、出發日期、人均費用、攻略正文等儲存下來,儲存到 MongoDB 中。

1 準備工作

請確保已經安裝了 pyspider 和 PhantomJS,安裝好了 MongoDB 並正常執行服務,還需要安裝 PyMongo 庫。這些教程網上都有詳細資料,大家自行搜尋。

2 啟動 pyspider

執行如下命令就可以啟動 pyspider:

pyspider all
複製程式碼

執行效果:

這樣可以啟動 pyspider 的所有元件,包括 PhantomJS、ResultWorker、Processer、Fetcher、Scheduler、WebUI,這些都是 pysipder 執行必備的元件。最後一行輸出 WebUI 執行在 5000 埠上。可以開啟瀏覽器,輸入連結 http://localhost:5000,這時我們會看到頁面。

此頁面便是 pyspider 的 WebUI,我們可以用它來管理專案、編寫程式碼、線上除錯、監控任務等

3 建立專案

新建一個專案,點選右邊的 Create 按鈕,在彈出的浮窗裡輸入專案的名稱和爬取的連結,再點選 create 按鈕,這樣就成功建立了一個專案。

接下來會看到 pyspider 的專案編輯和除錯頁面

左側就是程式碼的除錯頁面,點選左側右上角的 run 單步除錯爬蟲程式,在左側下半部分可以預覽當前的爬取頁面。右側是程式碼編輯頁面,我們可以直接編輯程式碼和儲存程式碼,不需要藉助於 IDE。
注意右側,pyspider 已經幫我們生成了一段程式碼。程式碼如下所示:

from pyspider.libs.base_handler import *


class Handler(BaseHandler):
    crawl_config = {
    }

    @every(minutes=24 * 60)
    def on_start(self):
        self.crawl('http://travel.qunar.com/travelbook/list.htm', callback=self.index_page)

    @config(age=10 * 24 * 60 * 60)
    def index_page(self, response):
        for each in response.doc('a[href^="http"]').items():
            self.crawl(each.attr.href, callback=self.detail_page)

    @config(priority=2)
    def detail_page(self, response):
        return {
            "url": response.url,
            "title": response.doc('title').text(),
        }
複製程式碼

這裡的 Handler 就是 pyspider 爬蟲的主類,我們可以在此處定義爬取、解析、儲存的邏輯。整個爬蟲的功能只需要一個 Handler 即可完成。

接下來我們可以看到一個 crawl_config 屬性。我們可以將本專案的所有爬取配置統一定義到這裡,如定義 Headers、設定代理等,配置之後全域性生效。

然後,on_start() 方法是爬取入口,初始的爬取請求會在這裡產生,該方法通過呼叫 crawl() 方法即可新建一個爬取請求,第一個引數是爬取的 URL,這裡自動替換成我們定義的 URL。crawl() 方法還有一個 callback,它指定了這個頁面爬取成功後用哪個方法進行解析,程式碼中指定為 index_page() 方法,即如果這個 URL 對應的頁面爬取成功了,那 Response 將交給 index_page() 方法解析。

index_page() 方法恰好接收這個 Response 引數,Response 對接了 pyquery。我們直接呼叫 doc() 方法傳入相應的 CSS 選擇器,就可以像 pyquery 一樣解析此頁面,程式碼中預設是 a[href^="http"],也就是說該方法解析了頁面的所有連結,然後將連結遍歷,再次呼叫了 crawl() 方法生成了新的爬取請求,同時再指定了 callback 為 detail_page,意思是說這些頁面爬取成功了就呼叫 detail_page() 方法解析。這裡,index_page() 實現了兩個功能,一是將爬取的結果進行解析,二是生成新的爬取請求。

detail_page() 同樣接收 Response 作為引數。detail_page() 抓取的就是詳情頁的資訊,就不會生成新的請求,只對 Response 物件做解析,解析之後將結果以字典的形式返回。當然我們也可以進行後續處理,如將結果儲存到資料庫。

接下來,我們改寫一下程式碼來實現攻略的爬取。

4 爬取首頁

點選左欄右上角的 run 按鈕,即可看到頁面下方 follows 便會出現一個標註,其中包含數字 1 ,這代表有新的爬取請求產生。

左欄左上角會出現當前 run 的配置文章,這裡有一個 callback 為 on_start,這說明點選 run 之後實際是執行了 on_start() 方法。在 on_start() 方法中,我們利用 crawl() 方法生成一個爬取請求,那下方 follows 部分的數字 1 就代表了這一個爬取請求。

點選下方的 follows 按鈕,即可看到生成的爬取請求的連結。每個連結的右側還有一個箭頭按鈕。

點選該箭頭,我們就可以對此連結進行爬取,也就是爬取攻略的首頁內容。

上方的 callback 已經變成了 index_page,這就代表當前執行了 index_page() 方法。index_page() 接收到的 response 引數就是剛才生成的第一個爬取請求的 Response 物件。index_page() 方法通過呼叫 doc() 方法,傳入提取所有 a 節點的 CSS 選擇器,然後獲取 a 節點的屬性 href,這樣實際上就是獲取了第一個爬取頁面中的所有連結。然後在 index_page() 方法裡遍歷了所有連結,同時呼叫 crawl() 方法,就把這一個個的連結構造成新的爬取請求了。所以最下方 follows 按鈕部分有 231 的數字標記,這代表新生成了 231 個爬取請求,同時這些請求的 URL 都呈現在當前的頁面了。

再點選下方的 web 按鈕,即可預覽當前爬取結果的頁面。

這裡編輯器並不是很友好,顯示的頁面只有一小些,但並不會妨礙我們的抓取。當前看到的頁面結果和瀏覽器看到的幾乎是完全一致的,在這裡我們可以方便地檢視頁面請求的結果。

點選 html 按鈕即可檢視當前頁面的原始碼。

我們剛才在 index_page() 方法中提取了所有的連結並生成了新的爬取請求。但是很明顯要爬取的肯定不是所有連結,只需要攻略詳情的頁面連結就夠了,所以我們要修改一下當前 index_page() 裡提取連結時的 CSS 選擇器。

在右側程式碼選中要更改的區域,點選左欄的右箭頭,此時在上方出現的標題的 CSS 選擇器就會被替換到右側程式碼中。

這樣就成功完成了 CSS 選擇器的替換,非常方便。
重新點選左欄右上角的 run 按鈕,即可重新執行 index_page() 方法。此時的 follows 就變成了 10 個,也就是說現在我們提取的只有當前頁面的 10 個攻略。

我們現在抓取的只是第一頁的內容,還需要抓取後續頁面,所以還需要一個爬取連結,即爬取下一頁的攻略列表頁面。我們再利用 crawl() 方法新增下一頁的爬取請求,在 index_page() 方法裡面新增如下程式碼,然後點選 save() 儲存。

next = response.doc('.next').attr.href
self.crawl(next, callback=self.index_page)
複製程式碼

利用 CSS 選擇器選中下一頁的連結,獲取它的 href 屬性,也就獲取了頁面的 URL。然後將該 URL 傳給 crawl() 方法,同時指定回撥函式,注意這裡回撥函式仍然指定為 index_page() 方法,因為下一頁的結構與此頁相同。

重新點選 run 按鈕,這時就可以看到 11 個爬取請求。follows 按鈕上會顯示 11,這就代表我們成功新增了下一頁的爬取請求。

現在,索引列表頁面的解析過程我們就完成了。

5 爬取詳情頁

任意選取一個詳情頁進入,點選前 10 個爬取請求的任意一個的右箭頭,執行詳情頁的爬取。

切換到 Web 頁面預覽效果,頁面下拉之後,頭圖正文中的一些圖片一直顯示載入中。

檢視原始碼,我們沒有看到 img 節點。

出現此現象的原因是 pyspider 預設傳送 HTTP 請求,請求的 HTML 文件本身就不包含 img 節點。但是在瀏覽器中我們看到了圖片,這是因為這張圖片是後期經過 JavaScrpit 出現的。那麼,我們該如何獲取呢?

幸運的是,pyspider 內部對接了 PhatomJS,那麼我們只需要修改一個引數即可。

我們將 index_page() 中生成抓取詳情頁的請求方法新增一個引數 fetch_type,改寫的 index_page() 變為如下內容:

def index_page(self, response):
        for each in response.doc('li > .tit > a').items():
            self.crawl(each.attr.href, callback=self.detail_page, fetch_type='js')
        next = response.doc('.next').attr.href
        self.crawl(next, callback=self.index_page)
複製程式碼

接下來,我們來試試它的抓取效果。

點選左欄上方的左箭頭返回,重新呼叫 index_page() 方法生成新的爬取詳情頁的 Request。

再點選新生成的詳情頁的 Request 的爬取按鈕,這時我們便可以看到頁面變成了這樣子。

圖片被成功渲染處理,這就是啟用了 PhantomJS 渲染後的結果。只需要新增一個 fetch_type 引數即可,這非常方便。
最後就是將詳情頁面中需要的資訊提取處理。最終的 detail_page() 方法改寫如下:

  def detail_page(self, response):
        return {
            "url": response.url,
            "title": response.doc('#booktitle').text(),
            "date": response.doc('.when .data').text(),
            "day": response.doc('.howlong .data').text(),
            "who": response.doc('.who .data').text(),
            "text": response.doc('#b_panel_schedule').text(),
            "image": response.doc('.cover_img').attr.src,
        }
複製程式碼

我們分別提取了頁面的連結、標題、出行日期、出現天數、人物、攻略正文、頭圖資訊,將這些資訊構造成一個字典。
重新執行,即可發現輸出結果。

左欄中輸出了最終構造的字典資訊,這就是一篇攻略的抓取結果。

6 啟動爬蟲

返回爬蟲的主頁面,將爬蟲的 status 設定成 DEBUG 或 RUNNING,點選右側的 Run 按鈕即可開始爬取。

在最左側我們可以定義專案的分組,以方便管理。rate/burst 代表當前的爬取速率。rate 代表 1 秒發出多少個請求,burst 相當於流量控制中的令牌桶演算法的令牌數,rate 和 burst 設定的越大,爬取速率越快,當然速率需要考慮本機效能和爬取過快被封的問題。process 中的 5m、1h、1d 指 的是最近 5 分、1 小時、1 天內的請求情況,all 代表所有的請求情況。請求由不同顏色表示、藍色的代表等待被執行的請求,綠色的代表成功的請求,黃色的代表請求失敗後等待重試的請求,紅色的代表失敗次數過多而被忽略的請求,這樣可以直觀知道爬取的進度和請求情況。

點選 Active Tasks,即可檢視最近請求的詳細狀況。

點選 Result,即可檢視所有的爬取結果。

點選右上角的按鈕,即可獲取資料的 JSON、CSV 格式。

本文首發於公眾號「痴海」,每天分享 python 乾貨,回覆「1024」,你懂得。


pyspider 實戰專案之爬取去哪兒

相關文章