那些年,我爬過的北科(三)——爬蟲進階之多程序的使用
在爬蟲基礎之環境搭建與入門中,介紹瞭如何用Requests下載(爬取)了一個頁面,並用BeautifulSoup這個HTML解析庫來解析頁面裡面我們想要的內容。
顯然,爬蟲肯定不是隻讓我們爬取一個網頁的,這樣的工作,人也可以做。下面我們來看:nladuo.cn/scce_site/這個頁面。這個頁面一共有10頁,點選下一頁之後可以看到在網頁的url中多了個欄位“2.html”,也就是當前頁面時第二頁的意思。

也就是我們如果要爬取下所有的新聞,只要爬取形如" nladuo.cn/scce_site/{… "的頁面就好了。
這裡使用一個for迴圈就可以完成全部頁面的爬取。
import requests from bs4 import BeautifulSoup import time def crawl_one_page(page_num): resp = requests.get("http://nladuo.cn/scce_site/{page}.html". format(page=page_num)) soup = BeautifulSoup(resp.content) items = soup.find_all("div", {"class": "every_list"}) for item in items: title_div = item.find("div", {"class": "list_title"}) title = title_div.a.get_text() url = title_div.a["href"] date = item.find("div", {"class": "list_time"}).get_text() print(date, title, url) if __name__ == '__main__': t0 = time.time() for i in range(1, 11): print("crawling page %d ......." % i) crawl_one_page(i) print("used:", (time.time() - t0)) 複製程式碼
CPU密集型和IO密集型業務
通過上面的程式碼,我們完成了一個順序結構的爬蟲。下面我們來討論如何爬取的速度瓶頸在哪裡,從而提升爬取速率。
這裡介紹一下 CPU密集型業務 和 I/O密集型業務 。
- CPU密集型業務(CPU-bound):也叫計算密集型,指的是系統的硬碟、記憶體效能相對CPU要好很多,此時,系統運作大部分的狀況是CPU Loading 100%,CPU要讀/寫I/O(硬碟/記憶體),I/O在很短的時間就可以完成,而CPU還有許多運算要處理,CPU Loading很高。
- I/O密集型業務(I/O-bound):指的是系統的CPU效能相對硬碟、記憶體要好很多,此時,系統運作,大部分的狀況是CPU在等I/O (硬碟/記憶體) 的讀/寫操作,此時CPU Loading並不高。
(上述解釋來自 ofollow,noindex">blog.csdn.net/youanyyou/a… )
網路爬蟲主要有兩個部分,一個是下載頁面,一個是解析頁面。顯然,下載是個長時間的I/O密集操作,而解析頁面則是需要呼叫演算法來查詢頁面結構,是個CPU操作。
對於爬蟲來說,耗時主要在下載一個網頁中,根據網路的連通性,下載一個網頁可能要幾百毫秒甚至幾秒,而解析一個頁面可能只需要幾十毫秒。所以爬蟲其實是屬於I/O密集型業務,其瓶頸主要在網路上面。
所以,提升爬蟲的爬取速度,不是把CPU都跑滿。而是要多開幾個下載器,同時進行下載,把網路I/O跑滿。
在Python中,使用多執行緒和多程序都可以實現併發下載。然而在python多執行緒無法跑多核(參見:GIL),而多程序可以。
這裡,我們主要說一下python中多程序的使用。
多程序
python中呼叫多程序使用multiprocessing這個包就好了。下面建立了兩個程序,每隔一秒列印一下程序ID。(這裡的time.sleep可以理解為耗時的I/O操作。)
import multiprocessing import time import os def process(process_id): while True: time.sleep(1) print('Task %d, pid: %d, doing something' % (process_id, os.getpid())) if __name__ == "__main__": # 程序1 p = multiprocessing.Process(target=process, args=(1,)) p.start() # 程序2 p2 = multiprocessing.Process(target=process, args=(2,)) p2.start() 複製程式碼
可以看到基本上是同時列印兩句話。而在沒用多程序前,我們的程式碼會像下面的程式碼的樣子。
while True: time.sleep(1) print 'Task 1, doing something' time.sleep(1) print 'Task 2, doing something' 複製程式碼
此時,我們建立兩個程序,一個程序爬取1-5頁,一個程序爬取6-10頁。再來試試,看看速度有沒有提升一倍。
import multiprocessing import requests from bs4 import BeautifulSoup import time def crawl_one_page(page_num): resp = requests.get("http://nladuo.cn/scce_site/{page}.html". format(page=page_num)) soup = BeautifulSoup(resp.content) items = soup.find_all("div", {"class": "every_list"}) for item in items: title_div = item.find("div", {"class": "list_title"}) title = title_div.a.get_text() url = title_div.a["href"] date = item.find("div", {"class": "list_time"}).get_text() print(date, title, url) def process(start, end): for i in range(start, end): print("crawling page %d ......." % i) crawl_one_page(i) if __name__ == '__main__': t0 = time.time() p = multiprocessing.Process(target=process, args=(1, 6))# 任務1, 爬取1-5頁 p.start() p2 = multiprocessing.Process(target=process, args=(6, 11))# 任務2, 爬取6-10頁 p2.start() p.join() p2.join() print("used:", (time.time() - t0)) 複製程式碼
程序池
像上面的方式,我們建立了兩個程序,分別處理兩個任務。然而有的時候,並不是那麼容易的把一個任務分成兩個任務。考慮一下把一個任務想象為爬取並解析一個網頁,當我們有兩個或者多個程序而任務有成千上萬個的時候,程式碼應該怎麼寫呢?
這時候,我們需要維護幾個程序,然後給每個程序分配一個網頁,如何分配,需要我們自己定義。在所有的程序都在執行時,要保證有程序結束時,再加入新的程序。
import multiprocessing import requests from bs4 import BeautifulSoup import time def crawl_one_page(page_num): resp = requests.get("http://nladuo.cn/scce_site/{page}.html". format(page=page_num)) soup = BeautifulSoup(resp.content, "html.parser") items = soup.find_all("div", {"class": "every_list"}) for item in items: title_div = item.find("div", {"class": "list_title"}) title = title_div.a.get_text() url = title_div.a["href"] date = item.find("div", {"class": "list_time"}).get_text() print(date, title, url) if __name__ == '__main__': t0 = time.time() p = None# 程序1 p2 = None# 程序2 for i in range(1, 11): if i % 2 == 1:# 把偶數任務分配給程序1 p = multiprocessing.Process(target=crawl_one_page, args=(i,)) p.start() else:# 把奇數任務分配給程序2 p2 = multiprocessing.Process(target=crawl_one_page, args=(i,)) p2.start() if i % 2 == 0:# 保證只有兩個程序, 等待兩個程序完成 p.join() p2.join() print("used:", (time.time() - t0)) 複製程式碼
上面的程式碼實現了一個簡單的兩程序的任務分配和管理,但其實也存在著一些問題:比如程序2先結束,此時就只有一個程序在執行,但程式還阻塞住,無法產生新的程序。這裡只是簡單的做個例子,旨在說明程序管理的複雜性。
下面我們說一說程序池,其實就是為了解決這個問題而設計的。
既然叫做程序池,那就是有個池子,裡面有一堆公用的程序;當有任務來了,拿一個程序出來;當任務完成了,把程序還回池子裡,給別的任務用;當池子裡面沒有可用程序的時候,那就要等待,等別人把程序歸還了再拿去用。
下面我們來看一下程式碼,讓每個程序每秒列印一下pid,一共列印兩遍。
from multiprocessing import Pool import time import os def do_something(num): for i in range(2): time.sleep(1) print("doing %d, pid: %d" % (num, os.getpid())) if __name__ == '__main__': p = Pool(3) for page in range(1, 11):# 10個任務 p.apply_async(do_something, args=(page,)) p.close()# 關閉程序池, 不再接受任務 p.join()# 等待子程序結束 複製程式碼
執行程式碼後可以看到,我們可以看到這裡是三個三個的列印的,我們成功完成了三併發。同時,程序池一共產生了三個程序:59650、59651、59652,說明後面的所有任務都是使用這三個程序完成的。

下面,修改爬蟲程式碼,用程序池實現併發爬取。
import requests from bs4 import BeautifulSoup from multiprocessing import Pool import time def crawl_one_page(page_num): resp = requests.get("http://nladuo.cn/scce_site/{page}.html". format(page=page_num)) soup = BeautifulSoup(resp.content, "html.parser") items = soup.find_all("div", {"class": "every_list"}) for item in items: title_div = item.find("div", {"class": "list_title"}) title = title_div.a.get_text() url = title_div.a["href"] date = item.find("div", {"class": "list_time"}).get_text() print(date, title, url) if __name__ == '__main__': t0 = time.time() p = Pool(5) for page in range(1, 11):# 1-10頁 p.apply_async(crawl_one_page, args=(page,)) # 關閉程序池, 等待子程序結束 p.close() p.join() print("used:", (time.time() - t0)) 複製程式碼
到這裡,多程序的講解就結束了。