【マナー】スクレイピングやAPIループ処理に必須の「time.sleep」設定法

自動化・運用

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

Pythonで株価データのスクレイピングやAPI連続呼び出しを実装し、動くコードが書けた段階にいる方は多いはずです。

しかし、待機時間を入れずにループ処理を回すと、サーバーからアクセス遮断(BAN)を受ける危険があります。

「昨日まで動いていたスクリプトが突然エラーを返すようになった」という相談は非常に多いです。

原因のほとんどは、time.sleepを入れていないか、待機秒数の設定が不適切なことにあります。

本記事では、time.sleepの基本的な使い方から、API・スクレイピングにおける適切な秒数設定、BAN回避のための実践的なループ設計までを解説します。

コピペで動くコードを掲載しているので、自分のスクリプトにそのまま組み込んでください。

time.sleepの基本と必要性

time.sleepの仕組み

time.sleepはPython標準ライブラリtimeモジュールに含まれる関数です。

引数に指定した秒数だけ、プログラムの実行を一時停止します。

time.sleep(1.0)と書けば1秒間停止し、time.sleep(0.5)なら0.5秒間停止します。

整数だけでなく浮動小数点数(float)も受け付けるため、ミリ秒単位の細かい制御が可能です。

待機処理を入れないとどうなるか

待機なしでAPIやWebサイトに連続アクセスすると、1秒間に数十〜数百回のリクエストが飛びます。

これはDoS攻撃(Denial of Service:サービス拒否攻撃)と同じパターンとみなされます。

結果として、IPアドレス単位でのBAN、APIキーの無効化、最悪の場合は法的措置の対象になります。

「たかだか数百銘柄のデータ取得」でも、待機なしのループは絶対に避けてください。

【コピペOK】基本のsleep付きループ処理

まずは最もシンプルな、固定秒数の待機を入れたAPI呼び出しループです。


pip install requests

import time
from typing import Final

import requests

# ==============================
# 設定エリア
# ==============================
BASE_URL: Final[str] = "https://query1.finance.yahoo.com/v8/finance/chart/{ticker}"
TICKERS: Final[list[str]] = ["7203.T", "9984.T", "6758.T", "8306.T", "9432.T"]
SLEEP_SECONDS: Final[float] = 1.5
REQUEST_TIMEOUT: Final[int] = 10
MAX_RETRIES: Final[int] = 3
RETRY_WAIT: Final[float] = 5.0


# ==============================
# データ取得(単一銘柄)
# ==============================
def fetch_chart_data(ticker: str) -> dict | None:
    '指定銘柄のチャートデータをJSON形式で取得する'
    url = BASE_URL.format(ticker=ticker)
    params = {"range": "1mo", "interval": "1d"}
    try:
        response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.HTTPError as e:
        print(f"  HTTPエラー: {e}")
        return None
    except requests.exceptions.ConnectionError:
        print(f"  接続エラー: {ticker}")
        return None
    except requests.exceptions.Timeout:
        print(f"  タイムアウト: {ticker}")
        return None


# ==============================
# リトライ付きデータ取得
# ==============================
def fetch_with_retry(ticker: str) -> dict | None:
    '最大MAX_RETRIES回までリトライする'
    for attempt in range(1, MAX_RETRIES + 1):
        print(f"  試行 {attempt}/{MAX_RETRIES}")
        data = fetch_chart_data(ticker)
        if data is not None:
            return data
        if attempt < MAX_RETRIES:
            print(f"  {RETRY_WAIT}秒待機してリトライします...")
            time.sleep(RETRY_WAIT)
    return None


# ==============================
# メインループ(sleep付き)
# ==============================
def run_screening(tickers: list[str]) -> list[dict]:
    '全銘柄を順番に取得し、結果をリストで返す'
    results: list[dict] = []
    total = len(tickers)
    for i, ticker in enumerate(tickers):
        print(f"[{i + 1}/{total}] {ticker} を取得中...")
        data = fetch_with_retry(ticker)
        if data is not None:
            results.append({"ticker": ticker, "data": data})
            print(f"  取得成功")
        else:
            print(f"  取得失敗(スキップ)")

        # ====== 最後の銘柄以外はsleepを入れる ======
        if i < total - 1:
            print(f"  {SLEEP_SECONDS}秒待機中...")
            time.sleep(SLEEP_SECONDS)

    return results


# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    print("スクリーニング開始")
    result_list = run_screening(TICKERS)
    print(f"n完了: {len(result_list)}/{len(TICKERS)} 銘柄取得成功")

コードの処理フロー解説

上記のコードは、以下の4ステップで構成されています。

* ステップ1 設定読み込み:銘柄リスト、待機秒数、リトライ回数を定数として冒頭に定義しています

* ステップ2 単一銘柄取得:requests.getでAPIを叩き、HTTPエラー・接続エラー・タイムアウトをそれぞれ個別にキャッチします

* ステップ3 リトライ制御:取得失敗時にRETRY_WAIT秒待機してから再試行し、最大MAX_RETRIES回まで繰り返します

* ステップ4 メインループ:全銘柄を順番に処理し、各取得の間にSLEEP_SECONDS秒の待機を挟みます

SLEEP_SECONDSの値を変えるだけで、待機時間を一括調整できます。

適切なsleep秒数の決め方

APIの場合のガイドライン

APIを利用する場合、まずそのAPIのレートリミット(Rate Limit:単位時間あたりのリクエスト上限)を公式ドキュメントで確認してください。

具体的な目安は以下のとおりです。

* レートリミットが明記されている場合:制限の70〜80%に収まるようsleep秒数を逆算してください。例えば「1分間に60回」なら1.2〜1.5秒間隔が安全です

* レートリミットが不明な場合:最低1.0秒、推奨1.5〜2.0秒を設定してください

* 無料プランのAPI:2.0〜3.0秒と余裕をもたせるのが無難です

Webスクレイピングの場合のガイドライン

スクレイピング対象のサイトにはrobots.txtが設置されている場合があります。

Crawl-delayディレクティブが指定されていれば、その秒数以上のsleepを必ず入れてください。

指定がない場合でも、最低2.0秒の間隔を空けるのがマナーです。

金融系サイトは特にアクセス制限が厳しいため、3.0〜5.0秒を推奨します。

相手サーバーへの配慮なくスクレイピングを行う行為は、不正アクセス禁止法に抵触する可能性もあります。過信してはいけません。

【コピペOK】ランダム待機とバックオフの発展パターン

固定秒数のsleepはパターンが規則的すぎるため、BOT検知に引っかかる場合があります。

ランダム待機と指数バックオフ(Exponential Backoff)を組み合わせた、より実践的なパターンを紹介します。


import random
import time
from typing import Final

# ==============================
# 設定エリア
# ==============================
SLEEP_MIN: Final[float] = 1.0
SLEEP_MAX: Final[float] = 3.0
BACKOFF_BASE: Final[float] = 2.0
BACKOFF_MAX: Final[float] = 60.0
MAX_RETRIES: Final[int] = 5
JITTER_MAX: Final[float] = 1.0


# ==============================
# ランダム待機
# ==============================
def random_sleep(min_sec: float = SLEEP_MIN, max_sec: float = SLEEP_MAX) -> None:
    'min_secからmax_secの範囲でランダムに待機する'
    wait = random.uniform(min_sec, max_sec)
    print(f"  待機: {wait:.2f}秒")
    time.sleep(wait)


# ==============================
# 指数バックオフ付きリトライ
# ==============================
def fetch_with_backoff(fetch_func, *args) -> dict | None:
    '失敗するたびに待機時間を指数的に増やしてリトライする'
    for attempt in range(MAX_RETRIES):
        result = fetch_func(*args)
        if result is not None:
            return result

        # ====== 指数バックオフ + ジッター ======
        base_wait = min(BACKOFF_BASE ** attempt, BACKOFF_MAX)
        jitter = random.uniform(0, JITTER_MAX)
        wait = base_wait + jitter
        print(f"  リトライ {attempt + 1}/{MAX_RETRIES}: {wait:.2f}秒後に再試行")
        time.sleep(wait)

    print("  最大リトライ回数を超過しました")
    return None


