python爬蟲實操專案_Python爬蟲開發與專案實戰 1.6 小結

憐鑫發表於2021-02-04

1.4 程式和執行緒

在爬蟲開發中,程式和執行緒的概念是非常重要的。提高爬蟲的工作效率,打造分散式爬蟲,都離不開程式和執行緒的身影。本節將從多程式、多執行緒、協程和分散式程式等四個方面,幫助大家回顧Python語言中程式和執行緒中的常用操作,以便在接下來的爬蟲開發中靈活運用程式和執行緒。

1.4.1 多程式

Python實現多程式的方式主要有兩種,一種方法是使用os模組中的fork方法,另一種方法是使用multiprocessing模組。這兩種方法的區別在於前者僅適用於Unix/Linux作業系統,對Windows不支援,後者則是跨平臺的實現方式。由於現在很多爬蟲程式都是執行在Unix/Linux作業系統上,所以本節對兩種方式都進行講解。

1.使用os模組中的fork方式實現多程式

Python的os模組封裝了常見的系統呼叫,其中就有fork方法。fork方法來自於Unix/Linux作業系統中提供的一個fork系統呼叫,這個方法非常特殊。普通的方法都是呼叫一次,返回一次,而fork方法是呼叫一次,返回兩次,原因在於作業系統將當前程式(父程式)複製出一份程式(子程式),這兩個程式幾乎完全相同,於是fork方法分別在父程式和子程式中返回。子程式中永遠返回0,父程式中返回的是子程式的ID。下面舉個例子,對Python使用fork方法建立程式進行講解。其中os模組中的getpid方法用於獲取當前程式的ID,getppid方法用於獲取父程式的ID。程式碼如下:

import os

if __name__ == '__main__':

print 'current Process (%s) start ...'%(os.getpid())

pid = os.fork()

if pid < 0:

print 'error in fork'

elif pid == 0:

print 'I am child process(%s) and my parent process is (%s)',(os.getpid(),

os.getppid())

else:

print 'I(%s) created a chlid process (%s).',(os.getpid(),pid)

執行結果如下:

current Process (3052) start ...

I(3052) created a chlid process (3053).

I am child process(3053) and my parent process is (3052)

2.使用multiprocessing模組建立多程式

multiprocessing模組提供了一個Process類來描述一個程式物件。建立子程式時,只需要傳入一個執行函式和函式的引數,即可完成一個Process例項的建立,用start()方法啟動程式,用join()方法實現程式間的同步。下面通過一個例子來演示建立多程式的流程,程式碼如下:

import os

from multiprocessing import Process

# 子程式要執行的程式碼

def run_proc(name):

print 'Child process %s (%s) Running...' % (name, os.getpid())

if __name__ == '__main__':

print 'Parent process %s.' % os.getpid()

for i in range(5):

p = Process(target=run_proc, args=(str(i),))

print 'Process will start.'

p.start()

p.join()

print 'Process end.'

執行結果如下:

Parent process 2392.

Process will start.

Process will start.

Process will start.

Process will start.

Process will start.

Child process 2 (10748) Running...

Child process 0 (5324) Running...

Child process 1 (3196) Running...

Child process 3 (4680) Running...

Child process 4 (10696) Running...

Process end.

以上介紹了建立程式的兩種方法,但是要啟動大量的子程式,使用程式池批量建立子程式的方式更加常見,因為當被操作物件數目不大時,可以直接利用multiprocessing中的Process動態生成多個程式,如果是上百個、上千個目標,手動去限制程式數量卻又太過繁瑣,這時候程式池Pool發揮作用的時候就到了。

3.multiprocessing模組提供了一個Pool類來代表程式池物件

Pool可以提供指定數量的程式供使用者呼叫,預設大小是CPU的核數。當有新的請求提交到Pool中時,如果池還沒有滿,那麼就會建立一個新的程式用來執行該請求;但如果池中的程式數已經達到規定最大值,那麼該請求就會等待,直到池中有程式結束,才會建立新的程式來處理它。下面通過一個例子來演示程式池的工作流程,程式碼如下:

from multiprocessing import Pool

import os, time, random

def run_task(name):

print 'Task %s (pid = %s) is running...' % (name, os.getpid())

time.sleep(random.random() * 3)

print 'Task %s end.' % name

if __name__=='__main__':

