Python 知名套件 requests 一直是每位 Python 應用開發者在開發 HTTP client 的首選,不過該套件原生並不支援 asyncio, 無法受益於 asyncio 在 I/O 方面高效率的優點,因此如要增加使用 requests 的效率,通常會搭配 multiprocessingthreading 模組,透過平行(concurrent)處理提高同一時間能夠執行 HTTP 要求的數量。

所幸我們仍有 AIOHTTP 可以使用,該套件不僅提供與 requests 套件相同的功能,更原生支援 asyncio, 因此可以在不使用 multiprocessingthreading 模組的情況下,就能夠達到相當高的執行效率,如果想透過 asyncio 提高 HTTP client 的效能,不妨考慮使用 AIOHTTP 吧!

本文將透過實際範例學習如何使用 AIOHTTP 實作 HTTP client, 建議需有 asyncio 的基礎再閱讀本文為佳。

本文環境

$ pip install aiohttp==3.7.4.post0

Hello, AIOHTTP

AIOHTTP 提供 ClientServer 2 種不同的 API 介面,讓我們能夠實作 asyncio 版的 HTTP client 與 HTTP server, 不過使用 AIOHTTP 開發 server 端應用並不常見,建議可以使用 DjangoFastAPI 等框架(framework)開發 server 端應用,因此本文也僅專注於如何透過 AIOHTTP 所提供的 HTTP API 實作 asyncio 版的 HTTP client 。

順帶一提,由於 AIOHTTP 是以 asyncio 實作,因此在使用其所提供的函式/API 時需要注意該函式/API 是 coroutine 還是 async context manager.

如果是 async context manager, 文件就會標註 async-with (如下圖所示),必須以 async with 進行呼叫:

如果是 coroutine, 文件就會標註 coroutine (如下圖所示),必須以 await 進行呼叫:

以下是最簡單的 AIOHTTP 範例,該範例會呼叫 aiohttp.request() 函式對 https://example.com 發出 HTTP GET 的要求,並將該網站的 HTML 原始碼印出:

import asyncio
import aiohttp


async def main():
    async with aiohttp.request('GET', 'https://example.com') as resp:
        assert resp.status == 200
        html = await resp.text()
        print(html)


asyncio.run(main())

上述範例執行結果如下:

$ python hello_aiohttp.py
<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

    }
    div {
        width: 600px;
        margin: 5em auto;
        padding: 2em;
        background-color: #fdfdff;
        border-radius: 0.5em;
        box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
    }
    a:link, a:visited {
        color: #38488f;
        text-decoration: none;
    }
    @media (max-width: 700px) {
        div {
            margin: 0 auto;
            width: auto;
        }
    }
    </style>
</head>

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is for use in illustrative examples in documents. You may use this
    domain in literature without prior coordination or asking for permission.</p>
    <p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

aiohttp.request()

AIOHTTP 最基本的 HTTP client API 為 aiohttp.requests() ,該函式滿足我們執行簡單的 HTTP 要求,requests 也有提供相同的 API 可以使用,只是兩者參數與行為有所不同。

另外,值得注意的是 aiohttp.requests() 並不支援HTTP keep-alive 等功能,因此該函式並不適合執行大量的 HTTP 要求,由於無法重複利用既有連線(connection),導致一直持續建立新的連線,使得執行效率較差,如果要執行大量的 HTTP 要求,建議使用 ClientSession 為佳。

Basic API is good for performing simple HTTP requests without keepaliving, cookies and complex connection stuff like properly configured SSL certification chaining.

以下範例以 httpbin 的 get/set cookies API 進行實驗,實際驗證每個 aiohttp.requests() 不支援官方文件提到的 cookies 互動,即每次呼叫 requests() 都不會載入先前 HTTP 要求中所得到 cookie 設定。

import asyncio
import aiohttp


async def main():
    set_cookies_url = 'http://httpbin.org/cookies/set?freeform=freedom'
    get_cookies_url = 'http://httpbin.org/cookies'

    async with aiohttp.request('GET', set_cookies_url, allow_redirects=False) as resp1:
        print('set cookies =>', resp1.cookies)

    async with aiohttp.request('GET', get_cookies_url) as resp2:
        print('get cookies =>', await resp2.text())


asyncio.run(main())

上述範例執行結果如下:

