python采用 多進程/多線程/協程 寫爬蟲以及性能對比,牛逼的分分鐘就將一個網站爬下來!
首先我們來了解下python中的進程,線程以及協程!
從計算機硬件角度:
計算機的核心是CPU,承擔了所有的計算任務。
一個CPU,在一個時間切片裏只能運行一個程序。
從操作系統的角度:
進程和線程,都是一種CPU的執行單元。
進程:表示一個程序的上下文執行活動(打開、執行、保存...)
線程:進程執行程序時候的最小調度單位(執行a,執行b...)
一個程序至少有一個進程,一個進程至少有一個線程。
並行 和 並發:
並行:多個CPU核心,不同的程序就分配給不同的CPU來運行。可以讓多個程序同時執行。
cpu1 -------------
cpu2 -------------
cpu3 -------------
cpu4 -------------
並發:單個CPU核心,在一個時間切片裏一次只能運行一個程序,如果需要運行多個程序,則串行執行。
cpu1 ---- ----
cpu1 ---- ----
多進程/多線程:
表示可以同時執行多個任務,進程和線程的調度是由操作系統自動完成。
進程:每個進程都有自己獨立的內存空間,不同進程之間的內存空間不共享。
進程之間的通信有操作系統傳遞,導致通訊效率低,切換開銷大。
線程:一個進程可以有多個線程,所有線程共享進程的內存空間,通訊效率高,切換開銷小。
共享意味著競爭,導致數據不安全,為了保護內存空間的數據安全,引入"互斥鎖"。
一個線程在訪問內存空間的時候,其他線程不允許訪問,必須等待之前的線程訪問結束,才能使用這個內存空間。
互斥鎖:一種安全有序的讓多個線程訪問內存空間的機制。
Python的多線程:
GIL 全局解釋器鎖:線程的執行權限,在Python的進程裏只有一個GIL。
一個線程需要執行任務,必須獲取GIL。
好處:直接杜絕了多個線程訪問內存空間的安全問題。
壞處:Python的多線程不是真正多線程,不能充分利用多核CPU的資源。
但是,在I/O阻塞的時候,解釋器會釋放GIL。
所以:
多進程:密集CPU任務,需要充分使用多核CPU資源(服務器,大量的並行計算)的時候,用多進程。 multiprocessing
缺陷:多個進程之間通信成本高,切換開銷大。
多線程:密集I/O任務(網絡I/O,磁盤I/O,數據庫I/O)使用多線程合適。
threading.Thread、multiprocessing.dummy
缺陷:同一個時間切片只能運行一個線程,不能做到高並行,但是可以做到高並發。
協程:又稱微線程,在單線程上執行多個任務,用函數切換,開銷極小。不通過操作系統調度,沒有進程、線程的切換開銷。genvent,monkey.patchall
多線程請求返回是無序的,那個線程有數據返回就處理那個線程,而協程返回的數據是有序的。
缺陷:單線程執行,處理密集CPU和本地磁盤IO的時候,性能較低。處理網絡I/O性能還是比較高.
下面以這個網站為例,采用三種方式爬取。爬取前250名的電影。。
https://movie.douban.com/top250?start=0
通過分析網頁發現第2頁的url start=25,第3頁的url start=50,第3頁的start=75。因此可以得出這個網站每一頁的數局是通過遞增start這個參數獲取的。
一般不看第一頁的數據,第一頁的沒有參考價值。
這次我們主要爬取,電影名字跟評分。只是使用不同方式去對比下不同點,所以數據方面就不過多提取或者保存。只是簡單的將其爬取下打印出來看看。
第一:采用多進程 , multiprocessing 模塊。 當然這個耗時更網絡好壞有關。在全部要請求都正常的情況下耗時15s多。
#!/usr/bin/env python2 # -*- coding=utf-8 -*- from multiprocessing import Process, Queue import time from lxml import etree import requests class DouBanSpider(Process): def __init__(self, url, q): # 重寫寫父類的__init__方法 super(DouBanSpider, self).__init__() self.url = url self.q = q self.headers = { ‘Host‘: ‘movie.douban.com‘, ‘Referer‘: ‘https://movie.douban.com/top250?start=225&filter=‘, ‘User-Agent‘: ‘Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36‘, } def run(self): self.parse_page() def send_request(self,url): ‘‘‘ 用來發送請求的方法 :return: 返回網頁源碼 ‘‘‘ # 請求出錯時,重復請求3次, i = 0 while i <= 3: try: print u"[INFO]請求url:"+url return requests.get(url=url,headers=self.headers).content except Exception as e: print u‘[INFO] %s%s‘% (e,url) i += 1 def parse_page(self): ‘‘‘ 解析網站源碼,並采用xpath提取 電影名稱和平分放到隊列中 :return: ‘‘‘ response = self.send_request(self.url) html = etree.HTML(response) # 獲取到一頁的電影數據 node_list = html.xpath("//div[@class=‘info‘]") for move in node_list: # 電影名稱 title = move.xpath(‘.//a/span/text()‘)[0] # 評分 score = move.xpath(‘.//div[@class="bd"]//span[@class="rating_num"]/text()‘)[0] # 將每一部電影的名稱跟評分加入到隊列 self.q.put(score + "\t" + title) def main(): # 創建一個隊列用來保存進程獲取到的數據 q = Queue() base_url = ‘https://movie.douban.com/top250?start=‘ # 構造所有url url_list = [base_url+str(num) for num in range(0,225+1,25)] # 保存進程 Process_list = [] # 創建並啟動進程 for url in url_list: p = DouBanSpider(url,q) p.start() Process_list.append(p) # 讓主進程等待子進程執行完成 for i in Process_list: i.join() while not q.empty(): print q.get() if __name__=="__main__": start = time.time() main() print ‘[info]耗時:%s‘%(time.time()-start)Process多進程實現
#!/usr/bin/env python2 # -*- coding=utf-8 -*- from multiprocessing import Process, Queue import time from lxml import etree import requests class DouBanSpider(Process): def __init__(self, url, q): # 重寫寫父類的__init__方法 super(DouBanSpider, self).__init__() self.url = url self.q = q self.headers = { ‘Host‘: ‘movie.douban.com‘, ‘Referer‘: ‘https://movie.douban.com/top250?start=225&filter=‘, ‘User-Agent‘: ‘Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36‘, } def run(self): self.parse_page() def send_request(self,url): ‘‘‘ 用來發送請求的方法 :return: 返回網頁源碼 ‘‘‘ # 請求出錯時,重復請求3次, i = 0 while i <= 3: try: print u"[INFO]請求url:"+url return requests.get(url=url,headers=self.headers).content except Exception as e: print u‘[INFO] %s%s‘% (e,url) i += 1 def parse_page(self): ‘‘‘ 解析網站源碼,並采用xpath提取 電影名稱和平分放到隊列中 :return: ‘‘‘ response = self.send_request(self.url) html = etree.HTML(response) # 獲取到一頁的電影數據 node_list = html.xpath("//div[@class=‘info‘]") for move in node_list: # 電影名稱 title = move.xpath(‘.//a/span/text()‘)[0] # 評分 score = move.xpath(‘.//div[@class="bd"]//span[@class="rating_num"]/text()‘)[0] # 將每一部電影的名稱跟評分加入到隊列 self.q.put(score + "\t" + title) def main(): # 創建一個隊列用來保存進程獲取到的數據 q = Queue() base_url = ‘https://movie.douban.com/top250?start=‘ # 構造所有url url_list = [base_url+str(num) for num in range(0,225+1,25)] # 保存進程 Process_list = [] # 創建並啟動進程 for url in url_list: p = DouBanSpider(url,q) p.start() Process_list.append(p) # 讓主進程等待子進程執行完成 for i in Process_list: i.join() while not q.empty(): print q.get() if __name__=="__main__": start = time.time() main() print ‘[info]耗時:%s‘%(time.time()-start)
采用多線程時,耗時10.4s
#!/usr/bin/env python2 # -*- coding=utf-8 -*- from threading import Thread from Queue import Queue import time from lxml import etree import requests class DouBanSpider(Thread): def __init__(self, url, q): # 重寫寫父類的__init__方法 super(DouBanSpider, self).__init__() self.url = url self.q = q self.headers = { ‘Cookie‘: ‘ll="118282"; bid=ctyiEarSLfw; ps=y; __yadk_uid=0Sr85yZ9d4bEeLKhv4w3695OFOPoedzC; dbcl2="155150959:OEu4dds1G1o"; as="https://sec.douban.com/b?r=https%3A%2F%2Fbook.douban.com%2F"; ck=fTrQ; _pk_id.100001.4cf6=c86baf05e448fb8d.1506160776.3.1507290432.1507283501.; _pk_ses.100001.4cf6=*; __utma=30149280.1633528206.1506160772.1507283346.1507290433.3; __utmb=30149280.0.10.1507290433; __utmc=30149280; __utmz=30149280.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.1475767059.1506160772.1507283346.1507290433.3; __utmb=223695111.0.10.1507290433; __utmc=223695111; __utmz=223695111.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_noty_num=0; push_doumail_num=0‘, ‘Host‘: ‘movie.douban.com‘, ‘Referer‘: ‘https://movie.douban.com/top250?start=225&filter=‘, ‘User-Agent‘: ‘Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36‘, } def run(self): self.parse_page() def send_request(self,url): ‘‘‘ 用來發送請求的方法 :return: 返回網頁源碼 ‘‘‘ # 請求出錯時,重復請求3次, i = 0 while i <= 3: try: print u"[INFO]請求url:"+url html = requests.get(url=url,headers=self.headers).content except Exception as e: print u‘[INFO] %s%s‘% (e,url) i += 1 else: return html def parse_page(self): ‘‘‘ 解析網站源碼,並采用xpath提取 電影名稱和平分放到隊列中 :return: ‘‘‘ response = self.send_request(self.url) html = etree.HTML(response) # 獲取到一頁的電影數據 node_list = html.xpath("//div[@class=‘info‘]") for move in node_list: # 電影名稱 title = move.xpath(‘.//a/span/text()‘)[0] # 評分 score = move.xpath(‘.//div[@class="bd"]//span[@class="rating_num"]/text()‘)[0] # 將每一部電影的名稱跟評分加入到隊列 self.q.put(score + "\t" + title) def main(): # 創建一個隊列用來保存進程獲取到的數據 q = Queue() base_url = ‘https://movie.douban.com/top250?start=‘ # 構造所有url url_list = [base_url+str(num) for num in range(0,225+1,25)] # 保存線程 Thread_list = [] # 創建並啟動線程 for url in url_list: p = DouBanSpider(url,q) p.start() Thread_list.append(p) # 讓主線程等待子線程執行完成 for i in Thread_list: i.join() while not q.empty(): print q.get() if __name__=="__main__": start = time.time() main() print ‘[info]耗時:%s‘%(time.time()-start)thread
#!/usr/bin/env python2 # -*- coding=utf-8 -*- from threading import Thread from Queue import Queue import time from lxml import etree import requests class DouBanSpider(Thread): def __init__(self, url, q): # 重寫寫父類的__init__方法 super(DouBanSpider, self).__init__() self.url = url self.q = q self.headers = { ‘Cookie‘: ‘ll="118282"; bid=ctyiEarSLfw; ps=y; __yadk_uid=0Sr85yZ9d4bEeLKhv4w3695OFOPoedzC; dbcl2="155150959:OEu4dds1G1o"; as="https://sec.douban.com/b?r=https%3A%2F%2Fbook.douban.com%2F"; ck=fTrQ; _pk_id.100001.4cf6=c86baf05e448fb8d.1506160776.3.1507290432.1507283501.; _pk_ses.100001.4cf6=*; __utma=30149280.1633528206.1506160772.1507283346.1507290433.3; __utmb=30149280.0.10.1507290433; __utmc=30149280; __utmz=30149280.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.1475767059.1506160772.1507283346.1507290433.3; __utmb=223695111.0.10.1507290433; __utmc=223695111; __utmz=223695111.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_noty_num=0; push_doumail_num=0‘, ‘Host‘: ‘movie.douban.com‘, ‘Referer‘: ‘https://movie.douban.com/top250?start=225&filter=‘, ‘User-Agent‘: ‘Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36‘, } def run(self): self.parse_page() def send_request(self,url): ‘‘‘ 用來發送請求的方法 :return: 返回網頁源碼 ‘‘‘ # 請求出錯時,重復請求3次, i = 0 while i <= 3: try: print u"[INFO]請求url:"+url html = requests.get(url=url,headers=self.headers).content except Exception as e: print u‘[INFO] %s%s‘% (e,url) i += 1 else: return html def parse_page(self): ‘‘‘ 解析網站源碼,並采用xpath提取 電影名稱和平分放到隊列中 :return: ‘‘‘ response = self.send_request(self.url) html = etree.HTML(response) # 獲取到一頁的電影數據 node_list = html.xpath("//div[@class=‘info‘]") for move in node_list: # 電影名稱 title = move.xpath(‘.//a/span/text()‘)[0] # 評分 score = move.xpath(‘.//div[@class="bd"]//span[@class="rating_num"]/text()‘)[0] # 將每一部電影的名稱跟評分加入到隊列 self.q.put(score + "\t" + title) def main(): # 創建一個隊列用來保存進程獲取到的數據 q = Queue() base_url = ‘https://movie.douban.com/top250?start=‘ # 構造所有url url_list = [base_url+str(num) for num in range(0,225+1,25)] # 保存線程 Thread_list = [] # 創建並啟動線程 for url in url_list: p = DouBanSpider(url,q) p.start() Thread_list.append(p) # 讓主線程等待子線程執行完成 for i in Thread_list: i.join() while not q.empty(): print q.get() if __name__=="__main__": start = time.time() main() print ‘[info]耗時:%s‘%(time.time()-start)
采用協程爬取,耗時15S,
#!/usr/bin/env python2 # -*- coding=utf-8 -*- from Queue import Queue import time from lxml import etree import requests import gevent # 打上猴子補丁 from gevent import monkey monkey.patch_all() class DouBanSpider(object): def __init__(self): # 創建一個隊列用來保存進程獲取到的數據 self.q = Queue() self.headers = { ‘Cookie‘: ‘ll="118282"; bid=ctyiEarSLfw; ps=y; __yadk_uid=0Sr85yZ9d4bEeLKhv4w3695OFOPoedzC; dbcl2="155150959:OEu4dds1G1o"; as="https://sec.douban.com/b?r=https%3A%2F%2Fbook.douban.com%2F"; ck=fTrQ; _pk_id.100001.4cf6=c86baf05e448fb8d.1506160776.3.1507290432.1507283501.; _pk_ses.100001.4cf6=*; __utma=30149280.1633528206.1506160772.1507283346.1507290433.3; __utmb=30149280.0.10.1507290433; __utmc=30149280; __utmz=30149280.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.1475767059.1506160772.1507283346.1507290433.3; __utmb=223695111.0.10.1507290433; __utmc=223695111; __utmz=223695111.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_noty_num=0; push_doumail_num=0‘, ‘Host‘: ‘movie.douban.com‘, ‘Referer‘: ‘https://movie.douban.com/top250?start=225&filter=‘, ‘User-Agent‘: ‘Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36‘, } def run(self,url): self.parse_page(url) def send_request(self,url): ‘‘‘ 用來發送請求的方法 :return: 返回網頁源碼 ‘‘‘ # 請求出錯時,重復請求3次, i = 0 while i <= 3: try: print u"[INFO]請求url:"+url html = requests.get(url=url,headers=self.headers).content except Exception as e: print u‘[INFO] %s%s‘% (e,url) i += 1 else: return html def parse_page(self,url): ‘‘‘ 解析網站源碼,並采用xpath提取 電影名稱和平分放到隊列中 :return: ‘‘‘ response = self.send_request(url) html = etree.HTML(response) # 獲取到一頁的電影數據 node_list = html.xpath("//div[@class=‘info‘]") for move in node_list: # 電影名稱 title = move.xpath(‘.//a/span/text()‘)[0] # 評分 score = move.xpath(‘.//div[@class="bd"]//span[@class="rating_num"]/text()‘)[0] # 將每一部電影的名稱跟評分加入到隊列 self.q.put(score + "\t" + title) def main(self): base_url = ‘https://movie.douban.com/top250?start=‘ # 構造所有url url_list = [base_url+str(num) for num in range(0,225+1,25)] # 創建協程並執行 job_list = [gevent.spawn(self.run,url) for url in url_list] # 讓線程等待所有任務完成,再繼續執行。 gevent.joinall(job_list) while not self.q.empty(): print self.q.get() if __name__=="__main__": start = time.time() douban = DouBanSpider() douban.main() print ‘[info]耗時:%s‘%(time.time()-start)gevent
用了多進程,多線程,協程,實現的代碼都一樣,沒有測試出明顯的那個好!都不分上下,可能跟網絡,或者服務器配置有關。
但理論上來說線程,協程在I/O密集的操作性能是要高於進程的。
也可能是我的方法有問題,還望大神們指教!
python采用 多進程/多線程/協程 寫爬蟲以及性能對比,牛逼的分分鐘就將一個網站爬下來!