print 'Current process %s.' % os.getpid()

p = Pool(processes=3)

for i in range(5):

p.apply_async(run_task, args=(i,))

print 'Waiting for all subprocesses done...'

p.close()

p.join()

print 'All subprocesses done.'

執行結果如下:

Current process 9176.

Waiting for all subprocesses done...

Task 0 (pid = 11012) is running...

Task 1 (pid = 12464) is running...

Task 2 (pid = 11260) is running...

Task 2 end.

Task 3 (pid = 11260) is running...

Task 0 end.

Task 4 (pid = 11012) is running...

Task 1 end.

Task 3 end.

Task 4 end.

All subprocesses done.

上述程式先建立了容量為3的程式池,依次向程式池中新增了5個任務。從執行結果中可以看到雖然新增了5個任務,但是一開始只執行了3個,而且每次最多執行3個程式。當一個任務結束了,新的任務依次新增進來,任務執行使用的程式依然是原來的程式,這一點通過程式的pid就可以看出來。

9ea96dd8e345568b3af8dc0fe743629e.png

注意

Pool物件呼叫join()方法會等待所有子程式執行完畢,呼叫join()之前必須先呼叫close(),呼叫close()之後就不能繼續新增新的Process了。

4.程式間通訊

假如建立了大量的程式,那程式間通訊是必不可少的。Python提供了多種程式間通訊的方式,例如Queue、Pipe、Value+Array等。本節主要講解Queue和Pipe這兩種方式。Queue和Pipe的區別在於Pipe常用來在兩個程式間通訊,Queue用來在多個程式間實現通訊。

首先講解一下Queue通訊方式。Queue是多程式安全的佇列,可以使用Queue實現多程式之間的資料傳遞。有兩個方法:Put和Get可以進行Queue操作:

·Put方法用以插入資料到佇列中,它還有兩個可選引數:blocked和timeout。如果blocked為True(預設值),並且timeout為正值,該方法會阻塞timeout指定的時間,直到該佇列有剩餘的空間。如果超時,會丟擲Queue.Full異常。如果blocked為False,但該Queue已滿,會立即丟擲Queue.Full異常。

·Get方法可以從佇列讀取並且刪除一個元素。同樣,Get方法有兩個可選引數:blocked和timeout。如果blocked為True(預設值),並且timeout為正值,那麼在等待時間內沒有取到任何元素,會丟擲Queue.Empty異常。如果blocked為False,分兩種情況:如果Queue有一個值可用,則立即返回該值;否則,如果佇列為空,則立即丟擲Queue.Empty異常。

下面通過一個例子進行說明:在父程式中建立三個子程式,兩個子程式往Queue中寫入資料,一個子程式從Queue中讀取資料。程式示例如下:

from multiprocessing import Process, Queue

import os, time, random

# 寫資料程式執行的程式碼:

def proc_write(q,urls):

print('Process(%s) is writing...' % os.getpid())

for url in urls:

q.put(url)

print('Put %s to queue...' % url)

time.sleep(random.random())

# 讀資料程式執行的程式碼:

def proc_read(q):

print('Process(%s) is reading...' % os.getpid())

while True:

url = q.get(True)

print('Get %s from queue.' % url)

if __name__=='__main__':

# 父程式建立Queue,並傳給各個子程式:

q = Queue()

proc_writer1 = Process(target=proc_write, args=(q,['url_1', 'url_2', 'url_3']))

proc_writer2 = Process(target=proc_write, args=(q,['url_4','url_5','url_6']))

proc_reader = Process(target=proc_read, args=(q,))

# 啟動子程式proc_writer,寫入:

proc_writer1.start()

proc_writer2.start()

# 啟動子程式proc_reader,讀取:

proc_reader.start()

# 等待proc_writer結束:

proc_writer1.join()

proc_writer2.join()

# proc_reader程式裡是死迴圈,無法等待其結束,只能強行終止:

proc_reader.terminate()

執行結果如下:

Process(9968) is writing...

Process(9512) is writing...

Put url_1 to queue...

Put url_4 to queue...

Process(1124) is reading...

Get url_1 from queue.

Get url_4 from queue.

Put url_5 to queue...

Get url_5 from queue.

Put url_2 to queue...

Get url_2 from queue.

Put url_6 to queue...

Get url_6 from queue.

Put url_3 to queue...

Get url_3 from queue.

最後介紹一下Pipe的通訊機制,Pipe常用來在兩個程式間進行通訊,兩個程式分別位於管道的兩端。

Pipe方法返回(conn1,conn2)代表一個管道的兩個端。Pipe方法有duplex引數,如果duplex引數為True(預設值),那麼這個管道是全雙工模式,也就是說conn1和conn2均可收發。若duplex為False,conn1只負責接收訊息,conn2只負責傳送訊息。send和recv方法分別是傳送和接收訊息的方法。例如,在全雙工模式下,可以呼叫conn1.send傳送訊息,conn1.recv接收訊息。如果沒有訊息可接收,recv方法會一直阻塞。如果管道已經被關閉,那麼recv方法會丟擲EOFError。

下面通過一個例子進行說明:建立兩個程式,一個子程式通過Pipe傳送資料,一個子程式通過Pipe接收資料。程式示例如下:

import multiprocessing

import random

import time,os

def proc_send(pipe,urls):

for url in urls:

print "Process(%s) send: %s" %(os.getpid(),url)

pipe.send(url)

time.sleep(random.random())

def proc_recv(pipe):

while True:

print "Process(%s) rev:%s" %(os.getpid(),pipe.recv())

time.sleep(random.random())

if __name__ == "__main__":

pipe = multiprocessing.Pipe()

p1 = multiprocessing.Process(target=proc_send, args=(pipe[0],['url_'+str(i)

for i in range(10) ]))

p2 = multiprocessing.Process(target=proc_recv, args=(pipe[1],))

p1.start()

p2.start()

p1.join()

p2.join()

執行結果如下:

Process(10448) send: url_0

Process(5832) rev:url_0

Process(10448) send: url_1

Process(5832) rev:url_1

Process(10448) send: url_2

Process(5832) rev:url_2

Process(10448) send: url_3

Process(10448) send: url_4

Process(5832) rev:url_3

Process(10448) send: url_5

Process(10448) send: url_6

Process(5832) rev:url_4

Process(5832) rev:url_5

Process(10448) send: url_7

Process(10448) send: url_8

Process(5832) rev:url_6

Process(5832) rev:url_7

Process(10448) send: url_9

Process(5832) rev:url_8

Process(5832) rev:url_9

9ea96dd8e345568b3af8dc0fe743629e.png

注意

以上多程式程式執行結果的列印順序在不同的系統和硬體條件下略有不同。

1.4.2 多執行緒

多執行緒類似於同時執行多個不同程式,多執行緒執行有如下優點:

·可以把執行時間長的任務放到後臺去處理。

·使用者介面可以更加吸引人,比如使用者點選了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度。

·程式的執行速度可能加快。

·在一些需要等待的任務實現上,如使用者輸入、檔案讀寫和網路收發資料等,執行緒就比較有用了。在這種情況下我們可以釋放一些珍貴的資源,如記憶體佔用等。

Python的標準庫提供了兩個模組:thread和threading,thread是低階模組,threading是高階模組,對thread進行了封裝。絕大多數情況下,我們只需要使用threading這個高階模組。

1.用threading模組建立多執行緒

threading模組一般通過兩種方式建立多執行緒:第一種方式是把一個函式傳入並建立Thread例項,然後呼叫start方法開始執行;第二種方式是直接從threading.Thread繼承並建立執行緒類,然後重寫__init__方法和run方法。

首先介紹第一種方法,通過一個簡單例子演示建立多執行緒的流程,程式如下:

import random

import time, threading

# 新執行緒執行的程式碼:

def thread_run(urls):

print 'Current %s is running...' % threading.current_thread().name

for url in urls:

print '%s ---->>> %s' % (threading.current_thread().name,url)

time.sleep(random.random())

print '%s ended.' % threading.current_thread().name

print '%s is running...' % threading.current_thread().name

