web-dev-qa-db-ja.com

urllib2またはその他のhttpライブラリを使用した読み取りタイムアウト

私はこのようなURLを読むためのコードを持っています:

from urllib2 import Request, urlopen
req = Request(url)
for key, val in headers.items():
    req.add_header(key, val)
res = urlopen(req, timeout = timeout)
# This line blocks
content = res.read()

タイムアウトはurlopen()呼び出しで機能します。しかし、コードはres.read()呼び出しに到達し、そこで応答データを読み取りたいのですが、そこでタイムアウトは適用されません。そのため、読み取り呼び出しは、サーバーからのデータを待ってほぼ永久にハングする可能性があります。私が見つけた唯一の解決策は、シグナルを使用してread()を中断することです。これは、スレッドを使用しているため、私には適していません。

他にどのようなオプションがありますか?読み取りタイムアウトを処理するPythonのHTTPライブラリはありますか?httplib2とリクエストを確認しましたが、上記と同じ問題が発生しているようです。独自のノンブロッキングを記述したくありません。このためのライブラリがすでにあるはずなので、ソケットモジュールを使用したネットワークコード。

更新:以下の解決策のどれも私のためにそれをしていません。大きなファイルをダウンロードする場合、ソケットまたはurlopenタイムアウトを設定しても効果がないことがわかります。

from urllib2 import urlopen
url = 'http://iso.linuxquestions.org/download/388/7163/http/se.releases.ubuntu.com/ubuntu-12.04.3-desktop-i386.iso'
c = urlopen(url)
c.read()

少なくともPython 2.7.3のWindowsでは、タイムアウトは完全に無視されています。

25

スレッドなどを介して何らかの非同期タイマーを使用せずに、ライブラリでこれを行うことはできません。その理由は、timeouturllib2およびその他のライブラリで使用されるhttplibパラメータが、基になるtimeoutsocketを設定するためです。そして、これが実際に行うことは、 ドキュメント で説明されています。

SO_RCVTIMEO

入力関数が完了するまで待機する最大時間を指定するタイムアウト値を設定します。入力操作が完了するのを待機する時間の制限を指定する秒数とマイクロ秒のtimeval構造を受け入れます。追加データを受信せずに受信操作がこれだけ長い間ブロックされた場合、部分カウントまたはerrnoを[EAGAIN]または[EWOULDBLOCK]に設定して戻ります。 ]データが受信されない場合。

太字部分が重要です。 socket.timeoutは、timeoutウィンドウの期間中に1バイトが受信されなかった場合にのみ発生します。言い換えれば、これは受信したバイト間のtimeoutです。

threading.Timerを使用した簡単な関数は次のようになります。

import httplib
import socket
import threading

def download(Host, path, timeout = 10):
    content = None

    http = httplib.HTTPConnection(Host)
    http.request('GET', path)
    response = http.getresponse()

    timer = threading.Timer(timeout, http.sock.shutdown, [socket.SHUT_RD])
    timer.start()

    try:
        content = response.read()
    except httplib.IncompleteRead:
        pass

    timer.cancel() # cancel on triggered Timer is safe
    http.close()

    return content

>>> Host = 'releases.ubuntu.com'
>>> content = download(Host, '/15.04/ubuntu-15.04-desktop-AMD64.iso', 1)
>>> print content is None
True
>>> content = download(Host, '/15.04/MD5SUMS', 1)
>>> print content is None
False

Noneをチェックする以外に、関数の内部ではなく外部でhttplib.IncompleteRead例外をキャッチすることもできます。ただし、HTTPリクエストにContent-Lengthヘッダーがない場合、後者の場合は機能しません。

6
user479870

テストで( ここ で説明されている手法を使用して)、urlopen()呼び出しで設定されたタイムアウトがread()呼び出しにも影響することがわかりました。

