淺度測評:requests、aiohttp、httpx 我應該用哪一個?

2020-12-22 Andrew說Python爬蟲

在武漢,房子裡待著,不出去影響世界了,轉載點文章。

在 Python 眾多的 HTTP 客戶端中,最有名的莫過於requests、aiohttp和httpx。在不藉助其他第三方庫的情況下,requests只能發送同步請求;aiohttp只能發送異步請求;httpx既能發送同步請求,又能發送異步請求。

所謂的同步請求,是指在單進程單線程的代碼中,發起一次請求後,在收到返回結果之前,不能發起下一次請求。所謂異步請求,是指在單進程單線程的代碼中,發起一次請求後,在等待網站返回結果的時間裡,可以繼續發送更多請求。

今天我們來一個淺度測評,僅僅以多次發送 POST 請求這個角度來對比這三個庫的性能。

測試使用的 HTTP 服務地址為http://122.51.39.219:8000/query,向它發送 POST 請求的格式如下圖所示:

請求發送的 ts 欄位日期距離今天大於10天,那麼返回{"success": false},如果小於等於10天,那麼返回{"success": true}。

首先我們通過各個客戶端使用相同的參數只發送一次請求,看看效果。

發送一次請求

requests

import requestsresp = requests.post('http://122.51.39.219:8000/query', json={'ts': '2020-01-20 13:14:15'}).json()print(resp)運行效果如下圖所示:

httpx

使用 httpx 發送同步請求:

import httpxresp = httpx.post('http://122.51.39.219:8000/query', json={'ts': '2020-01-20 13:14:15'}).json()print(resp)httpx 的同步模式與 requests 代碼重合度99%,只需要把requests改成httpx即可正常運行。如下圖所示:

使用 httpx 發送異步請求:

import httpximport asyncioasyncdefmain():asyncwith httpx.AsyncClient() as client: resp = await client.post('http://122.51.39.219:8000/query', json={'ts': '2020-01-20 13:14:15'}) result = resp.json() print(result)asyncio.run(main())運行效果如下圖所示:

aiohttp

import aiohttpimport asyncioasyncdefmain():asyncwith aiohttp.ClientSession() as client: resp = await client.post('http://122.51.39.219:8000/query', json={'ts': '2020-01-20 13:14:15'}) result = await resp.json() print(result)asyncio.run(main())運行效果如下圖所示:

aiohttp 的代碼與 httpx 異步模式的代碼重合度90%,只不過把AsyncClient換成了ClientSession,另外,在使用 httpx 時,當你await client.post時就已經發送了請求。但是當使用aiohttp時,只有在awiat resp.json() 時才會真正發送請求。

發送100次請求

我們現在隨機生成一個距離今天在5-15天的日期,發送到 HTTP接口中。如果日期距離今天超過10天,那麼返回的數據的 False,如果小於等於10天,那麼返回的數據是 True。

我們發送100次請求,計算總共耗時。

requests

在前幾天的文章中,我們提到,使用requests.post每次都會創建新的連接,速度較慢。而如果首先初始化一個 Session,那麼 requests 會保持連接,從而大大提高請求速度。所以在這次測評中,我們分別對兩種情況進行測試。

不保持連接

import randomimport timeimport datetimeimport requestsdefmake_request(body): resp = requests.post('http://122.51.39.219:8000/query', json=body) result = resp.json() print(result)defmain(): start = time.time()for _ in range(100): now = datetime.datetime.now() delta = random.randint(5, 15) ts = (now - datetime.timedelta(days=delta)).strftime('%Y-%m-%d %H:%M:%S') make_request({'ts': ts}) end = time.time() print(f'發送100次請求,耗時:{end - start}')if __name__ == '__main__': main()運行效果如下圖所示:

發送100次請求,requests 不保持連接時耗時2.7秒

保持連接

對代碼稍作修改,使用同一個 Session 發送請求:

import randomimport timeimport datetimeimport requestsdefmake_request(session, body): resp = session.post('http://122.51.39.219:8000/query', json=body) result = resp.json() print(result)defmain(): session = requests.Session() start = time.time()for _ in range(100): now = datetime.datetime.now() delta = random.randint(5, 15) ts = (now - datetime.timedelta(days=delta)).strftime('%Y-%m-%d %H:%M:%S') make_request(session, {'ts': ts}) end = time.time() print(f'發送100次請求,耗時:{end - start}')if __name__ == '__main__': main()運行效果如下圖所示:

發送100次請求,requests 保持連接耗時1.4秒

httpx

同步模式

代碼如下:

import randomimport timeimport datetimeimport httpxdefmake_request(client, body): resp = client.post('http://122.51.39.219:8000/query', json=body) result = resp.json() print(result)defmain(): session = httpx.Client() start = time.time()for _ in range(100): now = datetime.datetime.now() delta = random.randint(5, 15) ts = (now - datetime.timedelta(days=delta)).strftime('%Y-%m-%d %H:%M:%S') make_request(session, {'ts': ts}) end = time.time() print(f'發送100次請求,耗時:{end - start}')if __name__ == '__main__': main()運行效果如下圖所示:

發送100次請求,httpx 同步模式耗時1.5秒左右。

異步模式

代碼如下:

import httpximport randomimport datetimeimport asyncioimport timeasyncdefrequest(client, body): resp = await client.post('http://122.51.39.219:8000/query', json=body) result = resp.json() print(result)asyncdefmain():asyncwith httpx.AsyncClient() as client: start = time.time() task_list = []for _ in range(100): now = datetime.datetime.now() delta = random.randint(5, 15) ts = (now - datetime.timedelta(days=delta)).strftime('%Y-%m-%d %H:%M:%S') req = request(client, {'ts': ts}) task = asyncio.create_task(req) task_list.append(task)await asyncio.gather(*task_list) end = time.time() print(f'發送100次請求,耗時:{end - start}')asyncio.run(main())運行效果如下圖所示:

發送100次請求,httpx 異步模式耗時0.6秒左右。

aiohttp

測試代碼如下:

import aiohttpimport randomimport datetimeimport asyncioimport timeasyncdefrequest(client, body): resp = await client.post('http://122.51.39.219:8000/query', json=body) result = await resp.json() print(result)asyncdefmain():asyncwith aiohttp.ClientSession() as client: start = time.time() task_list = []for _ in range(100): now = datetime.datetime.now() delta = random.randint(5, 15) ts = (now - datetime.timedelta(days=delta)).strftime('%Y-%m-%d %H:%M:%S') req = request(client, {'ts': ts}) task = asyncio.create_task(req) task_list.append(task)await asyncio.gather(*task_list) end = time.time() print(f'發送100次請求,耗時:{end - start}')asyncio.run(main())運行效果如下圖所示:

發送100次請求,使用 aiohttp 耗時0.3秒左右

發送1000次請求

由於 request 保持連接的速度比不保持連接快,所以我們這裡只用保持連接的方式來測試。並且不列印返回的結果。

requests

運行效果如下圖所示:

發送1000次請求,requests 耗時16秒左右

httpx

同步模式

運行效果如下圖所示:

發送1000次請求,httpx 同步模式耗時18秒左右

異步模式

運行效果如下圖所示:

發送1000次請求,httpx 異步模式耗時5秒左右

aiohttp

運行效果如下圖所示:

發送1000次請求,aiohttp 耗時4秒左右

總結

如果你只發幾條請求。那麼使用 requests 或者 httpx 的同步模式,代碼最簡單。

如果你要發送很多請求,但是有些地方要發送同步請求,有些地方要發送異步請求,那麼使用 httpx 最省事。

如果你要發送很多請求,並且越快越好,那麼使用 aiohttp 最快。

這篇測評文章只是一個非常淺度的評測,只考慮了請求速度這一個角度。如果你要在生產環境使用,那麼你可以做更多實驗來看是不是符合你的實際使用情況。

