日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

第16讲:异步爬虫的原理和解析

發布時間:2024/4/11 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 第16讲:异步爬虫的原理和解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

我們知道爬蟲是 IO 密集型任務,比如如果我們使用 requests 庫來爬取某個站點的話,發出一個請求之后,程序必須要等待網站返回響應之后才能接著運行,而在等待響應的過程中,整個爬蟲程序是一直在等待的,實際上沒有做任何的事情。對于這種情況我們有沒有優化方案呢?

實例引入

比如在這里我們看這么一個示例網站:https://static4.scrape.cuiqingcai.com/,如圖所示。

這個網站在內部實現返回響應的邏輯的時候特意加了 5 秒的延遲,也就是說如果我們用 requests 來爬取其中某個頁面的話,至少需要 5 秒才能得到響應。

另外這個網站的邏輯結構在之前的案例中我們也分析過,其內容就是電影數據,一共 100 部,每個電影的詳情頁是一個自增 ID,從 1~100,比如 https://static4.scrape.cuiqingcai.com/detail/43 就代表第 43 部電影,如圖所示。


下面我們來用 requests 寫一個遍歷程序,直接遍歷 1~100 部電影數據,代碼實現如下:

import requests import logging import time logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s: %(message)s') TOTAL_NUMBER = 100 BASE_URL = 'https://static4.scrape.cuiqingcai.com/detail/{id}' start_time = time.time() for id in range(1, TOTAL_NUMBER + 1):url = BASE_URL.format(id=id)logging.info('scraping %s', url)response = requests.get(url) end_time = time.time() logging.info('total time %s seconds', end_time - start_time)

這里我們直接用循環的方式構造了 100 個詳情頁的爬取,使用的是 requests 單線程,在爬取之前和爬取之后記錄下時間,最后輸出爬取了 100 個頁面消耗的時間。

運行結果如下:

2020-03-31 14:40:35,411 - INFO: scraping https://static4.scrape.cuiqingcai.com/detail/1 2020-03-31 14:40:40,578 - INFO: scraping https://static4.scrape.cuiqingcai.com/detail/2 2020-03-31 14:40:45,658 - INFO: scraping https://static4.scrape.cuiqingcai.com/detail/3 2020-03-31 14:40:50,761 - INFO: scraping https://static4.scrape.cuiqingcai.com/detail/4 2020-03-31 14:40:55,852 - INFO: scraping https://static4.scrape.cuiqingcai.com/detail/5 2020-03-31 14:41:00,956 - INFO: scraping https://static4.scrape.cuiqingcai.com/detail/6 ... 2020-03-31 14:48:58,785 - INFO: scraping https://static4.scrape.cuiqingcai.com/detail/99 2020-03-31 14:49:03,867 - INFO: scraping https://static4.scrape.cuiqingcai.com/detail/100 2020-03-31 14:49:09,042 - INFO: total time 513.6309871673584 seconds 2020-03-31 14:49:09,042 - INFO: total time 513.6309871673584 seconds

由于每個頁面都至少要等待 5 秒才能加載出來,因此 100 個頁面至少要花費 500 秒的時間,總的爬取時間最終為 513.6 秒,將近 9 分鐘。

這個在實際情況下是很常見的,有些網站本身加載速度就比較慢,稍慢的可能 1~3 秒,更慢的說不定 10 秒以上才可能加載出來。如果我們用 requests 單線程這么爬取的話,總的耗時是非常多的。此時如果我們開了多線程或多進程來爬取的話,其爬取速度確實會成倍提升,但有沒有更好的解決方案呢?

本課時我們就來了解一下使用異步執行方式來加速的方法,此種方法對于 IO 密集型任務非常有效。如將其應用到網絡爬蟲中,爬取效率甚至可以成百倍地提升。

基本了解

在了解異步協程之前,我們首先得了解一些基礎概念,如阻塞和非阻塞、同步和異步、多進程和協程。

阻塞

阻塞狀態指程序未得到所需計算資源時被掛起的狀態。程序在等待某個操作完成期間,自身無法繼續處理其他的事情,則稱該程序在該操作上是阻塞的。

常見的阻塞形式有:網絡 I/O 阻塞、磁盤 I/O 阻塞、用戶輸入阻塞等。阻塞是無處不在的,包括 CPU 切換上下文時,所有的進程都無法真正處理事情,它們也會被阻塞。如果是多核 CPU 則正在執行上下文切換操作的核不可被利用。

