2012-03-03 12 views
22

Ho codice per la lettura di un URL simile a questo:Leggi timeout utilizzando urllib2 o qualsiasi altra libreria http

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() 

Il timeout lavora per la chiamata urlopen(). Ma poi il codice arriva alla chiamata res.read() dove voglio leggere i dati di risposta e il timeout non è applicato lì. Quindi la chiamata in lettura potrebbe bloccarsi quasi in attesa di dati dal server. L'unica soluzione che ho trovato è di usare un segnale per interrompere la lettura() che non è adatta a me dal momento che sto usando thread.

Quali altre opzioni ci sono? Esiste una libreria HTTP per Python che gestisce i timeout di lettura? Ho esaminato httplib2 e le richieste e sembrano soffrire lo stesso problema di cui sopra. Non voglio scrivere il mio codice di rete non bloccante usando il modulo socket perché penso che ci dovrebbe essere già una libreria per questo.

Aggiornamento: Nessuna delle soluzioni seguenti lo sta facendo per me. Si può vedere di persona che l'impostazione della presa o timeout urlopen non ha effetto quando si scarica un file di grandi dimensioni:

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() 

Almeno su Windows con Python 2.7.3, i timeout vengono completamente ignorati.

+0

relativo al totale * timeout di connessione *: [HTTPConnection.request non rispetta il timeout?] (Http://stackoverflow.com/q/28669744/4279) – jfs

risposta

0

Questo non è il comportamento che vedo. Ho un URLError quando i tempi di chiamata fuori:

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> 

Non riesci a prendere questo errore e quindi evitare di cercare di leggere res? Quando provo ad usare res.read() dopo questo ho NameError: name 'res' is not defined. è qualcosa di simile quello che vi serve:

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

Suppongo che il modo di implementare manualmente un timeout è via multiprocessing, no? Se il lavoro non è finito, puoi terminarlo.

+5

Penso che tu fraintenda. La chiamata urlopen() si connette al server con successo, ma poi il programma si blocca alla chiamata read() perché il server restituisce i dati lentamente. È qui che è necessario il timeout. –

4

Una possibile soluzione (imperfetta) è quello di impostare il timeout socket globale, ha spiegato più in dettaglio here:

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) 

Tuttavia, questo funziona solo se si è disposti a modificare globalmente il timeout per tutto utenti del modulo socket. Sto eseguendo la richiesta dall'interno di un'attività di Celery, quindi fare questo potrebbe rovinare i timeout per il codice di lavoro di Celery stesso.

Sarei felice di sentire altre soluzioni ...

+0

Almeno su Windows con Python 2.7 non ha alcun effetto sulla chiamata read(). –

+0

