続・urllib2 で timeout を捕まえる

6/26 追記: Python2.7.3 について追試

urllib2 を使って Web API を呼び出す場合、タイムアウトが気になりますよね。
urllib2 では urllib.urlopen() の引数に timeout を指定することができます。

import urllib2

urllib2.urlopen('http://www.yahoo.co.jp/', timeout=1)

それでは、タイムアウトが発生したことはどのように得られるのでしょうか。
実験してみたところ例外 urllib2.URLError が発生していたので、

import urllib2
import timeout

try:
  f = urllib2.urlopen('http://www.yahoo.co.jp/', timeout=1)
  content = f.read()
except urllib2.URLError, exc:
  if len(exc.args) > 0 and isinstance(exc.args[0], socket.timeout):
    print "Caught timeout!"

のようにエラーを取得することにしました。

しかし、@r_rudi さんの環境では urllib2.URLError ではなく socket.timeout が返ってくるという指摘がありました。

そこでコードを読みつつ、挙動を確認してみると次のようなことが分かりました。

  • urllib2.urlopen() は HTTPヘッダを受信し終わるまでブロックする
  • f.read() は HTTP ボディを受信し終わるまでブロックする
  • urllib2.urlopen() の中で発生したタイムアウトは urllib2.URLError として例外が発生する

- ただし Python 2.7.3 では通信状態によって例外が変化する

表にまとめると以下のような動きとなります。

HTTPヘッダ送信中(urlopen()中) HTTPヘッダ受信中(urlopen()中) HTTPヘッダ受信後(f.read()中)
Python2.7.1 urllib2.URLError urllib2.URLError socket.timeout
Python2.7.2 urllib2.URLError urllib2.URLError socket.timeout
Python2.7.3 urllib2.URLError socket.timeout socket.timeout

というわけで、これらを踏まえると以下のようなコードでタイムアウトをキャッチすることができるようです。

import urllib2
import timeout

try:
  f = urllib2.urlopen('http://www.yahoo.co.jp/', timeout=1)
  content = f.read()
except urllib2.URLError, exc:
  if len(exc.args) > 0 and isinstance(exc.args[0], socket.timeout):
    print "Caught timeout!"
except socke.timeout, exc:
  print "Caught timeout!"

ちゃんちゃん。