1. 程式人生 > >[爬蟲架構]使用scrapy-redis+multiprocessing構建超級爬蟲

[爬蟲架構]使用scrapy-redis+multiprocessing構建超級爬蟲

前言:

       這段時間研究了一下scrapy-redis元件,有些個人實踐經歷和想法,現將其變成文字呈現出來。

        前方高能預警:

                1、閱讀本文章的知識內需為:熟練使用scrapy,瞭解scrapy的排程器、去重器工作原理;瞭解redis的基本操作

                2、本文篇幅較長但耐心看完相信對你有不小的幫助,請備好瓜子板凳and飲料,寫的不好請拍磚

       瞭解scrapy-redis元件的朋友都知道,它的出現是為了彌補scrapy框架的天生缺陷,即不能分散式部署。scrapy-redis不能單獨部署,只作為scrapy的拓展元件存在。也不懂scrapy的開發人員是怎麼想的,但好在總有些有能人士願意來做優化scrapy的工作,並將成果開源釋出。

        scrapy-redis的主要作用無非兩點:

①、重寫了scrapy的排程類(scrapy.core.scheduler.Scheduler)和 佇列類(queuelib.queue),scrapy的排程類的作用主要是排程scrapy.http.Request物件,具體來說是根據settings中的'SCHEDULER_PRIORITY_QUEUE

'這個變數(預設變數值是queuelib.PriorityQueue)來決定使用什麼佇列規則排程Request物件,而佇列是存放在主機的記憶體資源中,當scrapy程序停掉(任何原因導致的)後,佇列中的Request物件將消失,在進行大規模爬蟲專案時,這是不能容忍的,所有必須將Request佇列的存放位置改一下,一個很好的選擇就是RedisDB:鍵值對儲存結構,工作時所有資料存放於記憶體中,可自定義規則將資料寫入磁碟即持久化操作(建議採用AOF方式)。如此一來,所有的Request物件到存放到資料庫中去了,那構建分散式爬蟲的關鍵難點就已經解決了,我們可以部署單機多爬蟲/多機多爬蟲的方式來組成一個大型的爬蟲架構,目標是統一的。它們是如何協同工作的:很簡單,每個爬蟲可以生產Request和消費Request,只是要先把Request送入redis中,再去redis中取出來消費。就是把Request集中存放到redis中了,大家統一排隊去redis中領取就好了,至於排隊規則,我們無需理會,這是計算機自身的排程決定,反正最終目的是把redis中的Request消費完就行。

②、重寫了scrapy的去重類(scrapy.dupefilters.RFPDupeFilter),去重類的作用主要是把所有生產過的Request物件的指紋(將Request物件經過hash函式計算生成的一串長度為40的包括有數字和字母的字串,指紋包括這個物件的headers、請求方式、body、url資訊)寫入python中的自動去重的集合set()中,當然這個類也可以將指紋寫入檔案。但是就算能寫入檔案持久化,那分散式部署的時候,每分每秒都在生產Request,那不是每次都去讀這個檔案?IO延時很容易造成瓶頸甚至導致檔案損壞以至於scrapy報錯。所以你可能想到了,Request佇列都已經放到redis中了,當然也會把它的指紋庫放入redis中,我們只要維護好這個資料庫就行了。這樣一來,分散式部署的問題得以解決。

    以上介紹了scrapy-redis的主要作用和內容。接下來說說一般的分散式爬蟲存在哪些問題:

        有較多scrapy開發經驗的朋友應該瞭解,在單臺主機上部署一個scrapy任務的時候,主機的資源消耗極低,效率極慢。其實這個問題在分散式爬蟲架構(多機單爬蟲)中仍未解決(可以在執行scrapy時通過Telnet本機6023埠來檢視Request物件數量,持續為一個較低值,詳細見官文),即使在settings中配置了併發請求數‘CONCURRENT_REQUESTS’或者單域名併發請求數‘CONCURRENT_REQUESTS_PER_DOMAIN’仍收效甚微,因為'DOWNLOAD_DELAY',雖然併發請求數上去了,但是程式並沒有併發下載response。所以即使在每臺主機上部署一個爬蟲也只是浪費資源,效率得不到太大提升。你可能已經先想到了,那就一臺主機上部署多個爬蟲唄,沒錯,思路很正確。這篇文章主要說的就是如何在單機上部署多個爬蟲來爬取一個網站,只有在單機上把爬取效率提升上去,多機的分散式部署才有意義。

