1件あたり3秒かかる取得を1000件やると、単純計算で3000秒、1時間近くかかります。通信の待ち時間がほとんどなので、待っている間に別の要求を出せば大幅に短縮できます。を使えば、この形が書けます。
ただし、書き方より先に決めるべきは同時実行数の上限です。ここを決めずに「全部同時に」と書くと、1000本の要求が一斉に飛びます。相手のサーバが耐えられなければ落ちますし、耐えたとしても遮断されます。速く取ることより、取り続けられることのほうが大事です。
同時実行数をどう決めるか
配信元がを明示しているなら、それが答えです。「1秒あたり10回」なら、同時実行数ではなく1秒あたりの発行回数で制御します。同時実行数と発行回数は別物で、混同すると上限を超えます。
明示が無い場合は、控えめな数から始めます。2〜4並列で問題が出ないことを確かめ、必要なら少しずつ上げる。「何並列まで大丈夫か」は相手にしか分からないので、こちらから踏み込む理由はありません。
並行させる道具としては が使いやすいです。requests とほぼ同じ書き味のまま非同期に対応しているので、既存のコードから移りやすい。接続を使い回す仕組みも入っているので、1件ごとに接続を張り直す無駄も避けられます。
なお、並行度を上げると の 429 だけでなく、接続そのものが拒否されることもあります。相手の前段にある仕組みが、同一の接続元からの本数を制限している場合です。この拒否はしても解決しないことが多いので、並行度を下げるのが正しい対処になります。
import asyncio
import httpx
CONCURRENCY = 4 # ここだけを変える
async def fetch_one(client: httpx.AsyncClient, sem: asyncio.Semaphore, url: str): async with sem: # 同時に走る数をここで抑える resp = await client.get(url, timeout=httpx.Timeout(5.0, read=30.0)) resp.raise_for_status() return resp.json()
async def fetch_many(urls: list[str]) -> list: sem = asyncio.Semaphore(CONCURRENCY) headers = {"User-Agent": "hukuhuku-data-fetch/1.0 (+https://hukuhuku.co.jp)"} async with httpx.AsyncClient(headers=headers) as client: tasks = [fetch_one(client, sem, u) for u in urls] # 1件の失敗で全部を落とさない return await asyncio.gather(*tasks, return_exceptions=True)付けないと、1件が失敗した時点で全体が例外で止まります。999件が成功していても結果は受け取れません。付けておけば、成功したものは受け取り、失敗したものだけを後で拾い直せます。
失敗を仕分ける
return_exceptions=True を付けると、結果の中に例外が混ざります。成功と失敗を仕分けて、失敗した対象だけを再取得できる形にしておいてください。仕分けないまま次の処理へ流すと、例外オブジェクトをデータとして扱うことになります。
def split_results(urls: list[str], results: list) -> tuple[list, list]: ok, failed = [], [] for url, r in zip(urls, results): if isinstance(r, Exception): failed.append((url, f"{type(r).__name__}: {r}")) else: ok.append((url, r)) return ok, failed
# 使う側# ok, failed = split_results(urls, await fetch_many(urls))# print(f"成功 {len(ok)} / 失敗 {len(failed)}")# for url, err in failed[:5]:# print(f" {url}: {err}")並行にしても速くならない場合
並行化が効くのは、待ち時間が支配的な処理です。通信を待っている間に別の要求を出せるから速くなります。逆に、受け取ったデータの解析に時間がかかっている場合、並行にしても速くなりません。asyncio は計算を速くする仕組みではないからです。
どちらが支配的かは、1件あたりの時間を通信と処理に分けて測れば分かります。通信が9割なら並行化の効果は大きく、処理が9割ならほとんど変わりません。測らずに並行化して、複雑になっただけで終わるのはよくある失敗です。
並行化はコードを確実に読みにくくします。失敗の扱い、順序の保証、途中で止めたときの後始末。どれも直列なら考えずに済んでいたことです。得られる時間短縮が、その複雑さに見合うかを先に見積もってください。1時間が5分になるなら価値がありますが、10分が8分になる程度なら、直列のままのほうがよいこともあります。
相手の状態を見ながら加減する
固定した同時実行数で回すより、相手の反応を見て加減するほうが安全です。429 や 5xx が出始めたら並行度を下げ、しばらく安定していたら少し戻す。相手が苦しくなったら自分から引くという作りです。
そこまで作り込まなくても、EP.03 のと組み合わせるだけで、事故の多くは防げます。並行度は控えめに固定し、失敗したら待ってやり直す。この2つで足ります。次回は、取ったものの保存形式の話をします。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。