$ python test.py
set cookies => Set-Cookie: freeform=freedom; Domain=httpbin.org; Path=/
get cookies => {
  "cookies": {}
}

從上述結果可以看到第 1 次執行 aiohttp.request('GET', set_cookies_url, allow_redirects=False) 時,伺服器端(server side)確實回傳 Set-Cookie: freeform=freedom; HTTP header 給我們,讓我們設定一個名稱為 freedom 值也為 freedom 的 cookie, 不過當第 2 次執行 aiohttp.request('GET', get_cookies_url) 時,可以從伺服器端的回應 "cookies": {} 發現 AIOHTTP 沒有自動載入第 1 次 HTTP 要求所得到 cookie, 這與 requests 套件所提供的 requests() 函式行為不一樣。

p.s. requests.request() 的原始碼顯示該 API 使用的是 Session object, 因此會自動使用 keep-alive 以及自動紀錄/載入 cookie 等功能。

Response object

每當 AIOHTTP 發出 HTTP request 之後,會回傳 1 個 Response object ,該 Response object 中包含所有 HTTP 回應相關的內容,例如 HTTP headers, response body 等等,並且支援非同步 context manager, 因此可以搭配 async with 語句使用。

值得注意的是,若搭配 async with 語句,離開 async with 區塊時,context manager 會自動呼叫 release() 方法,該方法會釋放 connection, 因此如果離開 async with 區塊後,要讀取 response 的內容,會導致 Connection closed 錯誤發生,例如以下範例:

When the client fully receives the payload, the underlying connection automatically returns back to pool. If the payload is not fully read, the connection is closed.

import asyncio
import aiohttp


async def main():
    async with aiohttp.request('GET', 'https://example.com') as resp:
        assert resp.status == 200
    html = await resp.text()
    print(html)


asyncio.run(main())

上述範例執行結果如下,可以發現離開 async with 區塊後,我們試圖以 await resp.text() 讀取變數 resp 的內容,導致 Connection closed 錯誤發生。

$ python test.py
Traceback (most recent call last):
  File "test.py", line 12, in <module>
    asyncio.run(main())
  File "/.../lib/python3.8/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/.../lib/python3.8/asyncio/base_events.py", line 608, in run_until_complete
    return future.result()
  File "test.py", line 8, in main
    html = await resp.text()
  File "/.../lib/python3.8/site-packages/aiohttp/client_reqrep.py", line 1076, in text
    await self.read()
  File "/.../lib/python3.8/site-packages/aiohttp/client_reqrep.py", line 1032, in read
    self._body = await self.content.read()
  File "/.../lib/python3.8/site-packages/aiohttp/streams.py", line 344, in read
    raise self._exception
aiohttp.client_exceptions.ClientConnectionError: Connection closed

json()

除了前述幾個範例很常出現的 text() 方法之外,如果 HTTP response 是以 JSON 格式回傳的話,可以呼叫 json() 方法將資料轉為 Python 的 dict, 例如以下範例呼叫 http://httpbin.org/json 試圖取得 1 筆 JSON 格式的資料,並且呼叫 json() 將資料轉為 dict 之後印出:

import asyncio
import aiohttp


async def main():
    url = 'http://httpbin.org/json'
    async with await async with aiohttp.request('GET', url) as resp:
        print(await resp.json())


asyncio.run(main())

上述範例執行結果如下:

$ python test.py
{'slideshow': {'author': 'Yours Truly', 'date': 'date of publication', 'slides': [{'title': 'Wake up to WonderWidgets!', 'type': 'all'}, {'items': ['Why <em>WonderWidgets</em> are great', 'Who <em>buys</em> WonderWidgets'], 'title': 'Overview', 'type': 'all'}], 'title': 'Sample Slide Show'}}

raise_for_status()

Python 知名的 requests 模組有提供 1 個方法,讓開發者能夠在接收到 HTTP 400 以上的狀態碼(status code)時拋出錯誤(HTTPError)。

同樣地, AIOHTTP 也為 Response object 提供一樣的方法可呼叫,例如以下範例以 httpbin 所提供的 API 分別模擬 HTTP 狀態碼 200 與 403:

import asyncio
import aiohttp


async def main():
    url_200 = 'http://httpbin.org/status/200'
    async with session.get(url_200) as resp:
        print('status code', resp.status)
        resp.raise_for_status()

    url_403 = 'http://httpbin.org/status/403'
    async with session.get(url_403) as resp:
        print('status code', resp.status)
        resp.raise_for_status()


