最近看過很多講爬蟲的教程[1][2],基本都是一個模式:html
具體地採集一個一個的數據的確讓人產生成就感,然而這些教程卻都忽略了爬蟲最核心的邏輯抽象,也就是「爬蟲應該採起什麼樣的策略遍歷網頁」。其實也很簡單,只須要兩個隊列和一個集合,Scrapy 等框架拆開來看也是如此,本文參照 Scrapy 實現一個最基礎的通用爬蟲。python
萬維網是由一個一個的頁面構成的,而每一個頁面和頁面之間是由連接來聯繫的,而且這些連接都是具備方向性的。對應到數據結構的話,咱們能夠把每個頁面都看做一個節點,而每個連接都是一個有向邊,也就是整個萬維網實際上是一個巨大的「有向圖」[3]。說到這裏,可能有的同窗已經明白了,能夠用廣度優先或者深度優先的算法來遍歷這個圖。固然,這個圖是在太巨大了,咱們不可能遍歷整個圖,而是加一些限定條件,只去訪問其中很小一部分咱們感興趣的節點,好比某個域名下的網頁。jquery
廣度優先和深度優先均可以使用遞歸或者輔助的隊列(queue/lifo_queue)來實現。然而若是你的爬蟲是用 python 寫的話,很遺憾不能使用遞歸來實現了,緣由很簡單,咱們要訪問的網頁可能成千上萬,若是採用遞歸來實現,那麼爬蟲每向前訪問一個節點,系統的調用棧就會 +1,而 python 中至今沒有尾遞歸優化,默認的堆棧深度爲1000,也就是極可能你訪問了1000個網頁以後就拋出異常了。因此咱們這裏使用隊列實現對網頁的遍歷訪問。ios
理論知識說了這麼多,下面以一個例子來講明一下如何爬取數據:爬取煎蛋網的妹子圖: http://jandan.net/ooxxgit
首先,咱們打開對應的網址,做爲起始頁面,也就是把這個頁面放入待訪問的頁面的隊列。注意,這是咱們須要的第一個隊列,存放咱們的待訪問頁面。github
class MiniSpider(object): def __init__(self): self._request_queue = queue.Queue() # 帶請求頁面的隊列 self._request_queue.put('http://jandan.net/ooxx') # 把第一個待訪問頁面入隊
接着,咱們先不考慮具體如何從頁面上抽取咱們須要的內容,而是考慮如何遍歷待訪問的頁面。咱們發現能夠經過頁面上的翻頁按鈕找到下一頁的連接,這樣一頁接着一頁,就能夠遍歷全部的頁面了。算法
固然,對這個頁面,你可能想到,其實咱們只要獲取了頁面的個數,而後用程序生成一下不就行了嗎?好比說第一http://jandan.net/ooxx/page-1,第二頁是http://jandan.net/ooxx/page-2。實際上,對這個例子來講是能夠的,可是,這種方法又回到了對於每一個站點都去尋找站點規律的老路,這並非一種通用的作法。cookie
在對應的按鈕上點擊右鍵,選擇審查元素(inspect),能夠看到對應 html 元素的代碼。咱們經過 xpath 來選擇對應的節點,來獲取下一頁的連接。若是你還不瞭解 xpath,建議你去 Mozilla Developer Network [4] 上學習一個,提升下自身姿式水平。數據結構
經過 xpath 表達式 //div[@class='comments']//a/@href 咱們得到了全部通向上一頁下一頁的連接。你能夠在第二頁和第三頁上驗證一下。框架
class MiniSpider(object): def __init__(self): self._request_queue = queue.Queue() # 帶請求頁面的隊列 self._request_queue.put('http://jandan.net/ooxx') # 把第一個待訪問頁面入隊 def run(self): while True: url = self._request_queue.get() rsp = download(url) new_urls = get_xpath(rsp, "//a") # 新的待訪問的頁面 map(self._request_queue.put, new_urls) # 放入隊列
這時候,你可能想到了另外一個問題,第一頁的下一頁和第三頁的上一頁都是同一個頁面——第二頁。若是不加處理的話,咱們就會重複屢次訪問一個頁面,浪費資源不說,還有可能致使爬蟲迷路,在幾個頁面之間循環訪問。這時候咱們就須要一個集合,把訪問過得頁面放入。從而避免重複訪問。
class MiniSpider(object): def __init__(self): self._request_queue = queue.Queue() # 帶請求頁面的隊列 self._request_queue.put('http://jandan.net/ooxx') # 把第一個待訪問頁面入隊 self._dedup_set = set() # 已經訪問過得頁面集合 def run(self): while True: url = self._request_queue.get() rsp = download(url) self._dedup_set.add(url) # 訪問過了,加入 new_urls = get_xpath(rsp, "//a") # 新的待訪問的頁面 for new_url in new_urls: if new_url not in self._dedup_set: # 若是尚未訪問過 self._request_queue.put(new_url) # 放入隊列
好了,既然咱們能夠遍歷須要爬取得頁面了,下一步咱們開始考慮從頁面抽取須要的數據了。咱們依然請出咱們的老朋友xpath了。在須要的元素上點擊右鍵,編寫對應的表達式就能夠了。在這個例子裏,咱們須要獲取的是圖片,對於圖片的下載也是一件很耗時的任務,若是能在另外一個線程裏進行就行了,因此這裏咱們引入第二個隊列,存放抽取出來的數據。
class MiniSpider(object): def __init__(self): self._request_queue = queue.Queue() # 帶請求頁面的隊列 self._request_queue.put('http://jandan.net/ooxx') # 把第一個待訪問頁面入隊 self._item_queue = queue.Queue() self._dedup_set = set() # 已經訪問過得頁面集合 def run_request(self): while True: url = self._request_queue.get() rsp = download(url) self._dedup_set.add(url) # 訪問過了,加入 new_urls = get_xpath(rsp, "//a") # 新的待訪問的頁面 for new_url in new_urls: if new_url not in self._dedup_set: # 若是尚未訪問過 self._request_queue.put(new_url) # 放入隊列 items = get_xpath(rsp, "//img/@src") # 抽取出來的圖片地址 map(self._item_queue, items) def run_item(self): while True: image = self._item_queue.get() download(image)
把 run_request 和 run_item 兩個函數放到不一樣的線程中,就能夠同時遍歷網頁和下載圖片了。
好了,到這裏咱們的煎蛋妹子圖爬蟲就寫好了,實際上全部的爬蟲框架無論多麼複雜,使用的異步等等不一樣的多任務模式也好,本質上都是同樣的。 Scrapy 也是採用了相似的方式,不一樣的地方時,scrapy 才使用的是 Lifo Queue,也就是棧,因此 scrapy 默認是深度優先便利的,而咱們上面的爬蟲是廣度優先遍歷的。scrapy 沒有采用線程,而是使用了 Twisted 提供的 Actor Model 實現多任務同時運行。
若是再多些幾個爬蟲以後,可能你就會發現,其實每次須要改動的地方無外乎是查找幾個 xpath 表達式,這樣咱們能夠把上面的邏輯抽象成爲一個框架,經過編寫配置文件來爬取目標數據。相關代碼參見: aiospider
好比,上面的代碼只須要以下命令:
python miniscrapy.py --spider ooxx.yml
在爬蟲運行過程當中,會遇到各類各樣的封鎖,封鎖 User-Agent, 封鎖 IP,封鎖 Cookie,可是這些封鎖都是在下載過程當中遇到的,和爬蟲的總體邏輯是無關的。本文中的邏輯能夠一直複用。對於遇到的各類各樣的封鎖,須要各類靈活多變的方式應對,這時候採用 pipeline 的方式是一個很好的選擇,下一篇文章將會介紹。
[1] http://www.jianshu.com/p/11d7da95c3ca [2] https://zhuanlan.zhihu.com/p/25296437 [3] https://zh.wikipedia.org/zh-hans/%E5%9B%BE_(%E6%95%B0%E5%AD%A6)