scrapy-redis是github上的一個開源項目,可以直接下載下傳到他的源代碼:
<a target="_blank" href="https://github.com/rolando/scrapy-redis">https://github.com/rolando/scrapy-redis</a>
scrapy-redis的官方文檔寫的比較簡潔,沒有提及其運作原理,是以如果想全面的了解分布式爬蟲的運作原理,還是得看scrapy-redis的源代碼才行(還得先了解scrapy的運作原理,不然看scrapy-redis還是比較費勁),不過scrapy-redis的源代碼很少,也比較好懂,很快就能看完。
scrapy-redis工程的主體還是是redis和scrapy兩個庫,工程本身實作的東西不是很多,這個工程就像膠水一樣,把這兩個插件粘結了起來。下面我們來看看,scrapy-redis的每一個源代碼檔案都實作了什麼功能,最後如何實作分布式的爬蟲系統:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
connect檔案引入了redis子產品,這個是redis-python庫的接口,用于通過python通路redis資料庫,可見,這個檔案主要是實作連接配接redis資料庫的功能(傳回的是redis庫的redis對象或者strictredis對象,這倆都是可以直接用來進行資料操作的對象)。這些連接配接接口在其他檔案中經常被用到。其中,我們可以看到,要想連接配接到redis資料庫,和其他資料庫差不多,需要一個ip位址、端口号、使用者名密碼(可選)和一個整形的資料庫編号,同時我們還可以在scrapy工程的setting檔案中配置套接字的逾時時間、等待時間等。
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
這個檔案看起來比較複雜,重寫了scrapy本身已經實作的request判重功能。因為本身scrapy單機跑的話,隻需要讀取記憶體中的request隊列或者持久化的request隊列(scrapy預設的持久化似乎是json格式的檔案,不是資料庫)就能判斷這次要發出的request url是否已經請求過或者正在排程(本地讀就行了)。而分布式跑的話,就需要各個主機上的scheduler都連接配接同一個資料庫的同一個request池來判斷這次的請求是否是重複的了。
在這個檔案中,通過繼承basedupefilter重寫他的方法,實作了基于redis的判重。根據源代碼來看,scrapy-redis使用了scrapy本身的一個fingerprint接request_fingerprint,這個接口很有趣,根據scrapy文檔所說,他通過hash來判斷兩個url是否相同(相同的url會生成相同的hash結果),但是當兩個url的位址相同,get型參數相同但是順序不同時,也會生成相同的hash結果(這個真的比較神奇。。。)是以scrapy-redis依舊使用url的fingerprint來判斷request請求是否已經出現過。這個類通過連接配接redis,使用一個key來向redis的一個set中插入fingerprint(這個key對于同一種spider是相同的,redis是一個key-value的資料庫,如果key是相同的,通路到的值就是相同的,這裡使用spider名字+dupefilter的key就是為了在不同主機上的不同爬蟲執行個體,隻要屬于同一種spider,就會通路到同一個set,而這個set就是他們的url判重池),如果傳回值為0,說明該set中該fingerprint已經存在(因為集合是沒有重複值的),則傳回false,如果傳回值為1,說明添加了一個fingerprint到set中,則說明這個request沒有重複,于是傳回true,還順便把新fingerprint加入到資料庫中了。
dupefilter判重會在scheduler類中用到,每一個request在進入排程之前都要進行判重,如果重複就不需要參加排程,直接舍棄就好了,不然就是白白浪費資源。
這裡實作了loads和dumps兩個函數,其實就是實作了一個serializer,因為redis資料庫不能存儲複雜對象(value部分隻能是字元串,字元串清單,字元串集合和hash,key部分隻能是字元串),是以我們存啥都要先串行化成文本才行。這裡使用的就是python的pickle子產品,一個相容py2和py3的串行化工具。這個serializer主要用于一會的scheduler存reuqest對象,至于為什麼不實用json格式,我也不是很懂,item pipeline的串行化預設用的就是json。
pipeline檔案實作了一個item pipieline類,和scrapy的item pipeline是同一個對象,通過從settings中拿到我們配置的redis_items_key作為key,把item串行化之後存入redis資料庫對應的value中(這個value可以看出出是個list,我們的每個item是這個list中的一個結點),這個pipeline把提取出的item存起來,主要是為了友善我們延後處理資料。
該檔案實作了幾個容器類,可以看這些容器和redis互動頻繁,同時使用了我們上邊picklecompat中定義的serializer。這個檔案實作的幾個容器大體相同,隻不過一個是隊列,一個是棧,一個是優先級隊列,這三個容器到時候會被scheduler對象執行個體化,來實作request的排程。比如我們使用spiderqueue最為排程隊列的類型,到時候request的排程方法就是先進先出,而實用spiderstack就是先進後出了。
我們可以仔細看看spiderqueue的實作,他的push函數就和其他容器的一樣,隻不過push進去的request請求先被scrapy的接口request_to_dict變成了一個dict對象(因為request對象實在是比較複雜,有方法有屬性不好串行化),之後使用picklecompat中的serializer串行化為字元串,然後使用一個特定的key存入redis中(該key在同一種spider中是相同的)。而調用pop時,其實就是從redis用那個特定的key去讀其值(一個list),從list中讀取最早進去的那個,于是就先進先出了。
這些容器類都會作為scheduler排程request的容器,scheduler在每個主機上都會執行個體化一個,并且和spider一一對應,是以分布式運作時會有一個spider的多個執行個體和一個scheduler的多個執行個體存在于不同的主機上,但是,因為scheduler都是用相同的容器,而這些容器都連接配接同一個redis伺服器,又都使用spider名加queue來作為key讀寫資料,是以不同主機上的不同爬蟲執行個體公用一個request排程池,實作了分布式爬蟲之間的統一排程。
151
152
153
154
155
156
157
這個檔案重寫了scheduler類,用來代替scrapy.core.scheduler的原有排程器。其實對原有排程器的邏輯沒有很大的改變,主要是使用了redis作為資料存儲的媒介,以達到各個爬蟲之間的統一排程。
scheduler負責排程各個spider的request請求,scheduler初始化時,通過settings檔案讀取queue和dupefilters的類型(一般就用上邊預設的),配置queue和dupefilters使用的key(一般就是spider name加上queue或者dupefilters,這樣對于同一種spider的不同執行個體,就會使用相同的資料塊了)。每當一個request要被排程時,enqueue_request被調用,scheduler使用dupefilters來判斷這個url是否重複,如果不重複,就添加到queue的容器中(先進先出,先進後出和優先級都可以,可以在settings中配置)。當排程完成時,next_request被調用,scheduler就通過queue容器的接口,取出一個request,把他發送給相應的spider,讓spider進行爬取工作。
同時我們可以看到,如果setting檔案中配置了scheduler_persist為true,那麼在爬蟲關閉的時候scheduler會調用自己的flush函數把redis資料庫中的判重和排程池全部清空,使得我們的爬取進度完全丢失(但是item沒有丢失,item資料在另一個鍵中儲存)。如果設定scheduler_persist為false,爬蟲關閉後,判重池和排程池仍然存在于redis資料庫中,則我們再次開啟爬蟲時,可以接着上一次的進度繼續爬取。
spider的改動也不是很大,主要是通過connect接口,給spider綁定了spider_idle信号,spider初始化時,通過setup_redis函數初始化好和redis的連接配接,之後通過next_requests函數從redis中取出strat url,使用的key是settings中redis_start_urls_as_set定義的(注意了這裡的初始化url池和我們上邊的queue的url池不是一個東西,queue的池是用于排程的,初始化url池是存放入口url的,他們都存在redis中,但是使用不同的key來區分,就當成是不同的表吧),spider使用少量的start
url,可以發展出很多新的url,這些url會進入scheduler進行判重和排程。直到spider跑到排程池内沒有url的時候,會觸發spider_idle信号,進而觸發spider的next_requests函數,再次從redis的start url池中讀取一些url。
最後總結一下scrapy-redis的總體思路:這個工程通過重寫scheduler和spider類,實作了排程、spider啟動和redis的互動。實作新的dupefilter和queue類,達到了判重和排程容器和redis的互動,因為每個主機上的爬蟲程序都通路同一個redis資料庫,是以排程和判重都統一進行統一管理,達到了分布式爬蟲的目的。
當spider被初始化時,同時會初始化一個對應的scheduler對象,這個排程器對象通過讀取settings,配置好自己的排程容器queue和判重工具dupefilter。每當一個spider産出一個request的時候,scrapy核心會把這個reuqest遞交給這個spider對應的scheduler對象進行排程,scheduler對象通過通路redis對request進行判重,如果不重複就把他添加進redis中的排程池。當排程條件滿足時,scheduler對象就從redis的排程池中取出一個request發送給spider,讓他爬取。當spider爬取的所有暫時可用url之後,scheduler發現這個spider對應的redis的排程池空了,于是觸發信号spider_idle,spider收到這個信号之後,直接連接配接redis讀取strart
url池,拿去新的一批url入口,然後再次重複上邊的工作。