['setdefaulttimeout()' non limita il timeout di lettura totale] (http://stackoverflow.com/a/32684677/4279), ad esempio, il server può inviare un byte ogni 5 secondi e il timeout non si attiva mai. – jfs

5

ho trovato nel mio test (con la tecnica descritta here) che un timeout impostato nella chiamata urlopen() effettua anche la read() chiamata:

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 

Forse è una funzionalità delle versioni più recenti? Sto usando Python 2.7 su Ubuntu 12.04 direttamente dalla scatola.

+2

potrebbe attivare il timeout per le singole chiamate '.recv()' (che potrebbero restituire dati parziali) ma [non limita il timeout di lettura totale (fino a EOF)] (http://stackoverflow.com/a/32684677/ 4279). – jfs

+0

Sì, quel chiarimento ha il suo valore. – Alfe

2

Mi aspetterei che questo sia un problema comune, eppure - nessuna risposta si trova da nessuna parte ...Appena costruito una soluzione per questo segnale di timeout utilizzando:

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. 

Il credito per la parte della soluzione segnale va qui btw: python timer mystery

+0

Ma timeout della chiamata 'read()' o 'urlopen()'? Mi piacerebbe testare questa soluzione, ma è piuttosto difficile impostare una situazione in cui i timeout del server durante la chiamata dei clienti sul recv. –

+0

Funziona anche nei thread? –

+0

Bjorn, come per la lettura vs urlopen - ha timeout sia letti che urlopen. L'ho provato con questo url: "http://uberdns.eu" - che, almeno ieri, ha causato il blocco del crawler. Questa è la soluzione che ho testato e funzionato dove sia il timeout di default del socket, sia il timeout di urlopen fallito. – kolinko

-1

avuto lo stesso problema con timeout presa sulla dichiarazione di lettura. Ciò che ha funzionato per me è stato mettere sia l'urlopen che la lettura all'interno di una dichiarazione di prova. Spero che questo ti aiuti!

5

Non è possibile per nessuna libreria eseguire questa operazione senza utilizzare una sorta di timer asincrono tramite thread o altro. Il motivo è che il parametro timeout utilizzato in httplib, urllib2 e altre librerie imposta lo timeout sul sottostante socket. E ciò che effettivamente fa è spiegato nello documentation.

SO_RCVTIMEO

Imposta il valore di timeout che specifica la quantità massima di tempo una funzione di ingresso attende fino al completamento. Accetta una struttura temporale con il numero di secondi e microsecondi che specifica il limite sul tempo di attesa per il completamento di un'operazione di input. Se un'operazione di ricezione è bloccata per questo tempo molto lungo, senza ricevere ulteriori dati, deve restituire con un conteggio parziale o errno impostato su [EAGAIN] o [EWOULDBLOCK] se non si ricevono dati.

La parte in grassetto è la chiave. A socket.timeout viene generato solo se non è stato ricevuto un singolo byte per la durata della finestra timeout. In altre parole, questo è uno timeout tra i byte ricevuti.

Una semplice funzione che utilizza threading.Timer potrebbe essere la seguente.

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 

Altro che il controllo per None, è anche possibile intercettare l'eccezione httplib.IncompleteRead non all'interno della funzione, ma al di fuori di esso. Quest'ultimo caso non funzionerà se la richiesta HTTP non ha un'intestazione Content-Length.

+0

Non hai bisogno di lambda qui: 'Timer (timeout, sock.shutdown, [socket.SHUT_RDWR])'. Si dovrebbe generare TimeoutError su timeout invece di restituire 'None'. – jfs

+0

@ J.F.Sebastian Sì, ci sono numerosi modi per segnalare il timeout qui, come aumentare un'eccezione personalizzata. Grazie per il suggerimento '' 'args'''. –

+1

Ci sono * modi preferibili * per segnalare il timeout: la funzione download() può essere sepolta da vari riquadri dello stack dal luogo in cui si impostano i suoi parametri, il timeout può essere attivato solo per determinati siti in determinati momenti - cosa ti aspetti funzioni intermedie da fare se il contenuto è Nessuno? Se anche un solo posto si dimentica di gestire il valore restituito dall'errore; potrebbe avere effetti collaterali indesiderati. Le eccezioni sono il meccanismo che fornisce l'errore dal luogo in cui viene rilevato al punto in cui è noto cosa fare con esso. E il comportamento predefinito (gli errori non sono ignorati) è più robusto. – jfs

2

pycurl.TIMEOUT option works for the whole request:

#!/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() 

Il codice genera l'errore di timeout in ~ 2 secondi. Ho testato il totale leggere timeout con il server che invia la risposta in più pezzi con il tempo inferiore al timeout tra pezzi:

$ python -mslow_http_server 1 

dove 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) 

ho testato the total connection timeout with http://google.com:22222.

0

Qualsiasi libreria di rete asincrona dovrebbe consentire di imporre il timeout totale su qualsiasi operazione di I/O, ad es., Ecco gevent code example:

#!/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) 

Ed ecco asyncio equivalent:

#!/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) 

Il test http server is defined here.

Problemi correlati