非阻塞

程序在等待某操作過程中,自身不被阻塞,可以繼續處理其他的事情,則稱該程序在該操作上是非阻塞的。

非阻塞并不是在任何程序級別、任何情況下都可以存在的。僅當程序封裝的級別可以囊括獨立的子程序單元時,它才可能存在非阻塞狀態。

非阻塞的存在是因為阻塞存在,正因為某個操作阻塞導致的耗時與效率低下,我們才要把它變成非阻塞的。

同步

不同程序單元為了完成某個任務,在執行過程中需靠某種通信方式以協調一致,我們稱這些程序單元是同步執行的。

例如購物系統中更新商品庫存,需要用“行鎖”作為通信信號,讓不同的更新請求強制排隊順序執行,那更新庫存的操作是同步的。

簡言之,同步意味著有序。

異步

為完成某個任務,不同程序單元之間過程中無需通信協調,也能完成任務的方式,不相關的程序單元之間可以是異步的。

例如,爬蟲下載網頁。調度程序調用下載程序后,即可調度其他任務,而無需與該下載任務保持通信以協調行為。不同網頁的下載、保存等操作都是無關的,也無需相互通知協調。這些異步操作的完成時刻并不確定。

簡言之,異步意味著無序。

多進程

多進程就是利用 CPU 的多核優勢,在同一時間并行地執行多個任務,可以大大提高執行效率。

協程

協程,英文叫作 Coroutine,又稱微線程、纖程,協程是一種用戶態的輕量級線程。

協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此協程能保留上一次調用時的狀態,即所有局部狀態的一個特定組合,每次過程重入時,就相當于進入上一次調用的狀態。

協程本質上是個單進程,協程相對于多進程來說,無需線程上下文切換的開銷,無需原子操作鎖定及同步的開銷,編程模型也非常簡單。

我們可以使用協程來實現異步操作,比如在網絡爬蟲場景下,我們發出一個請求之后,需要等待一定的時間才能得到響應,但其實在這個等待過程中,程序可以干許多其他的事情,等到響應得到之后才切換回來繼續處理,這樣可以充分利用 CPU 和其他資源,這就是協程的優勢。

協程用法

接下來,我們來了解下協程的實現,從 Python 3.4 開始,Python 中加入了協程的概念,但這個版本的協程還是以生成器對象為基礎的,在 Python 3.5 則增加了 async/await,使得協程的實現更加方便。

Python 中使用協程最常用的庫莫過于 asyncio,所以本文會以 asyncio 為基礎來介紹協程的使用。

首先我們需要了解下面幾個概念。

  • event_loop:事件循環,相當于一個無限循環,我們可以把一些函數注冊到這個事件循環上,當滿足條件發生的時候,就會調用對應的處理方法。
  • coroutine:中文翻譯叫協程,在 Python 中常指代為協程對象類型,我們可以將協程對象注冊到時間循環中,它會被事件循環調用。我們可以使用 async 關鍵字來定義一個方法,這個方法在調用時不會立即被執行,而是返回一個協程對象。
  • task:任務,它是對協程對象的進一步封裝,包含了任務的各個狀態。
  • future:代表將來執行或沒有執行的任務的結果,實際上和 task 沒有本質區別。

另外我們還需要了解 async/await 關鍵字,它是從 Python 3.5 才出現的,專門用于定義協程。其中,async 定義一個協程,await 用來掛起阻塞方法的執行。

定義協程

首先我們來定義一個協程,體驗一下它和普通進程在實現上的不同之處,代碼如下:

import asyncio async def execute(x):print('Number:', x) coroutine = execute(1) print('Coroutine:', coroutine) print('After calling execute') loop = asyncio.get_event_loop() loop.run_until_complete(coroutine) print('After calling loop')

運行結果:

Coroutine: <coroutine object execute at 0x1034cf830> After calling execute Number: 1 After calling loop

首先我們引入了 asyncio 這個包,這樣我們才可以使用 async 和 await,然后我們使用 async 定義了一個 execute 方法,方法接收一個數字參數,方法執行之后會打印這個數字。