t1 = threading.Thread(target=thread_run, name='Thread_1',args=(['url_1','url_2','

url_3'],))

t2 = threading.Thread(target=thread_run, name='Thread_2',args=(['url_4','url_5','

url_6'],))

t1.start()

t2.start()

t1.join()

t2.join()

print '%s ended.' % threading.current_thread().name

執行結果如下:

MainThread is running...

Current Thread_1 is running...

Thread_1 ---->>> url_1

Current Thread_2 is running...

Thread_2 ---->>> url_4

Thread_1 ---->>> url_2

Thread_2 ---->>> url_5

Thread_2 ---->>> url_6

Thread_1 ---->>> url_3

Thread_1 ended.

Thread_2 ended.

MainThread ended.

第二種方式從threading.Thread繼承建立執行緒類,下面將方法一的程式進行重寫,程式如下:

import random

import threading

import time

class myThread(threading.Thread):

def __init__(self,name,urls):

threading.Thread.__init__(self,name=name)

self.urls = urls

def run(self):

print 'Current %s is running...' % threading.current_thread().name

for url in self.urls:

print '%s ---->>> %s' % (threading.current_thread().name,url)

time.sleep(random.random())

print '%s ended.' % threading.current_thread().name

print '%s is running...' % threading.current_thread().name

t1 = myThread(name='Thread_1',urls=['url_1','url_2','url_3'])

t2 = myThread(name='Thread_2',urls=['url_4','url_5','url_6'])

t1.start()

t2.start()

t1.join()

t2.join()

print '%s ended.' % threading.current_thread().name

執行結果如下:

MainThread is running...

Current Thread_1 is running...

Thread_1 ---->>> url_1

Current Thread_2 is running...

Thread_2 ---->>> url_4

Thread_2 ---->>> url_5

Thread_1 ---->>> url_2

Thread_1 ---->>> url_3

Thread_2 ---->>> url_6

Thread_2 ended.

Thread_1 ended.

2.執行緒同步

如果多個執行緒共同對某個資料修改,則可能出現不可預料的結果,為了保證資料的正確性,需要對多個執行緒進行同步。使用Thread物件的Lock和RLock可以實現簡單的執行緒同步,這兩個物件都有acquire方法和release方法,對於那些每次只允許一個執行緒操作的資料,可以將其操作放到acquire和release方法之間。

對於Lock物件而言,如果一個執行緒連續兩次進行acquire操作,那麼由於第一次acquire之後沒有release,第二次acquire將掛起執行緒。這會導致Lock物件永遠不會release,使得執行緒死鎖。RLock物件允許一個執行緒多次對其進行acquire操作,因為在其內部通過一個counter變數維護著執行緒acquire的次數。而且每一次的acquire操作必須有一個release操作與之對應,在所有的release操作完成之後,別的執行緒才能申請該RLock物件。下面通過一個簡單的例子演示執行緒同步的過程:

import threading

mylock = threading.RLock()

num=0

class myThread(threading.Thread):

def __init__(self, name):

threading.Thread.__init__(self,name=name)

def run(self):

global num

while True:

mylock.acquire()

print '%s locked, Number: %d'%(threading.current_thread().name, num)

if num>=4:

mylock.release()

print '%s released, Number: %d'%(threading.current_thread().name, num)

break

num+=1

print '%s released, Number: %d'%(threading.current_thread().name, num)

mylock.release()

if __name__== '__main__':

thread1 = myThread('Thread_1')

thread2 = myThread('Thread_2')

thread1.start()

thread2.start()

執行結果如下:

Thread_1 locked, Number: 0

Thread_1 released, Number: 1

Thread_1 locked, Number: 1

Thread_1 released, Number: 2

Thread_2 locked, Number: 2

Thread_2 released, Number: 3

Thread_1 locked, Number: 3

Thread_1 released, Number: 4

Thread_2 locked, Number: 4

Thread_2 released, Number: 4

Thread_1 locked, Number: 4

Thread_1 released, Number: 4

3.全域性直譯器鎖(GIL)

在Python的原始直譯器CPython中存在著GIL(Global Interpreter Lock,全域性直譯器鎖),因此在解釋執行Python程式碼時,會產生互斥鎖來限制執行緒對共享資源的訪問,直到直譯器遇到I/O操作或者操作次數達到一定數目時才會釋放GIL。由於全域性直譯器鎖的存在,在進行多執行緒操作的時候,不能呼叫多個CPU核心,只能利用一個核心,所以在進行CPU密集型操作的時候,不推薦使用多執行緒,更加傾向於多程式。那麼多執行緒適合什麼樣的應用場景呢?對於IO密集型操作,多執行緒可以明顯提高效率,例如Python爬蟲的開發,絕大多數時間爬蟲是在等待socket返回資料,網路IO的操作延時比CPU大得多。

1.4.3 協程

協程

(coroutine),又稱微執行緒,纖程,是一種使用者級的輕量級執行緒。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧。因此協程能保留上一次呼叫時的狀態,每次過程重入時,就相當於進入上一次呼叫的狀態。在併發程式設計中,協程與執行緒類似,每個協程表示一個執行單元,有自己的本地資料,與其他協程共享全域性資料和其他資源。

協程需要使用者自己來編寫排程邏輯,對於CPU來說,協程其實是單執行緒,所以CPU不用去考慮怎麼排程、切換上下文,這就省去了CPU的切換開銷,所以協程在一定程度上又好於多執行緒。那麼在Python中是如何實現協程的呢?

Python通過yield提供了對協程的基本支援,但是不完全,而使用第三方gevent庫是更好的選擇,gevent提供了比較完善的協程支援。gevent是一個基於協程的Python網路函式庫,使用greenlet在libev事件迴圈頂部提供了一個有高階別併發性的API。主要特性有以下幾點:

·基於libev的快速事件迴圈,Linux上是epoll機制。

·基於greenlet的輕量級執行單元。

·API複用了Python標準庫裡的內容。

·支援SSL的協作式sockets。

·可通過執行緒池或c-ares實現DNS查詢。

·通過monkey patching功能使得第三方模組變成協作式。

gevent對協程的支援,本質上是greenlet在實現切換工作。greenlet工作流程如下:假如進行訪問網路的IO操作時,出現阻塞,greenlet就顯式切換到另一段沒有被阻塞的程式碼段執行,直到原先的阻塞狀況消失以後,再自動切換回原來的程式碼段繼續處理。因此,greenlet是一種合理安排的序列方式。

由於IO操作非常耗時,經常使程式處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在執行,而不是等待IO,這就是協程一般比多執行緒效率高的原因。由於切換是在IO操作時自動完成,所以gevent需要修改Python自帶的一些標準庫,將一些常見的阻塞,如socket、select等地方實現協程跳轉,這一過程在啟動時通過monkey patch完成。下面通過一個的例子來演示gevent的使用流程,程式碼如下:

from gevent import monkey; monkey.patch_all()

import gevent

import urllib2

def run_task(url):

print 'Visit --> %s' % url

try:

response = urllib2.urlopen(url)

data = response.read()

print '%d bytes received from %s.' % (len(data), url)

except Exception,e:

print e

if __name__=='__main__':

urls = ['https:// github.com/','https:// www.python.org/','http://www.cnblogs.com/']

greenlets = [gevent.spawn(run_task, url) for url in urls ]

gevent.joinall(greenlets)

執行結果如下:

Visit --> https:// github.com/

Visit --> https:// www.python.org/

Visit --> http://www.cnblogs.com/

45740 bytes received from http://www.cnblogs.com/.

25482 bytes received from https:// github.com/.

47445 bytes received from https:// www.python.org/.

以上程式主要用了gevent中的spawn方法和joinall方法。spawn方法可以看做是用來形成協程,joinall方法就是新增這些協程任務,並且啟動執行。從執行結果來看,3個網路操作是併發執行的,而且結束順序不同,但其實只有一個執行緒。

gevent中還提供了對池的支援。當擁有動態數量的greenlet需要進行併發管理(限制併發數)時,就可以使用池,這在處理大量的網路和IO操作時是非常需要的。接下來使用gevent中pool物件,對上面的例子進行改寫,程式如下:

from gevent import monkey

monkey.patch_all()

import urllib2

from gevent.pool import Pool

def run_task(url):

print 'Visit --> %s' % url

try:

response = urllib2.urlopen(url)

data = response.read()

print '%d bytes received from %s.' % (len(data), url)

except Exception,e:

print e

return 'url:%s --->finish'% url

if __name__=='__main__':

pool = Pool(2)

urls = ['https:// github.com/','https:// www.python.org/','http://www.cnblogs.com/']

results = pool.map(run_task,urls)

print results

執行結果如下:

Visit --> https:// github.com/

Visit --> https:// www.python.org/

25482 bytes received from https:// github.com/.

Visit --> http://www.cnblogs.com/

47445 bytes received from https:// www.python.org/.

45687 bytes received from http://www.cnblogs.com/.

['url:https:// github.com/ --->finish', 'url:https:// www.python.org/ --->finish', 'url:http://www.cnblogs.com/ --->finish']

通過執行結果可以看出,Pool物件確實對協程的併發數量進行了管理,先訪問了前兩個網址,當其中一個任務完成時,才會執行第三個。

1.4.4 分散式程式

分散式程式指的是將Process程式分佈到多臺機器上,充分利用多臺機器的效能完成複雜的任務。我們可以將這一點應用到分散式爬蟲的開發中。

分散式程式在Python中依然要用到multiprocessing模組。multiprocessing模組不但支援多程式,其中managers子模組還支援把多程式分佈到多臺機器上。可以寫一個服務程式作為排程者,將任務分佈到其他多個程式中,依靠網路通訊進行管理。舉個例子:在做爬蟲程式時,常常會遇到這樣的場景,我們想抓取某個網站的所有圖片,如果使用多程式的話,一般是一個程式負責抓取圖片的連結地址,將連結地址存放到Queue中,另外的程式負責從Queue中讀取連結地址進行下載和儲存到本地。現在把這個過程做成分散式,一臺機器上的程式負責抓取連結,其他機器上的程式負責下載儲存。那麼遇到的主要問題是將Queue暴露到網路中,讓其他機器程式都可以訪問,分散式程式就是將這一個過程進行了封裝,我們可以將這個過程稱為本地佇列的網路化。整體過程如圖1-24所示。

2cda45a4aad3fb6d328ea24c33de301f.png

圖1-24 分散式程式

要實現上面例子的功能,建立分散式程式需要分為六個步驟:

1)建立佇列Queue,用來進行程式間的通訊。服務程式建立任務佇列task_queue,用來作為傳遞任務給任務程式的通道;服務程式建立結果佇列result_queue,作為任務程式完成任務後回覆服務程式的通道。在分散式多程式環境下,必須通過由Queuemanager獲得的Queue介面來新增任務。

2)把第一步中建立的佇列在網路上註冊,暴露給其他程式(主機),註冊後獲得網路佇列,相當於本地佇列的映像。

3)建立一個物件(Queuemanager(BaseManager))例項manager,繫結埠和驗證口令。

4)啟動第三步中建立的例項,即啟動管理manager,監管資訊通道。

5)通過管理例項的方法獲得通過網路訪問的Queue物件,即再把網路佇列實體化成可以使用的本地佇列。

6)建立任務到“本地”佇列中,自動上傳任務到網路佇列中,分配給任務程式進行處理。