# ==============================
# 使用例
# ==============================
if __name__ == "__main__":
    # ランダム待機の例
    for i in range(3):
        print(f"リクエスト {i + 1}")
        random_sleep()

    # 指数バックオフの例(常にNoneを返すダミー関数でテスト)
    dummy_fetch = lambda: None
    fetch_with_backoff(dummy_fetch)

コードの処理フロー解説

上記のコードは、以下の3ステップで構成されています。

* ステップ1 ランダム待機:random.uniformで指定範囲内の乱数を生成し、毎回異なる秒数で待機します。BOT検知の回避に効果的です

* ステップ2 指数バックオフ:失敗回数に応じて待機時間を2の累乗で増やし、サーバー回復を待ちます。1回目は2秒、2回目は4秒、3回目は8秒と増加します

* ステップ3 ジッター(Jitter:揺らぎ)付与:バックオフにランダムな揺らぎを加えることで、複数クライアントの同時リトライ集中を防ぎます

基本コードのfetch_with_retryをこのfetch_with_backoffに差し替えるだけで、より堅牢なループ処理になります。

よくあるエラーと対処法

HTTPステータス429 Too Many Requests

レートリミット超過を示すHTTPステータスコードです。

サーバー側が「リクエストが多すぎる」と判断した場合に返されます。

以下を試してください。

* SLEEP_SECONDSを現在の2倍に増やしてください

* レスポンスヘッダのRetry-Afterフィールドに推奨待機秒数が記載されている場合があるので、その値をtime.sleepに設定してください

* 指数バックオフを導入し、連続失敗時に自動的に間隔を広げる設計にしてください

time.sleep(0)やマイナス値を指定した場合の挙動

time.sleep(0)はエラーにはなりませんが、実質的に待機なしと同じです。

BAN対策としては機能しないので、意味のある正の値を設定してください。

マイナス値を渡すとValueErrorが発生します。

以下を試してください。

* 設定エリアの定数に対してassert SLEEP_SECONDS > 0のバリデーションを入れてください

* ランダム待機のmin_secが0以下にならないよう、引数のデフォルト値を適切に設定してください

* 外部設定ファイルから読み込む場合は、読み込み直後に型と値の範囲チェックを行ってください

ConnectionError / TimeoutErrorが頻発する

ネットワーク環境の問題か、サーバー側の一時的な障害が原因です。

sleepの問題ではなく、リトライ設計の問題である場合が多いです。

以下を試してください。

* requests.gettimeout引数を10〜30秒に設定し、無限待ちを防いでください

* 指数バックオフ付きリトライを導入し、一時障害からの自動復旧を可能にしてください

* 3回以上連続で失敗した銘柄はスキップし、ループ全体を止めない設計にしてください

まとめ

この記事では、time.sleepの基本的な使い方から、API・スクレイピングにおけるBAN回避のための実践的なループ設計までを解説しました。

要点を整理します。

* time.sleepは引数に秒数を指定して処理を一時停止する、Python標準の関数です

* APIのレートリミットを確認し、制限の70〜80%に収まる間隔設定が安全です

* スクレイピングでは最低2.0秒、金融系サイトでは3.0〜5.0秒の間隔を推奨します

* ランダム待機でBOT検知を回避し、指数バックオフで障害時の自動復旧を実現してください

* 待機なしの連続アクセスはBAN・法的リスクの両面で危険です。必ずsleepを入れてください

次のステップとして、自分の既存スクリプトにrandom_sleep関数とfetch_with_backoff関数を組み込んでみてください。設定エリアの秒数定数を調整するだけで、あらゆるAPI呼び出しやスクレイピング処理に応用できます。

タイトルとURLをコピーしました