import urllib2 as u
c = u.urlopen('http://localhost/', timeout=5.0)
s = c.read(1<<20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/socket.py", line 380, in read
    data = self._sock.recv(left)
  File "/usr/lib/python2.7/httplib.py", line 561, in read
    s = self.fp.read(amt)
  File "/usr/lib/python2.7/httplib.py", line 1298, in read
    return s + self._file.read(amt - len(s))
  File "/usr/lib/python2.7/socket.py", line 380, in read
    data = self._sock.recv(left)
socket.timeout: timed out

多分それは新しいバージョンの機能ですか?私は箱から出してすぐに12.04UbuntuでPython 2.7を使用しています。

5
Alfe

考えられる(不完全な)解決策の1つは、グローバルソケットタイムアウトを設定することです。これについて詳しく説明します ここ

import socket
import urllib2

# timeout in seconds
socket.setdefaulttimeout(10)

# this call to urllib2.urlopen now uses the default timeout
# we have set in the socket module
req = urllib2.Request('http://www.voidspace.org.uk')
response = urllib2.urlopen(req)

ただし、これは、ソケットモジュールのallユーザーのタイムアウトをグローバルに変更する場合にのみ機能します。 Celeryタスク内からリクエストを実行しているので、これを行うと、Celeryワーカーコード自体のタイムアウトが台無しになります。

他の解決策を聞いてうれしいです...

4
nitwit

非同期ネットワークライブラリでは、I/O操作に合計タイムアウトを適用できるようにする必要があります。たとえば、次のようになります イベントコード例

#!/usr/bin/env python2
import gevent
import gevent.monkey # $ pip install gevent
gevent.monkey.patch_all()

import urllib2

with gevent.Timeout(2): # enforce total timeout
    response = urllib2.urlopen('http://localhost:8000')
    encoding = response.headers.getparam('charset')
    print response.read().decode(encoding)

そしてここに asyncio相当

#!/usr/bin/env python3.5
import asyncio
import aiohttp # $ pip install aiohttp

async def fetch_text(url):
    response = await aiohttp.get(url)
    return await response.text()

text = asyncio.get_event_loop().run_until_complete(
    asyncio.wait_for(fetch_text('http://localhost:8000'), timeout=2))
print(text)

テストhttpサーバーはここで定義されています

4
jfs

pycurl.TIMEOUTオプションはリクエスト全体で機能します

#!/usr/bin/env python3
"""Test that pycurl.TIMEOUT does limit the total request timeout."""
import sys
import pycurl

timeout = 2 #NOTE: it does limit both the total *connection* and *read* timeouts
c = pycurl.Curl()
c.setopt(pycurl.CONNECTTIMEOUT, timeout)
c.setopt(pycurl.TIMEOUT, timeout)
c.setopt(pycurl.WRITEFUNCTION, sys.stdout.buffer.write)
c.setopt(pycurl.HEADERFUNCTION, sys.stderr.buffer.write)
c.setopt(pycurl.NOSIGNAL, 1)
c.setopt(pycurl.URL, 'http://localhost:8000')
c.setopt(pycurl.HTTPGET, 1)
c.perform()

このコードでは、タイムアウトエラーが約2秒で発生します。チャンク間のタイムアウトよりも短い時間で複数のチャンクで応答を送信するサーバーで、合計readタイムアウトをテストしました。

$ python -mslow_http_server 1

どこ slow_http_server.py

#!/usr/bin/env python
"""Usage: python -mslow_http_server [<read_timeout>]

   Return an http response with *read_timeout* seconds between parts.
"""
import time
try:
    from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer, test
except ImportError: # Python 3
    from http.server import BaseHTTPRequestHandler, HTTPServer, test

def SlowRequestHandlerFactory(read_timeout):
    class HTTPRequestHandler(BaseHTTPRequestHandler):
        def do_GET(self):
            n = 5
            data = b'1\n'
            self.send_response(200)
            self.send_header("Content-type", "text/plain; charset=utf-8")
            self.send_header("Content-Length", n*len(data))
            self.end_headers()
            for i in range(n):
                self.wfile.write(data)
                self.wfile.flush()
                time.sleep(read_timeout)
    return HTTPRequestHandler

if __name__ == "__main__":
    import sys
    read_timeout = int(sys.argv[1]) if len(sys.argv) > 1 else 5
    test(HandlerClass=SlowRequestHandlerFactory(read_timeout),
         ServerClass=HTTPServer)

私はテストしました 合計接続タイムアウトとhttp://google.com:22222

3
jfs

これは一般的な問題だと思いますが、それでも-どこにも答えが見つかりません...タイムアウト信号を使用してこれに対するソリューションを構築しただけです。

import urllib2
import socket

timeout = 10
socket.setdefaulttimeout(timeout)

import time
import signal

def timeout_catcher(signum, _):
    raise urllib2.URLError("Read timeout")

signal.signal(signal.SIGALRM, timeout_catcher)

def safe_read(url, timeout_time):
    signal.setitimer(signal.ITIMER_REAL, timeout_time)
    url = 'http://uberdns.eu'
    content = urllib2.urlopen(url, timeout=timeout_time).read()
    signal.setitimer(signal.ITIMER_REAL, 0)
    # you should also catch any exceptions going out of urlopen here,
    # set the timer to 0, and pass the exceptions on.

ソリューションのシグナル部分のクレジットはここにあります: python timer mystery

2
kolinko

これは私が見ている行動ではありません。通話がタイムアウトすると、URLErrorが表示されます。

_from urllib2 import Request, urlopen
req = Request('http://www.google.com')
res = urlopen(req,timeout=0.000001)
#  Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
#  ...
#  raise URLError(err)
#  urllib2.URLError: <urlopen error timed out>
_

このエラーをキャッチして、resを読み取ろうとしないようにすることはできませんか?この後、res.read()を使おうとすると、_NameError: name 'res' is not defined._が表示されます。必要なものは次のようなものです。

_try:
    res = urlopen(req,timeout=3.0)
except:           
    print 'Doh!'
finally:
    print 'yay!'
    print res.read()
_

タイムアウトを手動で実装する方法はmultiprocessing経由だと思います、違いますか?ジョブが終了していない場合は、ジョブを終了できます。

0
ChrisP