接下來通過程式實現上面的例子(Linux版),首先編寫的是服務程式(taskManager.py),程式碼如下:

import random,time,Queue

from multiprocessing.managers import BaseManager

# 第一步:建立task_queue和result_queue,用來存放任務和結果

task_queue=Queue.Queue()

result_queue=Queue.Queue()

class Queuemanager(BaseManager):

pass

# 第二步:把建立的兩個佇列註冊在網路上,利用register方法,callable引數關聯了Queue物件,

# 將Queue物件在網路中暴露

Queuemanager.register('get_task_queue',callable=lambda:task_queue)

Queuemanager.register('get_result_queue',callable=lambda:result_queue)

# 第三步:繫結埠8001,設定驗證口令‘qiye’。這個相當於物件的初始化

manager=Queuemanager(address=('',8001),authkey='qiye')

# 第四步:啟動管理,監聽資訊通道

manager.start()

# 第五步:通過管理例項的方法獲得通過網路訪問的Queue物件

task=manager.get_task_queue()

result=manager.get_result_queue()

# 第六步:新增任務

for url in ["ImageUrl_"+i for i in range(10)]:

print 'put task %s ...' %url

task.put(url)

# 獲取返回結果

print 'try get result...'

for i in range(10):

print 'result is %s' %result.get(timeout=10)