隨后我們直接調用了這個方法,然而這個方法并沒有執行,而是返回了一個 coroutine 協程對象。隨后我們使用 get_event_loop 方法創建了一個事件循環 loop,并調用了 loop 對象的 run_until_complete 方法將協程注冊到事件循環 loop 中,然后啟動。最后我們才看到了 execute 方法打印了輸出結果。

可見,async 定義的方法就會變成一個無法直接執行的 coroutine 對象,必須將其注冊到事件循環中才可以執行。

上面我們還提到了 task,它是對 coroutine 對象的進一步封裝,它里面相比 coroutine 對象多了運行狀態,比如 running、finished 等,我們可以用這些狀態來獲取協程對象的執行情況。

在上面的例子中,當我們將 coroutine 對象傳遞給 run_until_complete 方法的時候,實際上它進行了一個操作就是將 coroutine 封裝成了 task 對象,我們也可以顯式地進行聲明,如下所示:

import asyncio async def execute(x):print('Number:', x)return x coroutine = execute(1) print('Coroutine:', coroutine) print('After calling execute') loop = asyncio.get_event_loop() task = loop.create_task(coroutine) print('Task:', task) loop.run_until_complete(task) print('Task:', task) print('After calling loop')

運行結果:

Coroutine: <coroutine object execute at 0x10e0f7830> After calling execute Task: <Task pending coro=<execute() running at demo.py:4>> Number: 1 Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1> After calling loop

這里我們定義了 loop 對象之后,接著調用了它的 create_task 方法將 coroutine 對象轉化為了 task 對象,隨后我們打印輸出一下,發現它是 pending 狀態。接著我們將 task 對象添加到事件循環中得到執行,隨后我們再打印輸出一下 task 對象,發現它的狀態就變成了 finished,同時還可以看到其 result 變成了 1,也就是我們定義的 execute 方法的返回結果。

另外定義 task 對象還有一種方式,就是直接通過 asyncio 的 ensure_future 方法,返回結果也是 task 對象,這樣的話我們就可以不借助于 loop 來定義,即使我們還沒有聲明 loop 也可以提前定義好 task 對象,寫法如下:

import asyncio async def execute(x):print('Number:', x)return x coroutine = execute(1) print('Coroutine:', coroutine) print('After calling execute') task = asyncio.ensure_future(coroutine) print('Task:', task) loop = asyncio.get_event_loop() loop.run_until_complete(task) print('Task:', task) print('After calling loop')

運行結果:

Coroutine: <coroutine object execute at 0x10aa33830> After calling execute Task: <Task pending coro=<execute() running at demo.py:4>> Number: 1 Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1> After calling loop

發現其運行效果都是一樣的。

綁定回調

另外我們也可以為某個 task 綁定一個回調方法,比如我們來看下面的例子:

import asyncio import requestsasync def request():url = 'https://www.baidu.com'status = requests.get(url)return statusdef callback(task):print('Status:', task.result())coroutine = request() task = asyncio.ensure_future(coroutine) task.add_done_callback(callback) print('Task:', task)loop = asyncio.get_event_loop() loop.run_until_complete(task) print('Task:', task)

在這里我們定義了一個 request 方法,請求了百度,獲取其狀態碼,但是這個方法里面我們沒有任何 print 語句。隨后我們定義了一個 callback 方法,這個方法接收一個參數,是 task 對象,然后調用 print 方法打印了 task 對象的結果。這樣我們就定義好了一個 coroutine 對象和一個回調方法,我們現在希望的效果是,當 coroutine 對象執行完畢之后,就去執行聲明的 callback 方法。

那么它們二者怎樣關聯起來呢?很簡單,只需要調用 add_done_callback 方法即可,我們將 callback 方法傳遞給了封裝好的 task 對象,這樣當 task 執行完畢之后就可以調用 callback 方法了,同時 task 對象還會作為參數傳遞給 callback 方法,調用 task 對象的 result 方法就可以獲取返回結果了。

運行結果:

Task: <Task pending coro=<request() running at demo.py:5> cb=[callback() at demo.py:11]> Status: <Response [200]> Task: <Task finished coro=<request() done, defined at demo.py:5> result=<Response [200]>>

實際上不用回調方法,直接在 task 運行完畢之后也可以直接調用 result 方法獲取結果,如下所示:

import asyncio import requestsasync def request():url = 'https://www.baidu.com'status = requests.get(url)return statuscoroutine = request() task = asyncio.ensure_future(coroutine) print('Task:', task)loop = asyncio.get_event_loop() loop.run_until_complete(task) print('Task:', task) print('Task Result:', task.result())

運行結果是一樣的:

Task: <Task pending coro=<request() running at demo.py:4>> Task: <Task finished coro=<request() done, defined at demo.py:4> result=<Response [200]>> Task Result: <Response [200]>

多任務協程

上面的例子我們只執行了一次請求,如果我們想執行多次請求應該怎么辦呢?我們可以定義一個 task 列表,然后使用 asyncio 的 wait 方法即可執行,看下面的例子:

import asyncio import requestsasync def request():url = 'https://www.baidu.com'status = requests.get(url)return statustasks = [asyncio.ensure_future(request()) for _ in range(5)] print('Tasks:', tasks)loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks))for task in tasks:print('Task Result:', task.result())

這里我們使用一個 for 循環創建了五個 task,組成了一個列表,然后把這個列表首先傳遞給了 asyncio 的 wait() 方法,然后再將其注冊到時間循環中,就可以發起五個任務了。最后我們再將任務的運行結果輸出出來,運行結果如下:

Tasks: [<Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>]Task Result: <Response [200]> Task Result: <Response [200]> Task Result: <Response [200]> Task Result: <Response [200]> Task Result: <Response [200]>

協程實現

前面講了這么多,又是 async,又是 coroutine,又是 task,又是 callback,但似乎并沒有看出協程的優勢啊?反而寫法上更加奇怪和麻煩了,別急,上面的案例只是為后面的使用作鋪墊,接下來我們正式來看下協程在解決 IO 密集型任務上有怎樣的優勢吧!

上面的代碼中,我們用一個網絡請求作為示例,這就是一個耗時等待的操作,因為我們請求網頁之后需要等待頁面響應并返回結果。耗時等待的操作一般都是 IO 操作,比如文件讀取、網絡請求等等。協程對于處理這種操作是有很大優勢的,當遇到需要等待的情況的時候,程序可以暫時掛起,轉而去執行其他的操作,從而避免一直等待一個程序而耗費過多的時間,充分利用資源。

為了表現出協程的優勢,我們還是拿本課時開始介紹的網站 https://static4.scrape.cuiqingcai.com/ 為例來進行演示,因為該網站響應比較慢,所以我們可以通過爬取時間來直觀地感受到爬取速度的提升。

為了讓你更好地理解協程的正確使用方法,這里我們先來看看使用協程時常犯的錯誤,后面再給出正確的例子來對比一下。

首先,我們還是拿之前的 requests 來進行網頁請求,接下來我們再重新使用上面的方法請求一遍:

import asyncio import requests import timestart = time.time()async def request():url = 'https://static4.scrape.cuiqingcai.com/'print('Waiting for', url)response = requests.get(url)print('Get response from', url, 'response', response)tasks = [asyncio.ensure_future(request()) for _ in range(10)] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks))end = time.time() print('Cost time:', end - start)

在這里我們還是創建了 10 個 task,然后將 task 列表傳給 wait 方法并注冊到時間循環中執行。

運行結果如下:

Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Waiting for https://static4.scrape.cuiqingcai.com/ ... Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Cost time: 51.422438859939575

可以發現和正常的請求并沒有什么兩樣,依然還是順次執行的,耗時 51 秒,平均一個請求耗時 5 秒,說好的異步處理呢?

其實,要實現異步處理,我們得先要有掛起的操作,當一個任務需要等待 IO 結果的時候,可以掛起當前任務,轉而去執行其他任務,這樣我們才能充分利用好資源,上面方法都是一本正經的串行走下來,連個掛起都沒有,怎么可能實現異步?想太多了。

要實現異步,接下來我們需要了解一下 await 的用法,使用 await 可以將耗時等待的操作掛起,讓出控制權。當協程執行的時候遇到 await,時間循環就會將本協程掛起,轉而去執行別的協程,直到其他的協程掛起或執行完畢。

所以,我們可能會將代碼中的 request 方法改成如下的樣子:

async def request():url = 'https://static4.scrape.cuiqingcai.com/'print('Waiting for', url)response = await requests.get(url)print('Get response from', url, 'response', response)

