前置說明
關於python多線程和多進程的說明,請參考如下:
https://zhuanlan.zhihu.com/p/46368084 (一位知乎用戶)
https://www.liaoxuefeng.com/wiki/1016959663602400/1017628290184064 (廖雪峰)
這是我找到的兩篇很棒的文章,里面詳細說明的python多進程、多線程的原理以及用法,大家可以仔細看看
多進程爬蟲例子
用一個實例說明下如何使用多進程進行爬蟲
目標網站:https://imgbin.com/,本次爬取的也是一個圖片網站,里面是一些透明背景圖
1.首先看一下不添加多進程/多線程時的爬取速度,代碼如下
# -*- coding:utf-8 -*- import requests from requests.exceptions import RequestException import os, time from lxml import etree def get_html(url): """獲取頁面內容""" response = requests.get(url, timeout=15) # print(response.status_code) try: if response.status_code == 200: # print(response.text) return response.text else: return None except RequestException: print("請求失敗") # return None def parse_html(html_text): """解析頁面內容,提取圖片url""" html = etree.HTML(html_text) if len(html) > 0: img_src = html.xpath("//img[@class='photothumb lazy']/@data-original") # 元素提取方法 # print(img_src) return img_src else: print("解析頁面元素失敗") def get_image_content(url): """請求圖片url,返回二進制內容""" try: r = requests.get(url, timeout=15) if r.status_code == 200: return r.content return None except RequestException: return None def main(depth=None): """ 主函數,下載圖片 :param depth: 爬取頁碼 :return: """ j = 1 base_url = 'https://imgbin.com/free-png/naruto/' # 定義初始url for i in range(1, depth): url = base_url + str(i) # 根據頁碼遍歷請求url html = get_html(url) # 解析每個頁面的內容 # print(html) if html: list_data = parse_html(html) # 提取頁面中的圖片url root_dir = os.path.dirname(os.path.abspath('.')) save_path = root_dir + '/pics/' # 定義保存路徑 for t in list_data: # 遍歷每個圖片url try: file_path = '{0}{1}.{2}'.format(save_path, str(j), 'jpg') if not os.path.exists(file_path): # 判斷是否存在文件,不存在則爬取 with open(file_path, 'wb') as f: f.write(get_image_content(t)) f.close() print('第{}個文件保存成功'.format(j)) else: print("第{}個文件已存在".format(j)) j = j + 1 except FileNotFoundError as e: print("遇到錯誤:", e) continue except TypeError as f: print("遇到錯誤:", f) continue else: print("無結果") if __name__ == '__main__': start = time.time() main(3) end = time.time() print(end-start)
測試了一下,晚上10點多,在當時的網速下,爬取2頁圖片,大概用了403s,並且下載失敗了幾張;
2.使用多進程爬取
如果要進行多進程爬取的話,必須要有一個准備並行執行的函數,既然要多進程爬取圖片,所以應該把下載圖片的功能定義為主函數
而上面代碼中的main()函數不適合作為主函數,它是用爬取頁碼作為參數的,而我們並行執行時並不是一次爬取多頁,而是並行爬取多個圖片(有點繞)
需要改造一下:
(1)定義一個函數,來提取所有頁面的圖片url,並存到一個列表中(下面代碼中的第39行:get_all_image_url()函數)
(2)定義一個主函數,接收圖片url,然后下載圖片(下面代碼中的第82行:main()函數)
代碼如下
1 # -*- coding:utf-8 -*- 2 import requests 3 from requests.exceptions import RequestException 4 from bs4 import BeautifulSoup 5 import bs4 6 import os, time 7 from hashlib import md5 8 from lxml import etree 9 from multiprocessing import Pool, Lock, cpu_count 10 13 def get_html(url): 14 response = requests.get(url, timeout=15) 15 # print(response.status_code) 16 try: 17 if response.status_code == 200: 18 19 # print(response.text) 20 return response.text 21 else: 22 return None 23 except RequestException: 24 print("請求失敗") 25 # return None 26 27 28 def parse_html(html_text): 29 html = etree.HTML(html_text) 30 31 if len(html) > 0: 32 img_src = html.xpath("//img[@class='photothumb lazy']/@data-original") # 元素提取方法 33 # print(img_src) 34 return img_src 35 36 else: 37 print("解析頁面元素失敗") 38 39 def get_all_image_url(page_number): 40 """ 41 獲取所有圖片的下載url 42 :param page_number: 爬取頁碼 43 :return: 所有圖片url的集合 44 """ 45 46 base_url = 'https://imgbin.com/free-png/naruto/' 47 image_urls = [] 48 49 x = 1 # 定義一個標識,用於給每個圖片url編號,從1遞增 50 for i in range(1, page_number): 51 url = base_url + str(i) # 根據頁碼遍歷請求url 52 try: 53 html = get_html(url) # 解析每個頁面的內容 54 if html: 55 data = parse_html(html) # 提取頁面中的圖片url 56 # print(data) 57 # time.sleep(3) 58 if data: 59 for j in data: 60 image_urls.append({ 61 'name': x, 62 'value': j 63 }) 64 x += 1 # 每提取一個圖片url,標識x增加1 65 except RequestException as f: 66 print("遇到錯誤:", f) 67 continue 68 # print(image_urls) 69 return image_urls 70 71 def get_image_content(url): 72 """請求圖片url,返回二進制內容""" 73 # print("正在下載", url) 74 try: 75 r = requests.get(url, timeout=15) 76 if r.status_code == 200: 77 return r.content 78 return None 79 except RequestException: 80 return None 81 82 def main(url, name): 83 """ 84 主函數:實現下載圖片功能 85 :param url: 圖片url 86 :param name: 圖片名稱 87 :return: 88 """ 89 save_path = os.path.dirname(os.path.abspath('.')) + '/pics/' 90 try: 91 file_path = '{0}/{1}.jpg'.format(save_path, name) 92 if not os.path.exists(file_path): # 判斷是否存在文件,不存在則爬取 93 with open(file_path, 'wb') as f: 94 f.write(get_image_content(url)) 95 f.close() 96 97 print('第{}個文件保存成功'.format(name)) 98 else: 99 print("第{}個文件已存在".format(name)) 100 101 except FileNotFoundError as f: 102 print("第{}個文件下載時遇到錯誤,url為:{}:".format(name, url)) 103 print("報錯:", f) 104 raise 105 106 except TypeError as e: 107 print("第{}個文件下載時遇到錯誤,url為:{}:".format(name, url)) 108 print("報錯:", e) 109 110 111 if __name__ == '__main__': 112 start = time.time() 113 urls = get_all_image_url(3) # 獲取所有圖片url列表,爬取2頁內容 114 # print(urls) 115 # print(cpu_count()) # 查看電腦是幾核的 116 117 pool = Pool(6) # 我的電腦是6核的,所以開啟6個線程試試 118 119 for t in urls: # 遍歷列表中的每個圖片下載url 120 # print(i) 121 pool.apply_async(main, args=(t["value"], t["name"])) # 使用apply_async函數實現多進程(並行請求url,下載圖片) 122 123 pool.close() 124 pool.join() 125 126 end = time.time() 127 print(end-start)
開啟了6個進程,晚上10點多,同樣爬取2頁內容,大概用了30s,速度提升還是挺明顯的
多線程爬蟲例子
看了開頭分享的兩篇文章后,應該了解到如下2點:
1、python解釋器有GIL全局鎖,導致多線程不能利用多核,多線程並發並不能在python中實現;
2、任務類型分為計算密集型和IO密集型,對於IO密集型任務,大部分時間都在等待IO操作完成,在等待時間中CPU是不需要工作的,即使提供多核CPU也利用不上
網絡爬蟲屬於IO密集型任務,發送網絡請求等待響應、把爬取圖片保存到本地,很多時間都消耗在等待中,如果啟動多線程會明顯提高效率
改造一下上面的代碼,由多進程爬蟲改為多線程爬蟲,如下
1 # -*- coding:utf-8 -*- 2 import requests 3 from requests.exceptions import RequestException 4 import os, time 5 from lxml import etree 6 import threading 7 8 9 def get_html(url): 10 response = requests.get(url, timeout=15) 11 # print(response.status_code) 12 try: 13 if response.status_code == 200: 14 15 # print(response.text) 16 return response.text 17 else: 18 return None 19 except RequestException: 20 print("請求失敗") 21 # return None 22 23 24 def parse_html(html_text): 25 html = etree.HTML(html_text) 26 27 if len(html) > 0: 28 img_src = html.xpath("//img[@class='photothumb lazy']/@data-original") # 元素提取方法 29 # print(img_src) 30 return img_src 31 32 else: 33 print("解析頁面元素失敗") 34 35 def get_all_image_url(page_number): 36 """ 37 獲取所有圖片的下載url 38 :param page_number: 爬取頁碼 39 :return: 所有圖片url的集合 40 """ 41 42 base_url = 'https://imgbin.com/free-png/naruto/' 43 image_urls = [] 44 45 x = 1 # 定義一個標識,用於給每個圖片url編號,從1遞增 46 for i in range(1, page_number): 47 url = base_url + str(i) # 根據頁碼遍歷請求url 48 try: 49 html = get_html(url) # 解析每個頁面的內容 50 if html: 51 data = parse_html(html) # 提取頁面中的圖片url 52 # print(data) 53 # time.sleep(3) 54 if data: 55 for j in data: 56 image_urls.append({ 57 'name': x, 58 'value': j 59 }) 60 x += 1 # 每提取一個圖片url,標識x增加1 61 except RequestException as f: 62 print("遇到錯誤:", f) 63 continue 64 # print(image_urls) 65 return image_urls 66 67 def get_image_content(url): 68 """請求圖片url,返回二進制內容""" 69 # print("正在下載", url) 70 try: 71 r = requests.get(url, timeout=15) 72 if r.status_code == 200: 73 return r.content 74 return None 75 except RequestException: 76 return None 77 78 def main(url, image_name): 79 """ 80 主函數:實現下載圖片功能 81 :param url: 圖片url 82 :param image_name: 圖片名稱 83 :return: 84 """ 85 print('當前子線程: {}'.format(threading.current_thread().name)) 86 save_path = os.path.dirname(os.path.abspath('.')) + '/pics/' 87 try: 88 file_path = '{0}/{1}.jpg'.format(save_path, image_name) 89 if not os.path.exists(file_path): # 判斷是否存在文件,不存在則爬取 90 with open(file_path, 'wb') as f: 91 f.write(get_image_content(url)) 92 f.close() 93 94 print('第{}個文件保存成功'.format(image_name)) 95 else: 96 print("第{}個文件已存在".format(image_name)) 97 98 except FileNotFoundError as f: 99 print("第{}個文件下載時遇到錯誤,url為:{}:".format(image_name, url)) 100 print("報錯:", f) 101 raise 102 103 except TypeError as e: 104 print("第{}個文件下載時遇到錯誤,url為:{}:".format(image_name, url)) 105 print("報錯:", e) 106 107 108 if __name__ == '__main__': 109 start = time.time() 110 print('這是主線程:{}'.format(threading.current_thread().name)) 111 112 urls = get_all_image_url(3) # 獲取所有圖片url列表 113 thread_list = [] # 定義一個列表,向里面追加線程 114 115 for t in urls: 116 # print(i) 117 m = threading.Thread(target=main, args=(t["value"], t["name"])) # 調用threading.Thread方法,傳入主函數及其參數,啟動多線程 118 119 thread_list.append(m) 120 121 for m in thread_list: 122 m.start() # 調用start()方法,開始執行 123 124 for m in thread_list: 125 m.join() # 子線程調用join()方法,使主線程等待子線程運行完畢之后才退出 126 127 128 end = time.time() 129 print(end-start)
同樣爬取2頁,因為有100張圖片,所以一共啟動了100個子線程,耗時大約6.5s
如果打開文件夾來看的話,圖片是一下子都出現的
通過對比,可以看到對於網絡爬蟲這種IO密集型任務,多線程的效率其實是比多進程高的(6.5s VS 29.9s)
小結:本篇通過一個圖片爬蟲實例來說了一下如何使用python的多線程與多進程,對比單線程爬蟲效率有明顯提高,更多細節請自行查看,網上有很多優質資料,這里就不細說了