※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。
Pythonとyfinanceで株価データの自動取得を実装し、VPS上で24時間稼働させている方も増えてきました。
しかし、短時間に大量のリクエストを送信すると、Yahoo Finance側からアクセス制限(HTTPステータスコード429:Too Many Requests)を受け、データが一切取得できなくなるケースが発生します。
一度制限を受けると、数時間〜数日間にわたってIPアドレス単位でブロックされる可能性があり、その間はプログラムが完全に停止します。
問題は、yfinanceの公式ドキュメントにはリクエスト制限の具体的な閾値が明記されていない点です。「何回までなら安全か」が分からないまま運用している方が大半であり、ある日突然データが取得できなくなって初めて問題に気づくパターンが非常に多いです。
この記事では、yfinanceのAPI制限の仕組みを解説し、429エラーを回避しながら長期間安定してデータを取得し続けるための安全な設計パターンとコードを提供します。
すべてコピペでそのまま動作する実装に限定していますので、自分の運用環境に差し替えて応用してください。
API制限(レートリミット)の仕組みと429エラーの正体
429エラーを回避するには、まず「なぜ制限がかかるのか」を正しく理解する必要があります。
yfinanceとYahoo Financeの関係
yfinanceはYahoo Financeの非公式Pythonラッパーです。
Yahoo Financeが公開しているWebページやエンドポイントに対してHTTPリクエストを送信し、レスポンスをパースしてデータを返す仕組みになっています。
* 非公式ライブラリである:Yahoo Financeが公式に提供しているAPIではなく、利用規約上グレーゾーンの手法です
* レート制限の閾値は非公開である:1分間に何回までリクエスト可能かは公式に明示されていません
* 制限はIPアドレス単位で適用される:同一IPから短時間に大量リクエストを送ると、そのIPがブロックされます
429エラーが発生する典型的なパターン
以下のようなコードは、429エラーを引き起こすリスクが高いです。
* forループで数十〜数百銘柄を間隔なしに連続取得する
* 1銘柄に対して複数の期間(1年・5年・10年など)を連続で取得する
* cronやscheduleで毎分リクエストを送信し続ける
429エラーを受けた後もリトライを繰り返すと、制限期間がさらに延長される可能性があります。エラーを検知したら即座にリクエストを停止し、一定時間待機してから再開する設計が必須です。
【コピペOK】安全なデータ取得の設計パターン
429エラーを回避するための設計原則は3つです。
リクエスト間隔を空ける、リトライ機構を組み込む、エラーをログに記録する。
この3つを満たすコードを提供します。
事前準備:ライブラリのインストール
pip install yfinance pandas
【コピペOK】レート制限対応の安全なデータ取得コード
import yfinance as yf
import pandas as pd
import time
import logging
from datetime import datetime
# ==============================
# 設定エリア
# ==============================
SYMBOLS = [
"7203.T", # トヨタ自動車
"6758.T", # ソニーグループ
"9984.T", # ソフトバンクグループ
"8306.T", # 三菱UFJフィナンシャル・グループ
"6861.T", # キーエンス
]
PERIOD = "1y" # 取得期間
SLEEP_BETWEEN_REQUESTS = 2.0 # リクエスト間隔(秒)
MAX_RETRIES = 3 # 最大リトライ回数
RETRY_BASE_WAIT = 10 # リトライ時の基本待機秒数
OUTPUT_DIR = "." # CSV保存先ディレクトリ
# ==============================
# ログ設定
# ==============================
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("fetch_log.txt", encoding="utf-8"),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
# ==============================
# 単一銘柄のデータ取得(リトライ機構付き)
# ==============================
def fetch_single(symbol: str, period: str, max_retries: int = 3) -> pd.DataFrame:
'"1銘柄のデータをリトライ付きで安全に取得します。'"
for attempt in range(1, max_retries + 1):
try:
logger.info(f"{symbol} のデータを取得中(試行 {attempt}/{max_retries})")
ticker = yf.Ticker(symbol)
df = ticker.history(period=period)
if df.empty:
logger.warning(f"{symbol} のデータが空でした。")
return pd.DataFrame()
logger.info(f"{symbol} の取得完了({len(df)}行)")
return df
except Exception as e:
wait_time = RETRY_BASE_WAIT * attempt # 指数的に待機時間を増やす
logger.error(
f"{symbol} の取得に失敗(試行 {attempt}/{max_retries}): {e}"
)
if attempt < max_retries:
logger.info(f"{wait_time}秒待機してリトライします...")
time.sleep(wait_time)
else:
logger.error(f"{symbol} の取得を断念しました。")
return pd.DataFrame()
return pd.DataFrame()
# ==============================
# 複数銘柄の一括取得(間隔制御付き)
# ==============================
def fetch_multiple(
symbols: list,
period: str,
sleep_sec: float = 2.0,
) -> dict:
'"複数銘柄のデータをリクエスト間隔を空けて安全に取得します。'"
results = {}
total = len(symbols)
for i, symbol in enumerate(symbols, start=1):
logger.info(f"--- [{i}/{total}] {symbol} ---")
df = fetch_single(symbol, period, max_retries=MAX_RETRIES)
results[symbol] = df
# 最後の銘柄以外はリクエスト間隔を空ける
if i < total:
logger.info(f"{sleep_sec}秒待機します...")
time.sleep(sleep_sec)
return results
# ==============================
# 取得結果の保存
# ==============================
def save_results(results: dict, output_dir: str):
'"取得したデータをCSVファイルとして保存します。'"
for symbol, df in results.items():
if df.empty:
logger.warning(f"{symbol} はデータが空のためスキップします。")
continue
filename = f"{output_dir}/{symbol.replace('.', '_')}.csv"
df.to_csv(filename)
logger.info(f"✔ {symbol} を保存しました: {filename}")
# ==============================
# 取得結果のサマリー表示
# ==============================
def print_summary(results: dict):
'"取得結果の成功・失敗をサマリー表示します。'"
success = [s for s, df in results.items() if not df.empty]
failed = [s for s, df in results.items() if df.empty]
logger.info(f"n=== 取得結果サマリー ===")
logger.info(f"成功: {len(success)}銘柄 {success}")
logger.info(f"失敗: {len(failed)}銘柄 {failed}")
# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
start_time = datetime.now()
logger.info(f"=== データ取得を開始 ({start_time.strftime('%Y-%m-%d %H:%M:%S')}) ===n")
# 1. 複数銘柄のデータ取得
results = fetch_multiple(SYMBOLS, PERIOD, sleep_sec=SLEEP_BETWEEN_REQUESTS)
# 2. CSV保存
save_results(results, OUTPUT_DIR)
# 3. サマリー表示
print_summary(results)
elapsed = (datetime.now() - start_time).total_seconds()
logger.info(f"n=== 完了(所要時間: {elapsed:.1f}秒) ===")
コードの処理フロー解説
上記のコードは、以下の5ステップで構成されています。
* ログ設定:ファイルとコンソールの両方にログを出力し、エラー発生時に原因を追跡できるようにします
* 単一銘柄取得:1銘柄ずつリトライ機構付きでデータを取得します。失敗時は待機時間を段階的に増やす「リニアバックオフ」方式です
* 複数銘柄一括取得:銘柄間に SLEEP_BETWEEN_REQUESTS 秒の間隔を空けてリクエストします
* CSV保存:取得に成功したデータのみCSVファイルとして保存します
* サマリー表示:成功・失敗の銘柄一覧をログに出力し、取得漏れを把握できるようにします
SYMBOLS リストの銘柄を差し替えるだけで、任意のポートフォリオに対応できます。
安全な設計のための3つの原則
コードを書いて終わりではなく、長期間安定して運用するための設計原則を押さえておく必要があります。
原則1:リクエスト間隔は最低2秒以上空ける
yfinanceのレート制限の閾値は非公開ですが、実務上の目安として以下の値を推奨します。
* 銘柄間の間隔:2〜3秒が安全圏です
* 10銘柄以上を取得する場合:3〜5秒に延ばしてください
* 50銘柄以上を取得する場合:5秒以上を推奨します。さらにバッチ分割(10銘柄ごとに30秒休憩等)を検討してください
「速く取得したい」という欲求は自然ですが、1度でもBAN(アクセス遮断)を受けると数時間〜数日間データが取得できなくなります。数秒の間隔を惜しんで数日間の停止を招くのは完全に割に合いません。
原則2:リトライにはバックオフを必ず組み込む
エラー発生時に即座にリトライすると、サーバー側の負荷をさらに高め、制限が強化されるリスクがあります。
リトライ時は待機時間を段階的に増やす「バックオフ」方式を必ず採用してください。
* 1回目のリトライ:10秒待機
* 2回目のリトライ:20秒待機
* 3回目のリトライ:30秒待機(これで失敗したら取得を断念する)
本記事のコードでは RETRY_BASE_WAIT * attempt のリニアバックオフを採用しています。より安全にしたい場合は RETRY_BASE_WAIT * (2 ** attempt) の指数バックオフに変更してください。
原則3:ログは必ずファイルに残す
VPS上で24時間稼働させる場合、コンソール出力だけではエラーの発生時刻や原因を後から追跡できません。
本記事のコードでは logging モジュールでファイルとコンソールの両方に出力する設定を採用しています。
ログファイルが肥大化するのを防ぐため、長期運用では RotatingFileHandler を使って古いログを自動的にローテーションする仕組みを追加することを推奨します。
【コピペOK】大量銘柄を安全に取得するバッチ分割コード
5〜10銘柄程度であればメインコードで十分ですが、50銘柄以上を取得する場合はバッチ分割が必要です。
リクエストを小さなグループに分け、グループ間に長めの休憩を入れることで429エラーのリスクを大幅に低減できます。
【コピペOK】バッチ分割取得関数
# ==============================
# バッチ分割取得
# ==============================
def fetch_in_batches(
symbols: list,
period: str,
batch_size: int = 10,
sleep_between_requests: float = 3.0,
sleep_between_batches: float = 30.0,
) -> dict:
'"銘柄リストをバッチに分割し、バッチ間に長めの休憩を入れて取得します。'"
all_results = {}
total_batches = (len(symbols) + batch_size - 1) // batch_size
for batch_idx in range(total_batches):
start = batch_idx * batch_size
end = start + batch_size
batch_symbols = symbols[start:end]
logger.info(
f"n=== バッチ {batch_idx + 1}/{total_batches} "
f"({len(batch_symbols)}銘柄) ==="
)
batch_results = fetch_multiple(
batch_symbols, period, sleep_sec=sleep_between_requests
)
all_results.update(batch_results)
# 最後のバッチ以外はバッチ間休憩を入れる
if batch_idx < total_batches - 1:
logger.info(f"バッチ間休憩: {sleep_between_batches}秒待機します...")
time.sleep(sleep_between_batches)
return all_results
この関数では、デフォルトで10銘柄ごとにバッチを区切り、バッチ間に30秒の休憩を入れます。
50銘柄を取得する場合、5バッチ × 30秒休憩 = 約2分の追加待機が発生しますが、429エラーで数時間停止するリスクと比較すれば十分に許容できるコストです。
# バッチ分割取得を呼び出す例
large_symbol_list = ["7203.T", "6758.T", "9984.T", ...] # 50銘柄以上
results = fetch_in_batches(
large_symbol_list,
period="1y",
batch_size=10,
sleep_between_requests=3.0,
sleep_between_batches=30.0,
)
save_results(results, OUTPUT_DIR)
print_summary(results)
よくあるエラーと対処法
yfinanceのAPI制限周りで初心者がつまずきやすいポイントを整理します。
HTTPError: 429 Too Many Requests が発生する
短時間に大量のリクエストを送信したことで、Yahoo Finance側にアクセスを制限されています。
以下を試してください。
* 即座にすべてのリクエストを停止する:リトライを繰り返すと制限期間が延長される可能性があります
* 最低30分〜1時間待機してから再実行する:制限解除のタイミングは非公開ですが、多くの場合30分〜数時間で解除されます
* リクエスト間隔を見直す:SLEEP_BETWEEN_REQUESTS を3〜5秒に延ばし、バッチ分割も導入してください
データが空のDataFrameで返される(エラーメッセージなし)
yfinanceはHTTP 429を受けた際に、例外を投げずに空のDataFrameを返す場合があります。
これはyfinanceの内部でエラーが握りつぶされているケースであり、実質的にはレート制限を受けている状態です。
df.empty チェックを必ず入れ、空の場合はリクエスト間隔を広げてリトライする設計にしてください。本記事のコードでは fetch_single() 関数内で空チェックとログ出力を実装しています。
VPSで長期間動かしていたら突然取得できなくなった
VPSのIPアドレスは固定であるため、長期間にわたって同一IPから定期的にリクエストを送信し続けると、通常よりも制限を受けやすくなる傾向があります。
以下を試してください。
* 取得頻度を見直す:「毎分取得」から「15分ごと」や「1時間ごと」に頻度を下げることで大幅にリスクが低減します
* 取得銘柄数を必要最小限に絞る:監視対象を厳選し、不要な銘柄のリクエストを削除してください
* yfinanceのキャッシュ機能を活用する:yf.Ticker() はセッション内でキャッシュが効くため、同一銘柄の重複リクエストを避ける設計にしてください
まとめ
この記事では、yfinanceのAPI制限(429エラー)の仕組みと、長期間安定してデータを取得し続けるための安全な設計パターンを解説しました。
要点を整理します。
* yfinanceは非公式ライブラリであり、レート制限の閾値は非公開のため、保守的な間隔設定(最低2秒以上)を徹底してください
* リトライ時は必ずバックオフ(段階的な待機時間延長)を組み込み、即座のリトライは避けてください
* 50銘柄以上を取得する場合はバッチ分割(10銘柄ごとに30秒休憩等)を導入してください
* エラーやデータ取得状況はログファイルに記録し、後から原因追跡できるようにしてください
* 429エラーを受けた場合は即座にリクエストを停止し、最低30分以上待機してから再開してください
次のステップとして、本記事の安全なデータ取得コードをベースに、取得したデータをテクニカル指標の算出やバックテストのパイプラインに接続する仕組みを構築してみてください。
fetch_multiple() の戻り値をそのままpandasのDataFrameとして後続処理に渡すだけで、安全なデータ取得と分析処理をシームレスにつなげることができます。

