爬蟲的基本框架


最近看過不少講爬蟲的教程[1][2],基本都是一個模式:

  1. 開始先來拿正則、lxml、jquery/pyquery等等教大家從頁面上摳出一個一個的值來
  2. 然后深入一些在講講http 協議,講講怎么拿出 cookie 來模擬登錄之類的,講講基本的反爬蟲和反反爬蟲的方法
  3. 最后在上一個 簡單地 scrapy 教程,似乎就皆大歡喜了。

具體地采集一個一個的數據的確讓人產生成就感,然而這些教程卻都忽略了爬蟲最核心的邏輯抽象,也就是「爬蟲應該采取什么樣的策略遍歷網頁」。其實也很簡單,只需要兩個隊列和一個集合,Scrapy 等框架拆開來看也是如此,本文參照 Scrapy 實現一個最基礎的通用爬蟲。

萬維網是由一個一個的頁面構成的,而每個頁面和頁面之間是由鏈接來聯系的,並且這些鏈接都是具有方向性的。對應到數據結構的話,我們可以把每一個頁面都看作一個節點,而每一個鏈接都是一個有向邊,也就是整個萬維網其實是一個巨大的「有向圖」[3]。說到這里,可能有的同學已經明白了,可以用廣度優先或者深度優先的算法來遍歷這個圖。當然,這個圖是在太巨大了,我們不可能遍歷整個圖,而是加一些限定條件,只去訪問其中很小一部分我們感興趣的節點,比如某個域名下的網頁。

廣度優先和深度優先都可以使用遞歸或者輔助的隊列(queue/lifo_queue)來實現。然而如果你的爬蟲是用 python 寫的話,很遺憾不能使用遞歸來實現了,原因很簡單,我們要訪問的網頁可能成千上萬,如果采用遞歸來實現,那么爬蟲每向前訪問一個節點,系統的調用棧就會 +1,而 python 中至今沒有尾遞歸優化,默認的堆棧深度為1000,也就是很可能你訪問了1000個網頁之后就拋出異常了。所以我們這里使用隊列實現對網頁的遍歷訪問。

理論知識說了這么多,下面以一個例子來說明一下如何爬取數據:爬取煎蛋網的妹子圖: http://jandan.net/ooxx

首先,我們打開對應的網址,作為起始頁面,也就是把這個頁面放入待訪問的頁面的隊列。注意,這是我們需要的第一個隊列,存放我們的待訪問頁面。

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。實際上,對這個例子來說是可以的,但是,這種方法又回到了對於每個站點都去尋找站點規律的老路,這並不是一種通用的做法。

在對應的按鈕上點擊右鍵,選擇審查元素(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/圖_(數學)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM