在爬蟲基礎之環境搭建與入門中,介紹瞭如何用Requests下載(爬取)了一個頁面,並用BeautifulSoup這個HTML解析庫來解析頁面裏面咱們想要的內容。html
顯然,爬蟲確定不是隻讓咱們爬取一個網頁的,這樣的工做,人也能夠作。下面咱們來看:nladuo.cn/scce_site/這個頁面。這個頁面一共有10頁,點擊下一頁以後能夠看到在網頁的url中多了個字段「2.html」,也就是當前頁面時第二頁的意思。python
也就是咱們若是要爬取下全部的新聞,只要爬取形如"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密集型業務和I/O密集型業務。併發
(上述解釋來自blog.csdn.net/youanyyou/a…)app
網絡爬蟲主要有兩個部分,一個是下載頁面,一個是解析頁面。顯然,下載是個長時間的I/O密集操做,而解析頁面則是須要調用算法來查找頁面結構,是個CPU操做。async
對於爬蟲來講,耗時主要在下載一個網頁中,根據網絡的連通性,下載一個網頁可能要幾百毫秒甚至幾秒,而解析一個頁面可能只須要幾十毫秒。因此爬蟲實際上是屬於I/O密集型業務,其瓶頸主要在網絡上面。性能
因此,提高爬蟲的爬取速度,不是把CPU都跑滿。而是要多開幾個下載器,同時進行下載,把網絡I/O跑滿。url
在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、5965一、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))
複製代碼
到這裏,多進程的講解就結束了。