# 關閉管理

manager.shutdown()

任務程式已經編寫完成,接下來編寫任務程式(taskWorker.py),建立任務程式的步驟相對較少,需要四個步驟:

1)使用QueueManager註冊用於獲取Queue的方法名稱,任務程式只能通過名稱來在網路上獲取Queue。

2)連線伺服器,埠和驗證口令注意保持與服務程式中完全一致。

3)從網路上獲取Queue,進行本地化。

4)從task佇列獲取任務,並把結果寫入result佇列。

程式taskWorker.py程式碼(win/linux版)如下:

# coding:utf-8

import time

from multiprocessing.managers import BaseManager

# 建立類似的QueueManager:

class QueueManager(BaseManager):

pass

# 第一步:使用QueueManager註冊用於獲取Queue的方法名稱

QueueManager.register('get_task_queue')

QueueManager.register('get_result_queue')

# 第二步:連線到伺服器:

server_addr = '127.0.0.1'

print('Connect to server %s...' % server_addr)

# 埠和驗證口令注意保持與服務程式完全一致:

m = QueueManager(address=(server_addr, 8001), authkey='qiye')

# 從網路連線:

m.connect()

# 第三步:獲取Queue的物件:

task = m.get_task_queue()

result = m.get_result_queue()

# 第四步:從task佇列獲取任務,並把結果寫入result佇列:

