HTTPXとasyncioを利用したPythonの非同期HTTPリクエスト

# HTTPXとasyncioを利用したPythonの非同期HTTPリクエスト
この記事は[JSL(日本システム技研) Advent Calendar 2021](https://qiita.com/advent-calendar/2021/jsl)の3日目の記事です！

# やりたいこと

今年のQiitaアドベントカレンダーのスポンサーされているTwilioさんのとても良い記事をお見かけしました。
asyncioで非同期にPokemonのAPIを呼び出し並行処理を実現するとても良い記事です！ぜひご一読ください！！

https://www.twilio.com/blog/asynchronous-http-requests-in-python-with-aiohttp-jp

上記の記事ではaiohttpを利用していたので、推しのasyncio対応HTTPクライアントであるHTTPXで書き換えたくなりました。そして生まれたのが本記事です。
HTTPX 1.0リリースまであと少し？！ということでよろしくお願いします。

https://twitter.com/starletdreaming/status/1437709389784309760?s=21

# HTTPX

HTTPXは、asyncio対応のHTTPクライアントです。

https://www.python-httpx.org

PythonのHTTPクライアントであるrequestsにインスパイアされており、使い方がよく似ています。  多機能なaiohttpと比べ、よりシンプルなクライアントです。  

また、HTTPXはEncodeの管理するOSSです。
EncodeはDjango REST FrameworkやUvicorn、Starletteを管理しているイギリスの会社です。（UK limited company）

他にも様々なOSSを公開していますので、興味のある方はぜひ覗いてみてください。

https://www.encode.io/projects/

# 動作環境

* MacOS 
* Python 3.10.0
* HTTPX 0.21.1

## 事前準備

HTTPXをpipでインストールしてください。

```bash
pip install httpx
```

# asyncioによる非同期リクエストで大切なこと

asyncioは外部IOの待ち時間でタスクを切り替えます。
大切なのは、外部のIOを行う処理がasyncioに対応していることです。
asyncioを利用した非同期処理を実装する場合、標準ライブラリであるurllib.requestやrequestsでなく、aiothttpやHTTPXを利用する必要があります。

今回の例ではHTTPアクセスですが、データベースアクセスでも同様です。
postgresの場合、psycpg2ではなく、asyncpgや今年の夏asyncioに対応したベータがリリースされたpsycpg3を利用する必要があります。

## HTTPXを利用する 

`httpx.AsyncClient()` がasyncio対応のクライアントを返すコンテキストマネージャーです。with文で使います。
アクセスする先は元記事同様、PokemonのAPIです。ケロマツが好きです。

```python
import asyncio
from time import time
import httpx

URL = "https://pokeapi.co/api/v2/pokemon/"


async def access_url_poke(client, num: int) -> str:
    r = await client.get(f"{URL}{num}")
    pokemon = r.json()  # JSONパース
    return pokemon["name"]  # ポケ名を抜く


async def main_poke():
    """httpxでポケモン151匹引っこ抜く"""
    start = time()
    async with httpx.AsyncClient() as client:
        tasks = [access_url_poke(client, number) for number in range(1, 151)]
        result = await asyncio.gather(*tasks, return_exceptions=False)
    print(result)
    print("time: ", time() - start)

asyncio.run(main_poke())
```

## 実行してみる
上記のコードを実行してみましょう。処理が1秒で終わっていることがわかります。

```bash
py3.10.0 ❯ python poke.py
['bulbasaur', 'ivysaur', 'venusaur', 'charmander', 'charmeleon', 'charizard', 'squirtle', 'wartortle', 'blastoise', 'caterpie', 'metapod', 'butterfree', 'weedle', 'kakuna', 'beedrill', 'pidgey', 'pidgeotto', 'pidgeot', 'rattata', 'raticate', 'spearow', 'fearow', 'ekans', 'arbok', 'pikachu', 'raichu', 'sandshrew', 'sandslash', 'nidoran-f', 'nidorina', 'nidoqueen', 'nidoran-m', 'nidorino', 'nidoking', 'clefairy', 'clefable', 'vulpix', 'ninetales', 'jigglypuff', 'wigglytuff', 'zubat', 'golbat', 'oddish', 'gloom', 'vileplume', 'paras', 'parasect', 'venonat', 'venomoth', 'diglett', 'dugtrio', 'meowth', 'persian', 'psyduck', 'golduck', 'mankey', 'primeape', 'growlithe', 'arcanine', 'poliwag', 'poliwhirl', 'poliwrath', 'abra', 'kadabra', 'alakazam', 'machop', 'machoke', 'machamp', 'bellsprout', 'weepinbell', 'victreebel', 'tentacool', 'tentacruel', 'geodude', 'graveler', 'golem', 'ponyta', 'rapidash', 'slowpoke', 'slowbro', 'magnemite', 'magneton', 'farfetchd', 'doduo', 'dodrio', 'seel', 'dewgong', 'grimer', 'muk', 'shellder', 'cloyster', 'gastly', 'haunter', 'gengar', 'onix', 'drowzee', 'hypno', 'krabby', 'kingler', 'voltorb', 'electrode', 'exeggcute', 'exeggutor', 'cubone', 'marowak', 'hitmonlee', 'hitmonchan', 'lickitung', 'koffing', 'weezing', 'rhyhorn', 'rhydon', 'chansey', 'tangela', 'kangaskhan', 'horsea', 'seadra', 'goldeen', 'seaking', 'staryu', 'starmie', 'mr-mime', 'scyther', 'jynx', 'electabuzz', 'magmar', 'pinsir', 'tauros', 'magikarp', 'gyarados', 'lapras', 'ditto', 'eevee', 'vaporeon', 'jolteon', 'flareon', 'porygon', 'omanyte', 'omastar', 'kabuto', 'kabutops', 'aerodactyl', 'snorlax', 'articuno', 'zapdos', 'moltres', 'dratini', 'dragonair', 'dragonite', 'mewtwo']
time:  1.0884361267089844
```

結果を見てみると、`bulbasaur`となっています。はて・・・🤔🤔
初代ポケモンの名前を覚えている世代ではないため、分かりません。（どの世代のポケモンもほぼ分かりません）

一番最後を見てみると`mewtwo`となっていることから、想定どおり初代ポケモン全ての名前を取得できてはいるようです。

## 結果を問い合わせる

そう、ポケモンの名前が全部英語になっていますね。このままではどれがだれなのかわかりません。
APIの仕様を確認しても、日本語名はないようです。
取得した結果をもとに日本語名を問い合わせるようにしてみましょう。

ポケモンの日本語名を返すAPIを探してみましたが、どうやらすぐには見当たりません。
少し調べるとJSONを作ってくださっている方がいらっしゃいました。

https://qiita.com/PonDad/items/210cd6aed647e7d04450

[PonDad/pokemon.json](https://gist.github.com/PonDad/93922f63c3143489e30c3716d3d176d2)

こいつをデータベースに突っ込んで、問い合わせるようにしてみましょう。

なぜデータベース？と思った方もいらっしゃるかもしれません。
それは、asyncioのためです（非同期IO）。
asyncioはI/Oバウンドな処理で効果を発揮するため、データベースから引っ張ります（英語名→日本語名だけならJSONを直接みてももちろん良いです）。

# 次回へ続く！
また来週！