Pythonで、aiohttp
パッケージを用いて非同期HTTP通信をする方法を解説します。
Contents
aiohttpによる非同期HTTP通信
非同期処理とは、複数のタスクが協調しあって処理を実行するプログラミングの実装方法です。非同期プログラミングは多くの支持を得ており、Python 3.5でもasync
やawait
といった非同期プログラミング方法が導入されています。
async
/await
を用いたプログラミングの基本は「async/awaitを用いた非同期プログラミングの基本」でまとめているので参考にしてください。
本記事では、非同期処理で効果が期待できる通信処理に焦点を当てます。具体的には、非同期HTTP通信が可能なaiohttp
を使用する方法について紹介します。
aiohttpとは
aiohttp
とは、クライアントとサーバーの両方のユースケースで非同期HTTP通信をサポートしているサードパーティ製ライブラリです。公式サイトはこちらを参照してください。
非同期プログラミングは、I/Oバウンドな処理を実装するためには非常に有益な実装方法です。I/Oバウンドな処理とは、ファイルへの入出力やネットワーク通信等が該当します。
Pythonでネットワーク通信というとrequests
パッケージを思い浮かべる方が多いかと思います。requests
の基本は「requestsの基本的な使い方」にまとめているので参考にしてください。
残念なことにrequests
パッケージは、非同期I/Oに対応していません。aiohttp
は、非同期なhttp通信パッケージとして非常に信頼できるライブラリとして使用することができます。
他に非同期I/Oに対応した人気のHTTPクライアントとしては「HTTPX」があります。クライアント機能に特化して非同期プログラミングをシンプルに行うための機能を提供しています。
aiohttpを用いた非同期通信の基本
この記事では、OpenWeatherのAPIを使ってaiohttp
を用いた非同期通信の実装方法を紹介します。OpenWeatherは、グローバルな気象データを取得できるサービスです。OpenWeatherのAPIをrequestsで使用する方法は「OpenWeatherのAPI使用方法」でまとめているので参考にしてください。なお、OpenWeatherに限らず類似するAPIで、今回紹介するコードは参考にしていただけるかと思います。
また、以降ではasync
やawait
といったキーワードやasyncio
が出てきますが、これらの基本は「async/awaitを用いた非同期プログラミングの基本」を参考にしてください。
aiohttpのインストール
aiohttp
はサードパーティ製のライブラリなので、以下でインストールをしてください。
pip install aiohttp
aiohttpの非同期通信
ここから紹介するプログラムでは、OpenWeatherの「Current weather data API」を使用し、経度・緯度を用いて現在の天気情報を取得する処理を非同期に行います。Current weather data APIのドキュメントはこちらを参照してください。
OpenWeatherのAPIは、アカウント登録して取得できるAPIキーを使います。APIキーと今回使用するAPIのURLは、コンフィグファイル(config.ini
)に記載して使用することにします。以下の「xxxxxxxxxxxxxxxxxxxxxxxx
」の部分には取得したAPIキーを指定してください。なお、設定ファイルの扱いが分からない方は「configparserによる設定ファイル管理」を参考にしてください。
【設定ファイル】config.ini
[API] key = xxxxxxxxxxxxxxxxxxxxxxxx url_current_weather_data = https://api.openweathermap.org/data/2.5/weather
【実行ファイル】aiohttp_current_weather.py
import asyncio import configparser import time from pprint import pprint import aiohttp async def get_current_weather_data(session, url, api_key, lat, lon, lang): """現在の天気情報 Args: session: セッション情報 url: APIのURL api_key: APIキー lat: 緯度 lon: 経度 lang: 言語 Returns: 天気情報 """ # パラメータの設定 params = { "lat": lat, "lon": lon, "appid": api_key, "lang": lang, } # 非同期通信 async with session.get(url, params=params) as response: if response.status == 200: data = await response.json() return data else: return f"Fail to get current weather for (lat:{lat} lon:{lon})" async def main(): """メインコルーチン""" # APIキーの読み込み config = configparser.ConfigParser() config.read("./config.ini") api_key = config["API"]["key"] url = config["API"]["url_current_weather_data"] locations = [ {"name": "sapporo", "lat": 43.065, "lon": 141.347}, {"name": "tokyo", "lat": 35.689, "lon": 139.692}, {"name": "nagoya", "lat": 35.180, "lon": 136.907}, {"name": "osaka", "lat": 34.686, "lon": 135.520}, {"name": "hiroshima", "lat": 34.397, "lon": 132.460}, {"name": "okinawa", "lat": 26.212, "lon": 127.681}, ] # 非同期セッションを作成 async with aiohttp.ClientSession() as session: # タスクを作成 tasks = [ get_current_weather_data( session, url, api_key, loc["lat"], loc["lon"], "ja" ) for loc in locations ] results = await asyncio.gather(*tasks) for result, loc in zip(results, locations): if isinstance(result, dict): print(f"対象地域: {loc['name']}") pprint(result) print("\n") else: pprint(result) if __name__ == "__main__": # 時間計測の開始 t = time.time() # asyncio.runを使用してメインコルーチンを実行 asyncio.run(main()) # 実行時間を表示 print(f"実行時間: {time.time() - t:.5f} sec")
【実行結果】 対象地域: sapporo {'base': 'stations', 'clouds': {'all': 51}, 'cod': 200, 'coord': {'lat': 43.065, 'lon': 141.347}, 'dt': 1710022443, 'id': 2128295, 'main': {'feels_like': 266.78, 'grnd_level': 1016, 'humidity': 71, 'pressure': 1019, 'sea_level': 1019, 'temp': 269.62, 'temp_max': 270.55, 'temp_min': 268.1}, 'name': '札幌市', 'snow': {'1h': 1.83}, 'sys': {'country': 'JP', 'id': 20766, 'sunrise': 1710017746, 'sunset': 1710059656, 'type': 2}, 'timezone': 32400, 'visibility': 10000, 'weather': [{'description': '雪', 'icon': '13d', 'id': 601, 'main': 'Snow'}], 'wind': {'deg': 291, 'gust': 3.74, 'speed': 1.86}} 対象地域: tokyo {'base': 'stations', 'clouds': {'all': 20}, 'cod': 200, 'coord': {'lat': 35.689, 'lon': 139.692}, 'dt': 1710022443, 'id': 1850144, 'main': {'feels_like': 272.54, 'humidity': 53, 'pressure': 1023, 'temp': 276.65, 'temp_max': 278.46, 'temp_min': 274.6}, 'name': '東京都', 'sys': {'country': 'JP', 'id': 268395, 'sunrise': 1710017958, 'sunset': 1710060239, 'type': 2}, 'timezone': 32400, 'visibility': 10000, 'weather': [{'description': '薄い雲', 'icon': '02d', 'id': 801, 'main': 'Clouds'}], 'wind': {'deg': 360, 'speed': 5.14}} 対象地域: nagoya {'base': 'stations', 'clouds': {'all': 3}, 'cod': 200, 'coord': {'lat': 35.18, 'lon': 136.907}, 'dt': 1710022625, 'id': 1856057, 'main': {'feels_like': 275.14, 'humidity': 74, 'pressure': 1023, 'temp': 277.55, 'temp_max': 278.02, 'temp_min': 277.55}, 'name': '名古屋市', 'sys': {'country': 'JP', 'id': 2001167, 'sunrise': 1710018614, 'sunset': 1710060919, 'type': 2}, 'timezone': 32400, 'visibility': 10000, 'weather': [{'description': '晴天', 'icon': '01d', 'id': 800, 'main': 'Clear'}], 'wind': {'deg': 341, 'gust': 6.42, 'speed': 2.74}} 対象地域: osaka {'base': 'stations', 'clouds': {'all': 20}, 'cod': 200, 'coord': {'lat': 34.686, 'lon': 135.52}, 'dt': 1710022624, 'id': 1853904, 'main': {'feels_like': 271.04, 'humidity': 73, 'pressure': 1027, 'temp': 273.51, 'temp_max': 278.01, 'temp_min': 272.03}, 'name': '大阪府', 'sys': {'country': 'JP', 'id': 8032, 'sunrise': 1710018936, 'sunset': 1710061263, 'type': 1}, 'timezone': 32400, 'visibility': 10000, 'weather': [{'description': '薄い雲', 'icon': '02d', 'id': 801, 'main': 'Clouds'}], 'wind': {'deg': 320, 'speed': 2.06}} 対象地域: hiroshima {'base': 'stations', 'clouds': {'all': 4}, 'cod': 200, 'coord': {'lat': 34.397, 'lon': 132.46}, 'dt': 1710022625, 'id': 1862415, 'main': {'feels_like': 272.35, 'grnd_level': 1027, 'humidity': 68, 'pressure': 1029, 'sea_level': 1029, 'temp': 274.03, 'temp_max': 274.03, 'temp_min': 274.03}, 'name': '広島市', 'sys': {'country': 'JP', 'id': 8029, 'sunrise': 1710019663, 'sunset': 1710062004, 'type': 1}, 'timezone': 32400, 'visibility': 10000, 'weather': [{'description': '晴天', 'icon': '01d', 'id': 800, 'main': 'Clear'}], 'wind': {'deg': 16, 'gust': 1.76, 'speed': 1.53}} 対象地域: okinawa {'base': 'stations', 'clouds': {'all': 40}, 'cod': 200, 'coord': {'lat': 26.212, 'lon': 127.681}, 'dt': 1710022722, 'id': 1856035, 'main': {'feels_like': 287.24, 'humidity': 65, 'pressure': 1024, 'temp': 288, 'temp_max': 288.1, 'temp_min': 283.3}, 'name': '那覇市', 'sys': {'country': 'JP', 'id': 8137, 'sunrise': 1710020640, 'sunset': 1710063321, 'type': 1}, 'timezone': 32400, 'visibility': 10000, 'weather': [{'description': '雲', 'icon': '03d', 'id': 802, 'main': 'Clouds'}], 'wind': {'deg': 60, 'speed': 3.09}} 実行時間: 0.38422 sec
上記プログラムは、指定した複数の緯度・経度に対して現在の天気情報を非同期で取得します。主要なライブラリとしてasyncio
とaiohttp
をインポートして使用します。
APIを使用する関数としてはget_current_weather_data
関数を定義しています。取得した各種情報はJSON形式で返却されますが、json
メソッドを使用することで辞書形式で扱うことができます。
【main
コルーチン】
プログラムはmain
コルーチンによって制御されます。この中でAPIキーを読み込み、取得対象の場所の緯度・経度情報をlocaitons
という辞書のリストとして定義しています。今回は以下の6か所の天気を取得しています。
locations = [ {"name": "sapporo", "lat": 43.065, "lon": 141.347}, {"name": "tokyo", "lat": 35.689, "lon": 139.692}, {"name": "nagoya", "lat": 35.180, "lon": 136.907}, {"name": "osaka", "lat": 34.686, "lon": 135.520}, {"name": "hiroshima", "lat": 34.397, "lon": 132.460}, {"name": "okinawa", "lat": 26.212, "lon": 127.681}, ]
aiohttp
を使用する場合には、aiohttp.ClientSession
で非同期セッションを作成します。今回は、このセッション(session
)とAPIキー(api_key
)や条件となる緯度・経度の情報を各関数に渡すことで実行します。
# 非同期セッションを作成 async with aiohttp.ClientSession() as session: # タスクを作成 tasks = [ get_current_weather_data( session, url, api_key, loc["lat"], loc["lon"], "ja" ) for loc in locations ] results = await asyncio.gather(*tasks)
タスクとして各地点を設定したタスクを作成し、asyncio.gather
でタスクを渡しています。asyncio.gather
では、全てのタスクの完了を待ち、結果リストを取得することができます。
【get_current_weather_data
関数】
実際にAPIを実行しているget_current_weather_data
関数の主要なポイントを説明します。まず、APIに設定するパラメータを以下のように用意します。
# パラメータの設定 params = { "lat": lat, "lon": lon, "appid": api_key, "lang": lang, }
ここで、緯度(lat
)、経度(lon
)、APIキー(api_key
)、言語(lang
)を指定しています。非同期通信の実行をしているのは以下の部分です。
# 非同期通信 async with session.get(url, params=params) as response: if response.status == 200: data = await response.json() return data else: return f"Fail to get current weather for (lat:{lat} lon:{lon})"
非同期セッションのget
を使用して、引数にAPIのURLと作成したパラメータを渡します。この時、async with
句を使用してレスポンスを取得します。レスポンスのステータスが正常(200
)であったら、jsonメソッドを使用して結果を辞書として取得し、結果を返却します。
レスポンスのステータスがそれ以外の場合は、失敗の文字列を返すようにしています。実際には200以外のステータスに対する処理、タイムアウトの考慮、独自の例外クラスのスローなどを考えるべきですが、今回は簡単のためエラー文字列を返すだけとしています。実際の場面では例外処理を十分検討するようにしてください。
実行結果を見てみると、指定地域の各種天気情報が返却されてきていることが分かるかと思います。
今回の実行は、私の使用した環境で「0.38422秒」で完了しました。これが速いのかどうかは分かりづらいですね。そこで、以降で同期通信で実行した場合も確認することで非同期処理の効果を確認してみましょう。
同期通信との比較 (requestsを使用)
上記まででaiohttp
を使用したOpenWeatherのAPI使用方法を紹介しました。通信処理はI/Oバウンドな処理であるため、同期的な処理に比べて非同期処理は効率的です。では、request
モジュールを使って同期的に実行した場合と具体的に比べてみましょう。
以下は、上記で紹介したプログラムをrequests
を用いて同期版にしたプログラムです。
import configparser import time from pprint import pprint import requests def get_current_weather_data(url, api_key, lat, lon, lang): """現在の天気情報 Args: url: APIのURL api_key: APIキー lat: 緯度 lon: 経度 lang: 言語 Returns: 天気情報 """ # パラメータの設定 params = { "lat": lat, "lon": lon, "appid": api_key, "lang": lang, } # 同期通信 response = requests.get(url, params=params) if response.status_code == 200: data = response.json() return data else: return f"Fail to get current weather for (lat:{lat} lon:{lon})" def main(): """メイン関数""" # APIキーの読み込み config = configparser.ConfigParser() config.read("./config.ini") api_key = config["API"]["key"] url = config["API"]["url_current_weather_data"] locations = [ {"name": "sapporo", "lat": 43.065, "lon": 141.347}, {"name": "tokyo", "lat": 35.689, "lon": 139.692}, {"name": "nagoya", "lat": 35.180, "lon": 136.907}, {"name": "osaka", "lat": 34.686, "lon": 135.520}, {"name": "hiroshima", "lat": 34.397, "lon": 132.460}, {"name": "okinawa", "lat": 26.212, "lon": 127.681}, ] results = [ get_current_weather_data(url, api_key, loc["lat"], loc["lon"], "ja") for loc in locations ] for result, loc in zip(results, locations): if isinstance(result, dict): print(f"対象地域: {loc['name']}") pprint(result) print("\n") else: pprint(result) if __name__ == "__main__": # 時間計測の開始 t = time.time() main() # 実行時間を表示 print(f"実行時間: {time.time() - t:.5f} sec")
【実行結果】 対象地域: sapporo {'base': 'stations', 'clouds': {'all': 75}, 'cod': 200, (...実行結果はほとんど同じなので省略...) 実行時間: 3.47440 sec
実行時間を見てみると「3.47440秒」かかりました。aiohttp
を使用した非同期通信では、「0.38422秒」であったことから、非同期通信で大幅に実行時間が改善されていることが分かります。
上記のように、I/Oバウンドな通信処理では非同期処理は非常に強力です。
まとめ
Pythonで、aiohttp
パッケージを用いて非同期HTTP通信をする方法を解説しました。
aiohttp
とは、クライアントとサーバーの両方のユースケースで非同期HTTP通信をサポートしているサードパーティ製ライブラリです。
この記事では具体例としてOpenWeatherのAPIを使ってaiohttp
を用いた非同期通信の実装方法を紹介しました。また、requests
を使用した同期通信に比べて処理速度が速くなることも確認しています。
非同期プログラミングは、I/Oバウンドな処理を実装するためには非常に有益な実装方法でAPIに対する通信は、効果を見込める代表的な例です。ぜひ、aiohttp
の使い方を理解していただき、効率的な通信処理を実装できるようになってもらいたいと思います。