python 中的 request,是發出一個 request 後,它會等待 server 回應後再發出下一個 request,這段等待的時間我們的機器是閒置的,為了提升效率所以採用非同步的方式。
Async 與 Sync 的差易 1
你填好單子送到櫃檯,然後…… a. 要站在櫃檯等櫃檯人員辦完 → Sync b. 櫃檯人員辦完再叫你的號碼 → Async,因為不用站在櫃檯等,你就可以跑很多櫃檯、送很多張單子出去。
- 在傳統的順序程式設計中, 所有傳送給直譯器的指令會一條條被執行。
- 阻塞:程序在等待某個操作完成期間,自身無法繼續幹別的事情,則稱該程序在該操作上是阻塞的。
- Sync 同步:不同程序單元為了完成某個任務,在執行過程中需靠某種通訊方式以協調一致,稱這些程序單元是同步執行的,同步意味著有序。
- Async 非同步:過程中無需通訊協調,不相關的程序單元之間可以是非同步的,非同步意味著無序。在同一個 Process 上面,接收到需求不用一直等到需求完成再執行其他需求。
import Asyncio
- Python 內建,適用於 Python 3.5 之後的版本。(之前的版本是使用 @asyncio.coroutine 裝飾器)
async
:能夠非同步的功能(async def function():)。await
:切換的點。
程式範例
同步:有順序性 job1(1 秒) 完成才執行 job2(2 秒),總共花費 3 秒。
import time def job(t): print('Start job ', t) time.sleep(t) #wait for "t" seconds print('Job ', t, ' takes ', t, ' s') def main(): [job(t) for t in range(1, 3)] t1 = time.time() main() print("NO async total time : ", time.time() - t1) >>> """ Start job 1 Job 1 takes 1 s Start job 2 Job 2 takes 2 s NO async total time : 3.008603096008301 """
Async
非同步:job 1 觸發後就開始 job2,最後同時等待 asyncio.sleep(t),所以最長等待時間為花費最久時間的任務(job 2 2秒)。import asyncio async def job(t): #async形式的功能 print('Start job ', t) await asyncio.sleep(t) #await:等待"t"秒,期間切换其他任務 print('Job ', t, ' takes ', t, ' s') async def main(loop): #async形式的功能 tasks = [ loop.create_task(job(t)) for t in range(1, 3) ] #初始化任務清單,但是不執行 await asyncio.wait(tasks) #模擬IO任務,執行並等待所有任務完成 t1 = time.time() loop = asyncio.get_event_loop() #get_event_loop()建立loop事件循環模型 loop.run_until_complete(main(loop)) #run_until_complete()執行loop loop.close() #close()關閉關閉事件循環清單 print("Async total time : ", time.time() - t1) >>> """ Start job 1 Start job 2 Job 1 takes 1 s Job 2 takes 2 s Async total time : 2.001495838165283 """
multiprocessing 和 Asyncio的差異
multiprocessing
多執行緒(左):有多條 process,process1 執行之後由 process2 去等待 process3 下載網頁,載完再去 process1 繼續執行。Asyncio
非同步(右):只有單 process,使用非同步計算,下載網頁和處理網頁時是不連續的,有效利用了等待下載的時間。
技術 | multiprocessing 多執行續 | Asyncio 非同步 |
---|---|---|
原理 | 不同 process 同時跑起來 | 同一 process 等待時間的有效利用 |
multiprocessing 分佈式爬蟲和 Asyncio 對比
用 multiprocessing 寫分佈式爬蟲 。
如果 Pool(n) 裡面的這個 n 越大,multiprocessing 才能越快,但是 asyncio 卻不會特別受 process 數的影響。
import aiohttp 官網
可將 requests 代替成一個非同步的 requests。
$ pip3 install aiohttp
程式範例
同步 requests:0.38秒(慢)。
import requests URL = 'https://morvanzhou.github.io/' def normal(): for i in range(2): r = requests.get(URL) url = r.url print(url) t1 = time.time() normal() print("Normal total time:", time.time()-t1) >>> """ https://morvanzhou.github.io/ https://morvanzhou.github.io/ Normal total time: 0.3869960308074951 """
async
非同步 requests:0.11秒(快)。import aiohttp async def job(session): response = await session.get(URL) #等待並切換 return str(response.url) async def main(loop): async with aiohttp.ClientSession() as session: #官網推薦建立Session的形式,也可以直接用request tasks = [loop.create_task(job(session)) for _ in range(2)] finished, unfinished = await asyncio.wait(tasks) #收集完成的結果,會返回完成的和沒完成的,等全部都完成了才返回 all_results = [r.result() for r in finished] #獲取所有結果 print(all_results) t1 = time.time() loop = asyncio.get_event_loop() loop.run_until_complete(main(loop)) loop.close() print("Async total time:", time.time() - t1) >>> """ ['https://morvanzhou.github.io/', 'https://morvanzhou.github.io/'] Async total time: 0.11447715759277344 """
應用非同步爬蟲 1
import aiohttp
import asyncio
import async_timeout
import time
from bs4 import BeautifulSoup
async def fetch_coroutine(client, url):
with async_timeout.timeout(10):
async with client.get(url) as response:
assert response.status == 200 #如果server端成功回應
html = await response.text() #取得html檔
soup = BeautifulSoup(html ,'lxml') #透過bs解析html
As = soup.find_all('a')
for a in As:
try:
print(a)
except:
print("Error")
return await response.release()
async def main(loop):
urls = ['http://python.org',
'http://python.org']
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'}
async with aiohttp.ClientSession(loop=loop, headers=headers, conn_timeout=5 ) as client:
tasks = [fetch_coroutine(client, url) for url in urls] #fetch_coroutine為重複執行的function
await asyncio.gather(*tasks) #把所有task打包
if __name__ == '__main__':
loop = asyncio.get_event_loop() #建立一個loop
loop.run_until_complete(main(loop)) #透過run_until_complete方法執行Main functio