爬蟲第六篇
python採用 多進程/多線程/協程 寫爬蟲
從操作系統的角度:
進程和線程,都是一種CPU的執行單元。
進程:表示一個程序的上下文執行活動(打開、執行、保存…)
線程:進程執行程序時候的最小調度單位(執行a,執行b…)
一個程序至少有一個進程,一個進程至少有一個線程。
並行:多個CPU核心,不同的程序就分配給不同的CPU來運行。可以讓多個程序同時執行
併發:單個CPU核心,在一個時間切片裏一次只能運行一個程序,如果需要運行多個程序,需要在多個程序間來回切換
多進程/多線程:表示可以同時執行多個任務,進程和線程的調度是由操作系統自動完成。
進程:每個進程都有自己獨立的內存空間,不同進程之間的內存空間不共享。
進程之間的通信有操作系統傳遞,導致通訊效率低,切換開銷大。
線程:一個進程可以有多個線程,所有線程共享進程的內存空間,通訊效率高,切換開銷小。
共享意味着競爭,導致數據不安全,爲了保護內存空間的數據安全,引入"互斥鎖"。
一個線程在訪問內存空間的時候,其他線程不允許訪問,必須等待之前的線程訪問結束,才能使用這個內存空間。
互斥鎖:一種安全有序的讓多個線程訪問內存空間的機制。
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性能還是比較高.
多線程多進程爬蟲
from multiprocessing import Process, Queue
# 多進程
p = Process(target=func, name, args, kwargs)
# 隊列
q = Queue()
q.put(url)
q.get()
q.empty()
多進程/多線程爬蟲的流程圖
import requests
from multiprocessing import Queue
from threading import Thread
from lxml import etree
import time
class XiaomiSpider:
def __init__(self):
self.baseurl = 'http://app.mi.com/category/12#page='
self.mainurl = 'http://app.mi.com'
self.headers = {"User-Agent":"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)"}
# URL隊列
self.urlQueue = Queue()
# 解析隊列
self.parseQueue = Queue()
# URL入隊列
def getUrl(self):
for page in range(20):
url = self.baseurl + str(page)
# 把拼接的url放到url隊列中
self.urlQueue.put(url)
# 採集線程函數,get出URL發請求,把html給解析隊列
def getHtml(self):
while True:
# 先判斷隊列是否爲空
if not self.urlQueue.empty():
url = self.urlQueue.get()
# 三步走
res = requests.get(url,headers=self.headers)
res.encoding = "utf-8"
html = res.text
# 把html放到解析隊列
self.parseQueue.put(html)
else:
break
# 解析線程函數,get出html源碼,提取並處理數據
def getData(self):
while True:
if not self.parseQueue.empty():
html = self.parseQueue.get()
# 創建解析對象,調用xpath
parseHtml = etree.HTML(html)
# [li1對象,li2對象]
baseList = parseHtml.xpath('//ul[@id="all-applist"]//li')
for base in baseList:
# 應用名稱
name = base.xpath('./h5/a/text()')[0]
# 應用鏈接
link = self.mainurl + base.xpath('./h5/a/@href')[0]
d = {
"分類":"學習教育",
"名稱":name,
"鏈接":link,
}
with open("XM.json","a", encoding="utf-8") as f:
f.write(str(d)+'\n')
else:
break
# 主函數
def workOn(self):
# url先入隊列
self.getUrl()
# 存放所有采集線程對象和解析線程對象列表
tList = []
# 統一創建線程,採集和解析一起幹活
for i in range(5):
t1 = Thread(target=self.getHtml)
t2 = Thread(target=self.getData)
tList.append(t1)
tList.append(t2)
t1.start()
t2.start()
# 統一回收採集線程和解析線程
for i in tList:
i.join()
if __name__ == "__main__":
start = time.time()
spider = XiaomiSpider()
spider.workOn()
end = time.time()
print("執行時間:%.2f" % (end - start))