僅僅是在 requests 前面加了一個 await,然而執行以下代碼,會得到如下報錯:

Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ ... Task exception was never retrieved future: <Task finished coro=<request() done, defined at demo.py:8> exception=TypeError("object Response can't be used in 'await' expression")> Traceback (most recent call last):File "demo.py", line 11, in requestresponse = await requests.get(url) TypeError: object Response can't be used in 'await' expression

這次它遇到 await 方法確實掛起了,也等待了,但是最后卻報了這么個錯,這個錯誤的意思是 requests 返回的 Response 對象不能和 await 一起使用,為什么呢?因為根據官方文檔說明,await 后面的對象必須是如下格式之一:

  • A native coroutine object returned from a native coroutine function,一個原生 coroutine 對象。
  • A generator-based coroutine object returned from a function decorated with types.coroutine,一個由 types.coroutine 修飾的生成器,這個生成器可以返回 coroutine 對象。
  • An object with an await method returning an iterator,一個包含 await 方法的對象返回的一個迭代器。

可以參見:https://www.python.org/dev/peps/pep-0492/#await-expression。

requests 返回的 Response 不符合上面任一條件,因此就會報上面的錯誤了。

那么你可能會發現,既然 await 后面可以跟一個 coroutine 對象,那么我用 async 把請求的方法改成 coroutine 對象不就可以了嗎?所以就改寫成如下的樣子:

import asyncio import requests import timestart = time.time()async def get(url):return requests.get(url)async def request():url = 'https://static4.scrape.cuiqingcai.com/'print('Waiting for', url)response = await get(url)print('Get response from', url, 'response', response)tasks = [asyncio.ensure_future(request()) for _ in range(10)] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks))end = time.time() print('Cost time:', end - start)

這里我們將請求頁面的方法獨立出來,并用 async 修飾,這樣就得到了一個 coroutine 對象,我們運行一下看看:

Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Waiting for https://static4.scrape.cuiqingcai.com/ ... Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <Response [200]> Cost time: 51.394437756259273

還是不行,它還不是異步執行,也就是說我們僅僅將涉及 IO 操作的代碼封裝到 async 修飾的方法里面是不可行的!我們必須要使用支持異步操作的請求方式才可以實現真正的異步,所以這里就需要 aiohttp 派上用場了。

使用 aiohttp

aiohttp 是一個支持異步請求的庫,利用它和 asyncio 配合我們可以非常方便地實現異步請求操作。

安裝方式如下:

pip3 install aiohttp

官方文檔鏈接為:https://aiohttp.readthedocs.io/,它分為兩部分,一部分是 Client,一部分是 Server,詳細的內容可以參考官方文檔。

下面我們將 aiohttp 用上來,將代碼改成如下樣子:

import asyncio import aiohttp import timestart = time.time()async def get(url):session = aiohttp.ClientSession()response = await session.get(url)await response.text()await session.close()return responseasync def request():url = 'https://static4.scrape.cuiqingcai.com/'print('Waiting for', url)response = await get(url)print('Get response from', url, 'response', response)tasks = [asyncio.ensure_future(request()) for _ in range(10)] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks))end = time.time() print('Cost time:', end - start)

在這里我們將請求庫由 requests 改成了 aiohttp,通過 aiohttp 的 ClientSession 類的 get 方法進行請求,結果如下:

Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Waiting for https://static4.scrape.cuiqingcai.com/ Get response from https://static4.scrape.cuiqingcai.com/ response <ClientResponse(https://static4.scrape.cuiqingcai.com/) [200 OK]> <CIMultiDictProxy('Server': 'nginx/1.17.8', 'Date': 'Tue, 31 Mar 2020 09:35:43 GMT', 'Content-Type': 'text/html; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'SAMEORIGIN', 'Strict-Transport-Security': 'max-age=15724800; includeSubDomains', 'Content-Encoding': 'gzip')> ... Get response from https://static4.scrape.cuiqingcai.com/ response <ClientResponse(https://static4.scrape.cuiqingcai.com/) [200 OK]> <CIMultiDictProxy('Server': 'nginx/1.17.8', 'Date': 'Tue, 31 Mar 2020 09:35:44 GMT', 'Content-Type': 'text/html; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'X-Frame-Options': 'SAMEORIGIN', 'Strict-Transport-Security': 'max-age=15724800; includeSubDomains', 'Content-Encoding': 'gzip')> Cost time: 6.1102519035339355

