ネットワークリクエストにおけるハングアップ問題
WebスクレイピングやAPI連携の実装において、リクエストを送信したままレスポンスがいつまでも返ってこず、プログラムが停止(フリーズ)してしまう現象は珍しくありません。ネットワークの不安定さや、サーバー側の過負荷などが原因で発生しますが、これに対する適切な対策を講じないと、クローラー全体の実行効率が著しく低下します。
PythonのRequestsライブラリでは、timeoutパラメータを活用することで、この問題に効率的に対処できます。本記事では、タイムアウト設定の具体的な手法とその仕組みについて解説します。
1. 基本的なタイムアウト設定と効果
特定のサーバーに対してリクエストを送り、接続が確立できない場合に無限に待ち続けるのを防ぐため、秒単位でタイムアウトを指定します。まずは、タイムアウトを設定した場合と設定しない場合の動作の違いを、コードで確認してみましょう。
import requests
import time
target_site = "https://www.google.com/" # アクセスしにくい環境を想定
custom_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
# 1. タイムアウト設定なし(デフォルト)
try:
print("タイムアウト設定なしでリクエスト開始...")
start_ts = time.perf_counter()
res = requests.get(target_site, headers=custom_headers)
print(f"ステータスコード: {res.status_code}")
except Exception as e:
duration = time.perf_counter() - start_ts
print(f"エラー発生までの時間: {duration:.2f} 秒")
print(f"例外内容: {e}")
# 2. タイムアウト設定あり(5秒)
try:
print("\nタイムアウトを5秒に設定してリクエスト開始...")
start_ts = time.perf_counter()
res = requests.get(target_site, headers=custom_headers, timeout=5)
print(f"ステータスコード: {res.status_code}")
except requests.exceptions.Timeout as e:
duration = time.perf_counter() - start_ts
print(f"タイムアウトにより停止。経過時間: {duration:.2f} 秒")
except Exception as e:
print(f"その他のエラー: {e}")
ここで注意が必要なのは、timeout=5 と指定した場合、これは「接続(Connect)」と「読み取り(Read)」のそれぞれに対して最大5秒の猶予を与えることを意味します。理論上、最大で10秒程度待機する可能性があるという点に留意してください。
2. 接続と読み取りを個別に制御する
より細かな制御が必要な場合、timeoutパラメータにタプルを渡すことができます。タプルの1番目の要素が「接続タイムアウト」、2番目が「読み取りタイムアウト」を指します。
import requests
# (接続タイムアウト, 読み取りタイムアウト)
# サーバーとの疎通に3.5秒、データの受信に10秒の猶予を与える
try:
response = requests.get(
"https://api.example.com/data",
timeout=(3.5, 10)
)
print(f"受信成功: {response.status_code}")
except requests.exceptions.ConnectTimeout:
print("サーバーへの接続自体がタイムアウトしました。")
except requests.exceptions.ReadTimeout:
print("接続はできましたが、データの受信中にタイムアウトしました。")
このように分割して設定することで、ネットワークインフラの問題(接続失敗)と、アプリケーション側の処理遅延(読み取り失敗)を切り分けて処理することが可能になります。
3. 無期限の待機(timeout=None)
特定のケース、例えばレスポンスに非常に時間がかかることが分かっている社内バッチ処理や、極端にレスポンスが遅いサーバー(例:混雑時の特定の海外サイトなど)を相手にする場合、タイムアウトを無効にしたいことがあります。その場合は、明示的に None を指定します。
import requests
# サーバーがレスポンスを返すまで永遠に待ち続ける
try:
response = requests.get(
"https://github.com/",
timeout=None
)
print("レスポンス取得完了")
except Exception as e:
print(f"エラー: {e}")
ただし、プロダクション環境のクローラーで timeout=None を使用すると、プロセスが永久に残り続けてしまうリスクがあるため、使用には十分な注意が必要です。原則として、たとえ長い時間であっても具体的な数値を設定することを推奨します。