※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。
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.getのtimeout引数を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呼び出しやスクレイピング処理に応用できます。