while(not task.empty()):

image_url = task.get(True,timeout=5)

print('run task download %s...' % image_url)

time.sleep(1)

result.put('%s--->success'%image_url)

# 處理結束:

print('worker exit.')

最後開始執行程式,先啟動服務程式taskManager.py,執行結果如下:

put task ImageUrl_0 ...

put task ImageUrl_1 ...

put task ImageUrl_2 ...

put task ImageUrl_3 ...

put task ImageUrl_4 ...

put task ImageUrl_5 ...

put task ImageUrl_6 ...

put task ImageUrl_7 ...

put task ImageUrl_8 ...

put task ImageUrl_9 ...

try get result...

接著再啟動任務程式taskWorker.py,執行結果如下:

Connect to server 127.0.0.1...

run task download ImageUrl_0...

run task download ImageUrl_1...

run task download ImageUrl_2...

run task download ImageUrl_3...

run task download ImageUrl_4...

run task download ImageUrl_5...

run task download ImageUrl_6...

run task download ImageUrl_7...

run task download ImageUrl_8...

run task download ImageUrl_9...

worker exit.

當任務程式執行結束後,服務程式執行結果如下:

result is ImageUrl_0--->success

result is ImageUrl_1--->success

result is ImageUrl_2--->success

result is ImageUrl_3--->success

result is ImageUrl_4--->success

result is ImageUrl_5--->success

result is ImageUrl_6--->success

result is ImageUrl_7--->success

result is ImageUrl_8--->success

result is ImageUrl_9--->success

其實這就是一個簡單但真正的分散式計算,把程式碼稍加改造,啟動多個worker,就可以把任務分佈到幾臺甚至幾十臺機器上,實現大規模的分散式爬蟲。

9ea96dd8e345568b3af8dc0fe743629e.png

注意

由於平臺的特性,建立服務程式的程式碼在Linux和Windows上有一些不同,建立工作程式的程式碼是一致的。

taskManager.py程式在Windows版下的程式碼如下:

# coding:utf-8

# taskManager.py for windows

import Queue

from multiprocessing.managers import BaseManager

from multiprocessing import freeze_support

# 任務個數

task_number = 10

# 定義收發佇列

task_queue = Queue.Queue(task_number);

result_queue = Queue.Queue(task_number);

def get_task():

return task_queue

def get_result():

return result_queue

# 建立類似的QueueManager:

class QueueManager(BaseManager):

pass

def win_run():

# Windows下繫結呼叫介面不能使用lambda,所以只能先定義函式再繫結

QueueManager.register('get_task_queue',callable = get_task)

QueueManager.register('get_result_queue',callable = get_result)

# 繫結埠並設定驗證口令,Windows下需要填寫IP地址,Linux下不填預設為本地

manager = QueueManager(address = ('127.0.0.1',8001),authkey = 'qiye')

# 啟動

manager.start()

try:

# 通過網路獲取任務佇列和結果佇列

task = manager.get_task_queue()

result = manager.get_result_queue()

# 新增任務

for url in ["ImageUrl_"+str(i) for i in range(10)]:

print 'put task %s ...' %url

task.put(url)

print 'try get result...'

for i in range(10):

print 'result is %s' %result.get(timeout=10)

except:

print('Manager error')

finally:

# 一定要關閉,否則會報管道未關閉的錯誤

manager.shutdown()

if __name__ == '__main__':

# Windows下多程式可能會有問題,新增這句可以緩解

freeze_support()

win_run()

相關文章