asyncio.run(main())

上述範例執行結果如下,可以發現 raise_for_status() 遇到狀態碼 403 時確實如預期般拋出錯誤:

$ python test.py
status code 200
status code 403
Traceback (most recent call last):
  File "test.py", line 17, in <module>
    asyncio.run(main())
  File "/.../lib/python3.8/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/.../lib/python3.8/asyncio/base_events.py", line 608, in run_until_complete
    return future.result()
  File "client_raise_for_status.py", line 15, in main
    resp.raise_for_status()
  File "/.../lib/python3.8/site-packages/aiohttp/client_reqrep.py", line 1000, in raise_for_status
    raise ClientResponseError(
aiohttp.client_exceptions.ClientResponseError: 403, message='FORBIDDEN', url=URL('http://httpbin.org/status/403')

ClientSession

如前述所言 aiohttp.requests() 並不適合執行大量的 HTTP 要求,因其無法重複利用既有連線(connection),導致一直持續建立新的連線,使得執行效率較差,如果要執行大量的 HTTP 要求,建議使用 ClientSession ,該類別不僅會建立 connection pool, 更實作了 keep-alive 等功能,可以有效提升執行效率。

aiohttp.ClientSession 的範例如下,可以看到我們必須建立 1 個 ClientSession 的實例(instance)之後,再透過該實例發出 GET, POST 等 HTTP request:

import asyncio
import aiohttp


async def main():
    url = 'https://example.com'
    async with aiohttp.ClientSession() as session:
        resp = await session.get(url)
        async with resp:
            assert resp.status == 200


asyncio.run(main())

另外值得注意的是,千萬別為每 1 個 HTTP request 各自建立 1 個 ClientSession, 這種用法就跟直接使用 aiohttp.request() 發出 HTTP request 一樣,執行效率較差。

Don’t create a session per request. Most likely you need a session per application which performs all requests altogether.

More complex cases may require a session per site, e.g. one for Github and other one for Facebook APIs. Anyway making a session for every request is a very bad idea.

A session contains a connection pool inside. Connection reusage and keep-alives (both are on by default) may speed up total performance.

此外, ClientSession 與 aiohttp.request() 不同的是, ClientSession 會自動記錄/載入 cookies, 適用於各種需要依賴 cookie 的情境,例如某種需要登入特定頁面後,再進行各項操作流程等情況,以下為展示 ClientSession 會自動記錄/載入 cookie 的範例:

import asyncio
import aiohttp


async def main():
    set_cookies_url = 'http://httpbin.org/cookies/set?freeform=freedom'
    get_cookies_url = 'http://httpbin.org/cookies'

    async with aiohttp.ClientSession() as session:
        async with session.get(set_cookies_url, allow_redirects=False) as resp1:
            print('set cookies =>', resp1.cookies)

        async with session.get(get_cookies_url) as resp2:
            print('get cookies =>', await resp2.text())


asyncio.run(main())

上述範例執行結果如下,從結果可以看到透過 ClientSession GET http://httpbin.org/cookies/set?freeform=freedom 讓 httpbin 伺服器回傳 Set-Cookie 的 header 之後,ClientSession 收到該 header 會紀錄該 cookie 值,因此第 2 次 GET http://httpbin.org/cookies 時, ClientSession 已經自動代入 Set-Cookie 的 cookie 值,因此 GET http://httpbin.org/cookies 會顯示 cookies:

$ python test.py
set cookies => Set-Cookie: freeform=freedom; Domain=httpbin.org; Path=/
get cookies => {
  "cookies": {
    "freeform": "freedom"
  }
}

各式 HTTP request

接著示範各種 HTTP request 的使用方式,例如 GET, POST, PUT, DELETE, PATCH 等。

GET

以下為 HTTP GET 的範例(礙於篇幅將不多做解釋):

import asyncio
import aiohttp


async def main():
    url = 'https://example.com'
    async with aiohttp.ClientSession() as session:
        async with await session.get(url) as resp:
            assert resp.status == 200
            print(await resp.text())


asyncio.run(main())
設定 Headers

發出各式 HTTP request 時,有些時候需要設定 HTTP header, 例如更改 User-Agent, 以偽裝該 HTTP request 為瀏覽器所發出,以下為設定 User-Agent 的範例,該範例傳入 headers={'User-Agent': 'my-user-agent'} 參數至 ClientSession 中,並且呼叫 httpbin 的 headers API 檢查 httpbin 收到哪些 HTTP header:

import asyncio
import aiohttp


async def main():
    url = 'http://httpbin.org/headers'
    async with aiohttp.ClientSession(headers={'User-Agent': 'my-user-agent'}) as session:
        async with await session.get(url) as resp:
            assert resp.status == 200
            print(await resp.json())


asyncio.run(main())

上述範例執行結果如下,可以發現 httpbin 伺服器確實收到 User-Agent 為 my-user-agent 的 HTTP header:

$ python test.py
{'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'my-user-agent', 'X-Amzn-Trace-Id': 'Root=1-613c56ed-1003e816541f368131e325b8'}}

如果想要針對單一 HTTP request 設定 HTTP header 也可以將 headers 改傳入 session.get() 中:

import asyncio
import aiohttp


async def main():
    url = 'http://httpbin.org/headers'
    async with aiohttp.ClientSession() as session:
        async with await session.get(url, headers={'User-Agent': 'my-user-agent'}) as resp:
            assert resp.status == 200
            print(await resp.json())


asyncio.run(main())

p.s. 該設定也適用 POST, DELETE, PATCH 等 HTTP request

設定 Cookies

實務上也有需要特別設定 cookie 的情況,可將需要的 cookie 鍵(key)與值(value)傳入 cookies 參數中,例如以下範例:

import asyncio
import aiohttp


async def main():
    url = 'http://httpbin.org/cookies'
    async with aiohttp.ClientSession(cookies={'name': 'coke'}) as session:
        async with await session.get(url) as resp:
            print(await resp.json())


asyncio.run(main())

上述範例結果如下,可以發現 httpbin 伺服器確實收到我們設定的 cookie:

$ python test.py
{'cookies': {'name': 'coke'}}

p.s. 該設定也適用 POST, DELETE, PATCH 等 HTTP request

HTTP Basic Authentication (HTTP 基本認證)

如果需要進行 HTTP 基本認證(例如上圖),可以使用 aiohttp.BasicAuth 類別設定帳號密碼之後,傳入 auth 參數,例如以下範例:

import asyncio
import aiohttp


async def main():
    url = 'http://httpbin.org/basic-auth/username/passwd'
    auth = aiohttp.BasicAuth('username', password='passwd')
    async with aiohttp.ClientSession(auth=auth) as session:
        async with session.get(url) as resp:
           print('status code', resp.status)


asyncio.run(main())

上述範例執行結果如下,可以看到我們順利通過 HTTP 基本認證得到狀態碼 200, 若帳號密碼錯誤將會得到狀態碼 403:

$ python test.py
status code 200

使用代理伺服器(Proxy)

AIOHTTP 也能透過 proxy 參數設定代理伺服器,例如以下範例:

import asyncio
import aiohttp


async def main():
    url = 'http://ifconfig.me/ip'
    proxy = 'http://121.78.139.44'
    async with aiohttp.request('GET', url, proxy=proxy) as resp:
        assert resp.status == 200
        print(await resp.text())

    # or
    async with aiohttp.ClientSession() as session:
        async with session.get(url, proxy=proxy) as resp:
            assert resp.status == 200
            print(await resp.text())

asyncio.run(main())

p.s. 可以至 https://www.freeproxylists.net/ 挑 1 個代理伺服器做實驗,建議不要在 production 環境使用這些免費的代理伺服器,一是品質不穩定之外,二是可能有資安疑慮

除了以參數方式設定代理伺服器之外,也可以用環境變數方式讓 AIOHTTP 自動使用指定的代理伺服器,例如以下範例:

$ HTTP_PROXY=http://121.78.139.44 python test.py
$ HTTPS_PROXY=https://121.78.139.44 python test.py

POST

另一種常見的 HTTP request 為 POST, 通常用在傳送表單(form)資料、呼叫 API 等。

以下為對 httpbin API 發出 HTTP POST request 的範例,其中 json={'x': 'y'} 為送出 {"x": "y"} JSON 格式的資料:

import asyncio
import aiohttp


async def main():
    url = 'http://httpbin.org/post'
    async with aiohttp.ClientSession() as session:
        async with await session.post(url, json={'x': 'y'}) as resp:
            print(await resp.json())


asyncio.run(main())

上述範例執行結果如下,從結果 'data': '{"x": "y"}' 可以發現 httpbin 伺服器確實收到 {"x": "y"} 的資料,而且 httpbin 伺服器也有收到 HTTP header 'Content-Type': 'application/json' , 代表該資料為 JSON 格式:

$ python test.py
{'args': {}, 'data': '{"x": "y"}', 'files': {}, 'form': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '10', 'Content-Type': 'application/json', 'Host': 'httpbin.org', 'User-Agent': 'Python/3.8 aiohttp/3.7.4.post0', 'X-Amzn-Trace-Id': 'Root=1-613f66d5-4f92d212028ac54907efa3e2'}, 'json': {'x': 'y'}, 'origin': '127.0.0.1', 'url': 'http://httpbin.org/post'}

Form data

如果要送出表單(form)資料,例如常見的登入表單,則可以使用 aiohttp.FormData() 類別,並且代入 data 參數即可,例如以下範例模擬傳送 username, password 的表單資料:

import asyncio
import aiohttp


async def main():
    url = 'http://httpbin.org/post'
    form_data = aiohttp.FormData({'username': 'aaa', 'password': 'bbb'})
    async with aiohttp.ClientSession() as session:
        async with session.post(url, data=form_data) as resp:
            print(await resp.json())


asyncio.run(main())

上述範例執行結果如下,從結果 'form': {'username': 'aaa', 'password': 'bbb'} 可以發現 httpbin 伺服器確實收到表單資料:

{'args': {}, 'data': '', 'files': {}, 'form': {'username': 'aaa', 'password': 'bbb'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '3', 'Content-Type': 'application/x-www-form-urlencoded', 'Host': 'httpbin.org', 'User-Agent': 'Python/3.8 aiohttp/3.7.4.post0', 'X-Amzn-Trace-Id': 'Root=1-613f6786-70a497e27888fe5d246e9419'}, 'json': None, 'origin': '127.0.0.1', 'url': 'http://httpbin.org/post'}

上傳檔案(upload file)

另一種常見的需求為上傳檔案,假設有一網頁的網頁原始碼有以下的片段:

<input type="file" name="fileField">

如果要使用 AIOHTTP 進行檔案上傳的話,同樣可以使用 aiohttp.FormData() 類別進行,只要呼叫該類別的 add_field() 方法,並傳入 file 實例, AIOHTTP 就會幫忙處理檔案的上傳。

以下範例上傳 1 個檔案(example.png)到 httpbin, 該範例 form_data.add_field('fileField', file, filename='example.png')'fileField' 通常對應到網頁原始碼中 <input type="file" name="fileField">name 的值,或者對應 API 所提供的參數,而 filename 參數可以隨意指定,也可以不指定:

import asyncio
import aiohttp


async def main():
    url = 'http://httpbin.org/post'
    form_data = aiohttp.FormData()
    file = open('/path/to/example.png', 'rb')
    form_data.add_field('fileField', file, filename='example.png')
    async with aiohttp.ClientSession() as session:
        async with session.post(url, data=form_data) as resp:
            print(await resp.json())


asyncio.run(main())

上述範例執行結果如下,從結果 'files': {'fileField': 'data:image/png;base64,iVBORw0KG...(略)...'} 可以看到 httpbin 收到我們上傳的資料(該檔案內容已經以 BASE64 進行編碼):

$ python test.py
{'args': {}, 'data': '', 'files': {'fileField': 'data:image/png;base64,iVBORw0KG...(略)...'}, 'form': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '100103', 'Content-Type': 'multipart/form-data; boundary=3f698401808e4c1cbbbf9624c2f4544d', 'Host': 'httpbin.org', 'User-Agent': 'Python/3.8 aiohttp/3.7.4.post0', 'X-Amzn-Trace-Id': 'Root=1-613f6ab7-26621cd2796bcf9779f51719'}, 'json': None, 'origin': '127.0.0.1', 'url': 'http://httpbin.org/post'}