成功了!我們發現這次請求的耗時由 51 秒變直接成了 6 秒,耗費時間減少了非常非常多。

代碼里面我們使用了 await,后面跟了 get 方法,在執行這 10 個協程的時候,如果遇到了 await,那么就會將當前協程掛起,轉而去執行其他的協程,直到其他的協程也掛起或執行完畢,再進行下一個協程的執行。

開始運行時,時間循環會運行第一個 task,針對第一個 task 來說,當執行到第一個 await 跟著的 get 方法時,它被掛起,但這個 get 方法第一步的執行是非阻塞的,掛起之后立馬被喚醒,所以立即又進入執行,創建了 ClientSession 對象,接著遇到了第二個 await,調用了 session.get 請求方法,然后就被掛起了,由于請求需要耗時很久,所以一直沒有被喚醒。

當第一個 task 被掛起了,那接下來該怎么辦呢?事件循環會尋找當前未被掛起的協程繼續執行,于是就轉而執行第二個 task 了,也是一樣的流程操作,直到執行了第十個 task 的 session.get 方法之后,全部的 task 都被掛起了。所有 task 都已經處于掛起狀態,怎么辦?只好等待了。5 秒之后,幾個請求幾乎同時都有了響應,然后幾個 task 也被喚醒接著執行,輸出請求結果,最后總耗時,6 秒!

怎么樣?這就是異步操作的便捷之處,當遇到阻塞式操作時,任務被掛起,程序接著去執行其他的任務,而不是傻傻地等待,這樣可以充分利用 CPU 時間,而不必把時間浪費在等待 IO 上。

你可能會說,既然這樣的話,在上面的例子中,在發出網絡請求后,既然接下來的 5 秒都是在等待的,在 5 秒之內,CPU 可以處理的 task 數量遠不止這些,那么豈不是我們放 10 個、20 個、50 個、100 個、1000 個 task 一起執行,最后得到所有結果的耗時不都是差不多的嗎?因為這幾個任務被掛起后都是一起等待的。

理論來說確實是這樣的,不過有個前提,那就是服務器在同一時刻接受無限次請求都能保證正常返回結果,也就是服務器無限抗壓,另外還要忽略 IO 傳輸時延,確實可以做到無限 task 一起執行且在預想時間內得到結果。但由于不同服務器處理的實現機制不同,可能某些服務器并不能承受這么高的并發,因此響應速度也會減慢。

在這里我們以百度為例,來測試下并發數量為 1、3、5、10、…、500 的情況下的耗時情況,代碼如下:

import asyncio import aiohttp import timedef test(number):start = time.time()async def get(url):session = aiohttp.ClientSession()response = await session.get(url)await response.text()await session.close()return responseasync def request():url = 'https://www.baidu.com/'await get(url)tasks = [asyncio.ensure_future(request()) for _ in range(number)]loop = asyncio.get_event_loop()loop.run_until_complete(asyncio.wait(tasks))end = time.time()print('Number:', number, 'Cost time:', end - start)for number in [1, 3, 5, 10, 15, 30, 50, 75, 100, 200, 500]:test(number)

運行結果如下:

Number: 1 Cost time: 0.05885505676269531 Number: 3 Cost time: 0.05773782730102539 Number: 5 Cost time: 0.05768704414367676 Number: 10 Cost time: 0.15174412727355957 Number: 15 Cost time: 0.09603095054626465 Number: 30 Cost time: 0.17843103408813477 Number: 50 Cost time: 0.3741800785064697 Number: 75 Cost time: 0.2894289493560791 Number: 100 Cost time: 0.6185381412506104 Number: 200 Cost time: 1.0894129276275635 Number: 500 Cost time: 1.8213098049163818

可以看到,即使我們增加了并發數量,但在服務器能承受高并發的前提下,其爬取速度幾乎不太受影響。

綜上所述,使用了異步請求之后,我們幾乎可以在相同的時間內實現成百上千倍次的網絡請求,把這個運用在爬蟲中,速度提升是非常可觀的。

總結

以上便是 Python 中協程的基本原理和用法,在后面一課時會詳細介紹 aiohttp 的使用和爬取實戰,實現快速高并發的爬取。

本節代碼:https://github.com/Python3WebSpider/AsyncTest。

總結

以上是生活随笔為你收集整理的第16讲:异步爬虫的原理和解析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。