用 python 挺久了,但并沒有深入了解過多線程多進(jìn)程之類的知識钦铺,最近看了許多關(guān)于多線程多進(jìn)程的知識订雾,記錄簡單的實現(xiàn)過程。
方案
- 爬取某網(wǎng)站 20 頁圖片矛洞,每頁大概 20~30 張圖片
- 該網(wǎng)站沒有反爬措施
- 爬蟲全速爬取洼哎,不設(shè)置休眠時間
- 依次爬取每頁的圖片鏈接,保存至一個列表中(對于單線程沼本、多進(jìn)程方案)噩峦,保存至隊列(對于多線程方案),這一步使用單線程抽兆;然后用
urllib
下載圖片识补,這一步使用單線程、多線程辫红、多進(jìn)程分別爬取 - 依次使用單線程凭涂、多線程祝辣、多進(jìn)程爬取相同的資源
共同代碼
下面的代碼是單線程、多線程切油、多進(jìn)程都要用到的基本代碼蝙斜。
import ... #省略...
folder_path3 = 'C://Users/Stone/Desktop/jiandan3/'
header = {
... # 省略...
}
image_links = []
def get_links(page): # 獲取圖片鏈接
for i in xrange(page):
url = "https://some.net/pic/page-{}".format(str(i))
response = requests.get(url, headers=header)
soup = BeautifulSoup(response.text, "lxml")
links = soup.select("div.row img")
for pic_tag in links: # 獲取圖片鏈接
pic_link = pic_tag.get('src')
if pic_link.startswith("http"):
image_links.append(pic_link)
# myqueue.put(pic_link)
else:
pic_link = 'http:' + pic_link
image_links.append(pic_link)
#myqueue.put(pic_link)
def download_pic(url): # 下載圖片
img_name = "{}".format(os.path.basename(url))
urllib.urlretrieve(url, folder_path3 + img_name)
print "{} download seccessfully!".format(img_name)
單線程
單線程代碼比較簡單,獲取完 20 頁的圖片鏈接后澎胡,用 download_pic
方法一個一個下載圖片 :
if __name__ == '__main__':
start_time = time.time()
get_links(20) # 爬取20頁
for link in image_links:
download_pic(link) # 下載圖片
end_time = time.time()
print "all done!"
print end_time - start_time
此段代碼運行完后孕荠,統(tǒng)計數(shù)據(jù)如下:
爬取圖片數(shù)量 | 耗時 |
---|---|
571張 | 102.9秒 |
多進(jìn)程
多進(jìn)程使用的是 multiprocessing
包下的 Pool
類,它會根據(jù)電腦所擁有的核心自動創(chuàng)建 Pool
類的實例滤馍,也可以手動傳入?yún)?shù)岛琼;在 Pool
這個類中有 map
和 map_async
兩種方法,map 方法是阻塞的巢株,也就是 map 方法之后的代碼必須等待 map 方法執(zhí)行完成才能繼續(xù)進(jìn)行槐瑞,下面測試 map 方法:
if __name__ == '__main__':
start_time = time.time()
get_links(20)
pool = Pool()
pool.map(download_pic, image_links) # 同步/阻塞
end_time = time.time()
print "all done!"
print end_time - start_time # 多進(jìn)程
map
方法測試結(jié)果:
爬取圖片數(shù)量 | 耗時 |
---|---|
571張 | 37.88秒 |
使用 map_async
方法爬取:
if __name__ == '__main__':
start_time = time.time()
get_links(20)
pool = Pool()
pool.map_async(download_pic, image_links) # 異步
pool.close() # 關(guān)閉進(jìn)程連接
pool.join() # 等待 map_async 函數(shù)執(zhí)行完成阁苞,在這阻塞
end_time = time.time()
print "all done!"
print end_time - start_time
map_async
方法是異步的困檩,這一整段代碼運行到 map_async
方法時,不會等待這個方法完成那槽,而是繼續(xù)后面的代碼邏輯面哥,而 map_async
方法也在背后繼續(xù)進(jìn)行著香府;其中有個問題就是,當(dāng)后面的代碼運行完之后,就要停止特石,那么map_async
方法沒有運行完也會被停止弓叛,所以上面的代碼比 map
方法多了兩行镶苞,join
方法的功能就是等待map_async
函數(shù)執(zhí)行完成肝断,測試結(jié)果:
爬取圖片數(shù)量 | 耗時 |
---|---|
571張 | 38.54秒 |
多線程
使用多線程需要 threading
包中 Thread
類,以及配合 Queue
類丈钙,多線程中非驮,使用 Queue
代替 List
,Queue
有多種雏赦,FIFO
(先進(jìn)先出)劫笙、LIFO
(后進(jìn)先出)、優(yōu)先級隊列星岗,在此方案中用 FIFO
隊列填大,在原代碼做出相應(yīng)改變:
myqueue = Queue()
....
if pic_link.startswith("http"):
# image_links.append(pic_link)
myqueue.put(pic_link)
else:
pic_link = 'http:' + pic_link
# image_links.append(pic_link)
myqueue.put(pic_link)
此外,再封裝一個方法:
def woker():
while not myqueue.empty():
img_url = myqueue.get()
download_pic(img_url)
myqueue.task_done()
if __name__ == '__main__':
start_time = time.time()
get_links(20)
for x in range(4): # 根據(jù)電腦性能設(shè)置核心
thread = Thread(target=woker) # 創(chuàng)建線程
thread.start()
myqueue.join()
print "all done!"
print time.time() - start_time # 多線程伍茄!
此段代碼的作用:判斷隊列是否為空栋盹,不為空則將里面的 url
取出給 download_pic
下載,下載完成后敷矫,調(diào)用 Queue
類的 task_done
例获,告知電腦此次任務(wù)完成,結(jié)束資源占用曹仗。
myqueue.join()
方法和多進(jìn)程中的 pool.join()
方法作用大致相同榨汤,防止主線程結(jié)束后殺掉子線程。
測試結(jié)果:
爬取圖片數(shù)量 | 耗時 |
---|---|
555張 | 32.97秒 |
總結(jié)
方案結(jié)果匯總:
方案 | 爬取圖片數(shù)量/張 | 耗時/秒 |
---|---|---|
單線程 | 571 | 102.9 |
多進(jìn)程 map | 571 | 37.88 |
多進(jìn)程 map_async | 571 | 38.54 |
多線程 | 555 | 32.97 |
- 多線程以 32.97 秒的時間排名第一
- 多進(jìn)程中的 map 和 map_async 效率相差不遠(yuǎn) 怎茫,位列第二
- 單線程速度最慢收壕,位列第三
此次爬取試驗中,多線程下載圖片相對其他方案來說少了 16 張轨蛤,具體原因還沒有查過蜜宪,影響較小,暫時不做處理祥山。
在網(wǎng)上的討論中圃验,多線程適用于網(wǎng)頁請求以及 I/O 讀寫操作,多進(jìn)程適用于 CPU 密集型操作缝呕,由于作者水平有限澳窑,還沒有對線程、進(jìn)程供常、全局解釋器鎖(GIL)等知識進(jìn)行深入了解摊聋,下一次,有機會在做深入學(xué)習(xí)栈暇。