PUT, DELETE, PATCH

如果是針對 RESTful API 所需要的 PUT, DELETE, PATCH requests, 其實只要將前述範例中的 session.get(), session.post() 改成以下形式即可:

session.put()
session.delete()
session.patch()

詳見官方文件

TraceConfig

除了各種 HTTP request 相關的功能之外, AIOHTTP 也為 HTTP request 設計生命週期(life cycle), 該生命週期中有著各式各樣的狀態,每個狀態都有相對應的事件(event)供開發者利用,例如我們可以在 HTTP request 處於轉址(redirect)觸發 on_request_redirect 事件時,利用我們實作的函式監控 URL 的轉換。

而為了在特定狀態觸發我們實作的函式就需要使用 TraceConfig 類別,並且將我們實作的函式附加(append)到相對應的事件(event)上(詳見官方文件)。

例如以下範例實作 3 個函式 on_request_start , on_request_end , on_request_redirect 分別用以監聽 HTTP request 開始、結束以及轉址的情況,接著在第 19 - 21 行將這些函式以 append 方式綁定到相對應的事件上,最後傳入 ClientSession 的 trace_configs 參數中以啟用:

import asyncio
import aiohttp


async def on_request_start(session, trace_config_ctx, params):
    print('Starting request')


async def on_request_end(session, trace_config_ctx, params):
    print('Ending request')