相關焦點

  • python爬蟲篇二:HTTP協議六大方法
    我們看,其中有一個「home/news/data/」,是所有爬蟲都禁止的,即該目錄保存著重要文件,對外不可見。我們可以使用python寫一個程序去嘗試獲取一下該目錄的內容,結果是無法獲取。這也向我們說明:爬蟲不是萬能的,如果文件不向外界展示,我們根本無法爬取。
  • HTTP協議之狀態碼詳解
    HTTP狀態碼,我都是現查現用。 我以前記得幾個常用的狀態碼,比如200,302,304,404, 503。 一般來說我也只需要了解這些常用的狀態碼就可以了。 如果是做AJAX,REST,網絡爬蟲,機器人等程序。還是需要了解其他狀態碼。 本文我花了一個多月的時間把所有的狀態碼都總結了下,內容太多,看的時候麻煩耐心點了。
  • Go中的HTTP請求之——HTTP1.1請求流程分析
    希望讀者讀完此文後, 能夠有以下幾個收穫:對http1.1的請求流程有一個大概的了解在平時的開發中能夠更好地復用底層TCP連接對http1.1的線頭阻塞能有一個更清楚的認識HTTP1.1流程今天內容較多, 廢話不多說, 直接上乾貨。
  • python代碼福利:用requests爬取B站視頻封面
    最近看到一篇文章介紹了利用Python爬蟲爬取B站視頻封面的文章,雖然我完全沒看文章,但是只看了一眼這個封面圖就徹底把我吸引了。不過我也對爬蟲這方面比較熟悉了,這麼簡單的事情還用看別人的文章教我做事?當然是自己動手豐衣足食啦!
  • 如何對手機http進行抓包?Fiddler工具超好用
    在做手機或移動端APP的接口測試時,需要從開發人員那裡獲取接口文檔,接口文檔應該包括完整的功能接口、接口請求方式、接口請求URL、接口請求參數、接口返回參數。如果當前項目沒有接口文檔,則可以使用fiddler對APP進行抓包確認。在手機上對APP進行操作,然後在Fiddler中可以抓取對應的網絡交互信息(一個功能中可能設計多個接口的交互)。
  • 從一個HTTP請求來讀懂HTTP、TCP協議
    http://www.dumain.com 服務端只認ip地址,瀏覽器將域名解析出來,看下瀏覽器裡有沒有域名對應DNS的緩存,有的話直接拿到服務端的ip地址,沒有的話去本地的host文件看有沒有配置,沒有配置的話才會發起一個DNS請求用來獲取伺服器ip地址。
  • Linux使用epoll異步發送http請求
    它與上一行的\r\n正好構成連續的2個回車換行,這可以用來判斷http頭的結束和正文的開始。把這行字符串發送到伺服器,然後讀取返回結果,就是伺服器的應答。應答可能是個html文件,也可以是其他文件,或者一個flv的動態視頻流(直播一般使用http-flv)。
  • Apache HTTP Server 2.4.43 發布 修復安全漏洞
    受影響版本為 Apache HTTP Server 2.4.0 至 2.4.41mod_SSL:修復 OCSP stapling 響應的內存洩漏問題詳情查看 https://downloads.apache.org/httpd/CHANGES_2.4.43下載地址:http://httpd.apache.org/download.cgiApache HTTP Server
  • 一次完整的http請求詳解
    Http請求的一次詳解:(1) 客戶端輸入URL(2) 客戶端檢測緩存:有緩存且較新,客戶端直接讀取本地緩存進行資源展示有緩存但是不新,準備http請求包,發送至服務端進行緩存校驗備註:http1.0中Expire、http1.1中是Cache-Control根據發起http請求: 請求報文包含:
  • 2020.10.26 更新一套Http格式直播源,低調速用
    .m3u8無線新聞,http://bbdd.228888888.xyz:8081/hls/ch201.m3u8有線新聞,http://bbdd.228888888.xyz:8081/hls/ch109.m3u8NOW新聞,http://bbdd.228888888.xyz:8081/hls/ch114.m3u8VIUTV,http://bbdd
  • Go發起HTTP2.0請求流程分析(前篇)
    1、初始化一個http2ClientConn:上面的源碼新建了一個默認的http2ClientConn。initialWindowSize:初始化窗口大小為65535,這個值之後會初始化每一個數據流可發送的數據窗口大小。
  • 有了HTTP,為什麼還要RPC?
    很長時間以來都沒有怎麼好好搞清楚 RPC(即 Remote Procedure Call,遠程過程調用)和 HTTP 調用的區別,不都是寫一個服務然後在客戶端調用麼?這裡請允許我迷之一笑~Naive!
  • Jmeter之HTTP請求與響應
    HTTP請求詳解一個http請求指從客戶端到服務端的請求信息,我們可以通過瀏覽器的F12鍵,可以看到以下信息:1.請求地址:uri>3.HTTP協議/版本:可以打開瀏覽器按下F12仔細查看4.請求頭5.請求參數HTTP響應詳解一個
  • requests-html:最簡單的爬蟲框架,看完你就會了
    pip install requests-html開始使用requests-html用起來也十分簡單,下面是一個簡單例子。照例說明一下,第一段引入了HTMLSession用於創建連接,獲取網頁數據。第二段創建連接,獲取了我的簡書用戶頁面。第三段用xpath語法獲取了網頁上的用戶名,最後列印出來。
  • HTTP3 為什麼比 HTTP2 靠譜?| 技術頭條
    作者 | 浪裡行舟責編 | 郭芮HTTP/2 相比於 HTTP/1,可以說是大幅度提高了網頁的性能,只需要升級到該協議就可以減少很多之前需要做的性能優化工作,當然兼容問題以及如何優雅降級應該是國內還不普遍使用的原因之一。
  • Linux使用epoll控制多個socket發送http請求
    它判斷是否為寫事件,如果是則用getsockopt()讀取錯誤碼,然後填充http請求的緩衝區。最後,設置狀態為連接完成CONNECTED,繼續監控寫事件EPOLLOUT,等待發送數據。一個文件描述符fd在第一次加入epoll時用EPOLL_CTL_ADD,第二次用EPOLL_CTL_MOD修改要監控的事件。因為接下來要發送http請求,這裡還是EPOLLOUT事件。
  • 你還在為 HTTP 的這些概念頭疼嗎?
    1.1 之前版本的歷史遺留欄位,僅作為與 http 的向後兼容而定義。HTTP / 1.1標準還建議出於兼容性目的,支持此內容編碼的伺服器應將 x-gzip 識別為別名。 identity:使用身份功能(即無壓縮或修改)。