超級爬蟲:

    邏輯拓撲圖如下:


    我的畫圖水平應該還跟得上,大家湊合著看吧,哈哈。。整個分散式爬蟲的邏輯結構就是這張圖了,說說幾個細節:

        1、任務程序區別於程序池。程序池是python多程序的Pool,任務程序是debug.py檔案,詳細後面有介紹

        2、mongo資料庫是適用於儲存非結構化、複雜型別的資料,所以還挺適合爬蟲

    超級爬蟲部署步驟:

                1、寫好一個完整的scrapy專案,比如下圖(看不清就儲存放大看):


      2、安裝scrapy-redis,在yourspider.py主程式中,原本繼承scrapy的爬蟲類改為scrapy-redis的類‘RedisCrawlSpider’or  ‘Spider’

                3、在settings中做如下配置(custom_settings):

        'SCHEDULER_PERSIST':True,
        'DUPEFILTER_CLASS':'scrapy_redis.dupefilter.RFPDupeFilter',
        'SCHEDULER':'scrapy_redis.scheduler.Scheduler',
        'REDIS_HOST':'your_redis_server_ip', #如192.168.0.1
        'REDIS_PORT':6379,
        'REDIS_START_URLS_KEY':'%(name)s:start_urls',

                如果你是在settings.py中配置,把冒號改等號,去掉逗號

                4、按照上面的redis配置,準備好你的redis服務,提前確認好爬蟲程式所在的主機能夠與redis伺服器通訊

                5、在spiders資料夾下建立debug.py檔案,內容如下:

from scrapy.cmdline import execute
import os,sys
from time import sleep
from multiprocessing import Pool

sys.path.append(os.path.dirname(__file__))
#execute('scrapy crawl your_spider_name'.split(' '))

def run_spider(number):
    print 'spider %s is started...'%number
    execute('scrapy runspider your_spider_name.py'.split(' '))  #修改此處

if __name__ == '__main__':
    p =Pool(20)
    for i in range(20):
        p.apply_async(run_spider,args=(i,))
        sleep(1)
    p.close()
    p.join()

                6、執行debug.py檔案,正常輸出:


              啟動了20個程序,應該有【spider0-19】個字樣出現在DEBUG日誌中,注意settings中的'LOG_LEVEL'暫時不要修改,預設就是DEBUG。且必須看到127.0.0.1:port這樣的日誌出現才能表示所有程序全部正常啟動,第一次執行時程序數為20比較合適,一臺8g記憶體,i5-8250的電腦可以開到55個程序作用。超出數量會有報錯日誌,這個自己注意觀察。

                7、進入redis控制檯,將start_urls送入:

[email protected]:~$ redis-cli 
127.0.0.1:6379> lpush your_spider_name:start_urls http://something.com/page1
(integer) 1
127.0.0.1:6379> 

                    然後觀察pycharm控制檯輸出,大概2秒後應當有如下輸出:


                    即提示從redis中查詢到起始url了,現將從這條url開始爬取。到此,一個簡單的單機單任務多程序爬蟲就啟動了,事實上這也叫做分散式爬蟲,在邏輯上是這樣。超級爬蟲是什麼,將這個單機單任務爬蟲變成多機多工爬蟲就是“超級爬蟲”!(我命名的,哈哈),抓取效率對比單任務scrapy爬蟲快了幾十倍,這還僅僅是單機單任務模式。

優點:

        1、充分利用主機的硬體資源

        2、繼承多程序的優勢。一個是程序之間任何物件都是相互隔離的,一個程序掛掉不會影響其他程序(目前我操作過的超級爬蟲不間斷執行時間超過2天,還沒有出現程序掛掉的現象)

        3、極大提升爬取效率,節省大量的時間成本。