async def on_request_redirect(session, trace_config_ctx, params):
    print('will redirect to', params.response.headers['location'])


async def main():
    trace_config = aiohttp.TraceConfig()
    trace_config.on_request_start.append(on_request_start)
    trace_config.on_request_end.append(on_request_end)
    trace_config.on_request_redirect.append(on_request_redirect)
    async with aiohttp.ClientSession(trace_configs=[trace_config]) as client:
        async with client.get('http://httpbin.org/cookies/set?freeform=freedom') as resp:
            print(await resp.text())


asyncio.run(main())

上述範例執行結果如下,可以發現我們利用 TraceConfig 以及相對應的事件,就達成監聽 HTTP request 開始、結束以及轉址的功能:

$ python test
Starting request
will redirect to /cookies
Ending request
{
  "cookies": {
    "freeform": "freedom"
  }
}

實際上,需要用到 TraceConfig 的機會並不多,但了解 TraceConfig 的用途將能有效幫助除錯(debug)以及從 AIOHTTP 與伺服器互動的過程中挖掘更多需要的資訊(例如可透過追蹤轉址了解 1 個 HTTP request 從開始到結束經過多少伺服器)。

解放威能

前述範例都是以發出單一或者少數 HTTP requests 為例,大多實務上使用 AIOHTTP 多半有著效能方面的要求,以求能在有限時間內處理大量的 HTTP requests, 因此實務上會有類似以下範例的程式碼,將多個 AIOHTTP HTTP request 所回傳的 coroutine 先存到 1 個 list 中之後,再以 asyncio.gather() 或者 asyncio.as_completed 同時(concurrent)執行這些 HTTP requests 並回收結果:

import aiohttp
import asyncio


def do_requests(session):
    return session.get('https://example.com')


async def main():
    tasks = []
    async with aiohttp.ClientSession() as session:
        for _ in range(0, 10):
            tasks.append(do_requests(session))

        results = await asyncio.gather(*tasks)
        for r in results:
            print('example.com =>', r.status)


if __name__ == '__main__':
    asyncio.run(main())

上述範例執行結果如下,可以發現 10 個 HTTP requests 僅花 1.278 秒:

$ time python test.py
example.com => 200
example.com => 200
example.com => 200
example.com => 200
example.com => 200
example.com => 200
example.com => 200
example.com => 200
example.com => 200
example.com => 200
python gather.py  0.34s user 0.12s system 36% cpu 1.278 total

前述範例如果不以 asyncio.gather() 執行,而是直接以 await 1 個接著 1 個執行的話,將會明顯慢上許多。

如有興趣,不妨可以試著執行以下版本,並測試看看執行時間是否較慢:

import aiohttp
import asyncio


def do_requests(session):
    return session.get('https://example.com')


async def main():
    tasks = []
    async with aiohttp.ClientSession() as session:
        for _ in range(0, 10):
            resp = await do_requests(session)
            print('example.com =>', resp.status)


if __name__ == '__main__':
    asyncio.run(main())

結語

至此為 AIOHTTP client 粗略的重點教學,如有興趣不妨進一步翻閱 AIOHTTP 的官方文件, 相信可以對 AIOHTTP 有更深的了解。

以上!

Happy Coding!

References

https://docs.aiohttp.org/en/stable/tracing_reference.html#tracing-reference