ふくふくHukuhuku Inc.
EP.11Data Fetch 10分公開: 2026-09-02

並行して取る ── 速さより先に、相手への礼儀を決める

1件ずつ取ると1000件で1時間かかる。並行にすれば数分で終わりますが、上限を決めずにやると相手を潰します。同時実行数の決め方から書きます。

#Python#asyncio#設計
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

1件あたり3秒かかる取得を1000件やると、単純計算で3000秒、1時間近くかかります。通信の待ち時間がほとんどなので、待っている間に別の要求を出せば大幅に短縮できます。を使えば、この形が書けます。

ただし、書き方より先に決めるべきは同時実行数の上限です。ここを決めずに「全部同時に」と書くと、1000本の要求が一斉に飛びます。相手のサーバが耐えられなければ落ちますし、耐えたとしても遮断されます。速く取ることより、取り続けられることのほうが大事です。

同時実行数をどう決めるか

配信元がを明示しているなら、それが答えです。「1秒あたり10回」なら、同時実行数ではなく1秒あたりの発行回数で制御します。同時実行数と発行回数は別物で、混同すると上限を超えます。

明示が無い場合は、控えめな数から始めます。2〜4並列で問題が出ないことを確かめ、必要なら少しずつ上げる。「何並列まで大丈夫か」は相手にしか分からないので、こちらから踏み込む理由はありません。

並行させる道具としては が使いやすいです。requests とほぼ同じ書き味のまま非同期に対応しているので、既存のコードから移りやすい。接続を使い回す仕組みも入っているので、1件ごとに接続を張り直す無駄も避けられます。

なお、並行度を上げると の 429 だけでなく、接続そのものが拒否されることもあります。相手の前段にある仕組みが、同一の接続元からの本数を制限している場合です。この拒否はしても解決しないことが多いので、並行度を下げるのが正しい対処になります。

同時実行数を明示的に絞る。数を変えるのは1か所だけ
Python
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)
return_exceptions=True を付ける理由

付けないと、1件が失敗した時点で全体が例外で止まります。999件が成功していても結果は受け取れません。付けておけば、成功したものは受け取り、失敗したものだけを後で拾い直せます。

失敗を仕分ける

return_exceptions=True を付けると、結果の中に例外が混ざります。成功と失敗を仕分けて、失敗した対象だけを再取得できる形にしておいてください。仕分けないまま次の処理へ流すと、例外オブジェクトをデータとして扱うことになります。

成功と失敗を分け、失敗した対象だけを次に回す
Python
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 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp