[Python爬蟲] 08. async 和 await 非同步爬蟲

 · 5 mins read

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



ref: