aiohttp

【Python】aiohttpを用いた非同期HTTP通信の基本

【Python】aiohttpを用いた非同期HTTP通信の基本

Pythonで、aiohttpパッケージを用いて非同期HTTP通信をする方法を解説します。

aiohttpによる非同期HTTP通信

非同期処理とは、複数のタスクが協調しあって処理を実行するプログラミングの実装方法です。非同期プログラミングは多くの支持を得ており、Python 3.5でもasyncawaitといった非同期プログラミング方法が導入されています。

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で、今回紹介するコードは参考にしていただけるかと思います。

また、以降ではasyncawaitといったキーワードや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

上記プログラムは、指定した複数の緯度・経度に対して現在の天気情報を非同期で取得します。主要なライブラリとしてasyncioaiohttpをインポートして使用します。

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の使い方を理解していただき、効率的な通信処理を実装できるようになってもらいたいと思います。