監控:

        當我們構建了一個大型的超級爬蟲架構後,會產生一些新的問題(目前想到的):

                1、原本的scrapy的依賴於訊號‘scrapy.signals’工作的擴充套件類就不存在實際意義了

                2、代理ip呼叫頻率受平臺限制,如果是呼叫平臺介面的話

        監控scrapy的狀態主要依賴於它的signals,當使用多程序後,我們要監控的是這個程序池內的所有scrapy任務的狀態,但很無奈,我目前還沒想到怎麼做這個。

        其實我們根本不用去監控單個scrapy程序或者是單個任務的狀態,因為它們的角色僅僅是我超級爬蟲“大軍”中的一小部分,極少機率會大規模程序掛掉(目前還沒遇到過,已連續執行超過30個小時)。我們需要監控的是超級爬蟲架構中一種特殊的狀態:

    真/偽空佇列狀態

                在一般的分散式爬蟲架構中,我們一般是在Redis中通過以下方式查詢Requests佇列的長度:
127.0.0.1:6379> SCARD yunqi:dupefilter
(integer) 527516
127.0.0.1:6379> ZCARD yunqi:requests
(integer) 14023 (這裡其實是0)
 通過Requests的長度來監控整個分散式爬蟲架構的排程器是否處理空閒狀態,一旦處於空閒狀態,將會有一個預設的空閒時間T(在settings中設定' SCHEDULER_IDLE_BEFORE_CLOSE'),T(秒)後scrapy認為爬取結束,程序自動終止。但是在超級爬蟲架構中,即使redis中的Request數量為0也不一定爬取任務結束了,它執行的常態是這樣的:

              1、我們在redis中手動查詢Request佇列的數量幾乎會始終為0,但爬蟲卻一直在工作。因為程序數量多達幾十上百個,剛生產出來的Request立馬被自己或其他程序給拿走消費了,我們很少有機會能看到redis中的Request佇列大於0,即偽空佇列狀態

              2、爬取一段時間後(通常十幾個小時,由啟動方式和程序數量決定),在pycharm控制檯看到這樣的狀態:


即任務中的所有scrapy程序一段時間內都抓取不到item了(必須連續全部為0)。這個狀態意味著真的沒有Request物件可供消費了。往往這個時候實際的爬取任務並沒有完成,導致這種狀態的原因很簡單,供不應求,爬蟲部隊已經生產不過來了。此即真-空佇列狀態

如何讓爬蟲大軍繼續爬取?還記得我們是如何讓超級爬蟲啟動的嗎?是的,往redis的start_urls送入url。那送入哪個url呢?例舉一個場景:我想要爬取一個小說網站(雲起書院)的所有圖書的基本資訊,這個網站有一個入口,類似下圖:


每個頁面有10本小說,頁面的url是規律性的,xxx/page1,xxx/page2,那麼當遇到真-空佇列狀態時,我可以去mongoDB中查詢的已爬取了多少本小說,比如2300本,那麼已爬取的最後一個url應該是xxx/page230,且這個url已經被加入去重集合了,需要我們送入下一個有效的url應該是xxx/page231,或者是232(可能有小的誤差)。所以我們可以通過“超級爬蟲”章節第7步往redis中push新的URL,然後觀察pycharm控制檯日誌輸出,如果提示被去重,則增加頁碼號繼續送入新的URL直到爬蟲繼續工作為止。說到這裡相信你已經有如何編寫監控指令碼的思路了,即在指令碼中先通過查詢item儲存資料庫中的數量是否一個時間段內都沒有變化(建議10分鐘,最好自行除錯確認最佳引數)

代理:

       超極爬蟲架構充分利用了每臺主機的硬體資源(主要是記憶體),在每臺主機上都運行了幾十個甚至更多的scrapy程序,這意味著爬蟲執行時,一個IP同時向一個網站傳送了大量的請求(成百上千,與scrapy程序數相關)。這樣頻率的併發請求,不使用代理幾乎是不可能的,除非網站沒有IP限制的反爬策略。

       如此高的併發請求量,意味著代理IP的質量和請求速度將直接影響爬取效率。在此推薦幾個好用的代理平臺:芝麻HTTP代理、快代理、訊代理。有一篇不錯的文章進行了各大代理平臺IP的質量、價格測試,傳送門。為此需要在本地維護一個IP快取池,用於在本地維護一批可用的代理IP實時供給分散式爬蟲使用,關於快取池的介紹請參考我的另一篇文章

待續。。。