一、基本概念
進程:進程是一個具有獨立功能的程序關於某個數據集合的一次運行活動。進程是操作系統動態執行的基本單元。
線程:一個進程中包含若干線程,當然至少有一個線程,線程可以利用進程所擁有的資源。線程是獨立運行和獨立調度的基本單元。
協程:協程是一種用戶態的輕量級線程。協程無需線程上下文切換的開銷,也無需原子操作鎖定及同步的開銷。
同步:不同程序單元為了完成某個任務,在執行過程中需靠某種通信方式以協調一致,稱這些程序單元是同步執行的。
異步:為完成某個任務,不同程序單元之間過程中無需通信協調,也能完成任務的方式,不相關的程序單元之間可以是異步的。
多進程:多進程就是利用 CPU 的多核優勢,在同一時間並行地執行多個任務。多進程模式優點就是穩定性高,因為一個子進程崩潰了,不會影響主進程和其他子進程,但是操作系統能同時運行的進程數是有限的。
多線程:多線程模式通常比多進程快一點,但是也快不到哪去,而且,多線程模式致命的缺點就是任何一個線程掛掉都可能直接造成整個進程崩潰,因為所有線程共享進程的內存。
二、異步協程
Python 中使用協程最常用的庫莫過於 asyncio,然后我們還需要了解一些概念:
event_loop:事件循環,相當於一個無限循環,我們可以把一些函數注冊到這個事件循環上,當滿足條件發生的時候,就會調用對應的處理方法。
coroutine:協程對象類型,我們可以將協程對象注冊到事件循環中,它會被事件循環調用。我們可以使用 async 關鍵字來定義一個方法,這個方法在調用時不會立即被執行,而是返回一個協程對象。
task:任務,它是對協程對象的進一步封裝,包含了任務的各個狀態,比如 running、finished 等。
另外我們還需要了解兩個關鍵字:async(定義一個協程),await(用來掛起阻塞方法的執行)。下面是一個示例:
1 import asyncio 2 3 4 async def show(num): 5 print("Number is {}".format(num)) 6 7 8 cor = show(1) 9 print("Coroutine: ", cor) 10 print("After execute...") 11 task = asyncio.ensure_future(cor) 12 print("Task: ", task) 13 loop = asyncio.get_event_loop() 14 loop.run_until_complete(cor) 15 print("Task: ", task) 16 print("After loop...")
運行結果如下:
Coroutine: <coroutine object show at 0x0000000012ED91A8>
After execute...
Task: <Task pending coro=<show() running at E:/Python/1.py:4>>
Number is 1
Task: <Task finished coro=<show() done, defined at E:/Python/1.py:4> result=None>
After loop...
這里首先使用async定義了一個show方法,傳入一個數字然后打印出來,我們調用了這個方法,但是這個方法並沒有執行,而是返回了一個Coroutine協程對象。然后我們使用了asyncio的ensure_future()方法,該方法會返回一個task對象,此時task的狀態是pending。然后我們使用 get_event_loop() 方法創建了一個事件循環 loop,並調用了run_until_complete() 方法將協程注冊到事件循環loop中,然后啟動。最后我們才看到了show() 方法打印了輸出結果,此時task的狀態已經是finished了。
再來看一個例子:
1 import time
2 import asyncio
3
4
5 async def show(num):
6 print("Number is {}".format(num))
7 await asyncio.sleep(1) # 必須加await實現協程 這里asyncio.sleep(1)是一個子協程
8 # time.sleep(1) # time.sleep()不能與await搭配使用
9
10
11 start = time.time()
12 tasks = [asyncio.ensure_future(show(i)) for i in [1, 2, 3, 4, 5]]
13
14 loop = asyncio.get_event_loop()
15 loop.run_until_complete(asyncio.wait(tasks))
16 end = time.time()
17 print("Cost time: ", end - start)
這里我們有多個任務組成了一個列表tasks,然后我們將tasks添加到事件循環中,等到執行完畢了打印出所花費的時間。當我們使用await asyncio.sleep(1)的時候,結果如下:
Number is 1
Number is 2
Number is 3
Number is 4
Number is 5
Cost time: 1.0040574073791504
使用time,sleep(1)的時候結果如下:
Number is 1
Number is 2
Number is 3
Number is 4
Number is 5
Cost time: 5.001286029815674
結果很明顯了,前者所花費的時間更少,原因在於await會將asyncio.sleep(1)這個協程暫時掛起阻塞,第一個任務(show(1))運行到這里的時候就會掛起,然后執行下一個任務(show(2)),以此類推,等到所有的任務都執行完畢,再執行asyncio.sleep(1),所以最后花費的時間就是一秒多一點了。
三、編寫爬蟲
1、aiohttp
要利用協程來寫網絡爬蟲,還需要使用一個第三方庫--aiohttp,aiohttp是一個支持異步請求的庫,利用它和 asyncio配合我們可以非常方便地實現異步請求操作。沒有安裝的可以使用pip install aiohttp進行安裝,其官方文檔的鏈接是:https://aiohttp.readthedocs.io/en/stable/,需要注意的是aiohttp支持的python版本是3.5.3+,如果運行出錯的話建議先檢查下你的python版本。先來看看官網上給出的例子吧:
1 import aiohttp 2 import asyncio 3 4 async def fetch(session, url): 5 async with session.get(url) as response: 6 return await response.text() 7 8 async def main(): 9 async with aiohttp.ClientSession() as session: 10 html = await fetch(session, 'http://python.org') 11 print(html) 12 13 loop = asyncio.get_event_loop() 14 loop.run_until_complete(main())
首先是導入我們需要的模塊,然后定義了一個fetch方法,傳入的參數是一個session和一個url,然后使用session的get()方法去請求這個鏈接,並返回結果。在main方法中,首先引用了aiohttp里的ClientSession類,建立 了一個session對象,然后將這個session和一個鏈接傳入到fetch方法中,最后將fetch方法返回的結果打印出來。
2、具體步驟
這次寫的爬蟲實現了對崔慶才的個人博客上的文章基本信息的爬取,包括標題、鏈接、瀏覽的數目、評論的數目以及喜歡的人數,最后分別將瀏覽數、評論數以及喜歡數排前十的文章統計出來並繪制出圖表。
首先進入崔慶才個人博客,可以看到一頁有二十篇文章,把頁面下拉,就會出現更多的文章,顯然這是動態加載的,於是我們打開開發者工具,繼續下拉頁面,然后在XHR選項中看到了我們需要的內容:
不停地下拉頁面,會發現最后數字會定格在35,也就是說總共有35頁,每頁的鏈接都形如https://cuiqingcai.com/page/2,這樣的話我們爬取的話就簡單多了。基本思路是將所有鏈接組成一個列表,然后利用aiohttp去請求網頁並返回結果,然后我們再對結果進行解析,對於解析得到的結果,保存在MongoDB數據庫中。然后再對數據進行一下簡單的分析,並繪制圖表,結果如下:
完整代碼已上傳到GitHub!