2011-03-06 17 views
10

Usar urllibs (o urllibs2) y querer lo que quiero es inútil. ¿Alguna solución?Python - buscar en la corriente de respuesta http

+0

¿Qué quiere decir con 'buscar en la secuencia de respuesta http'? – phooji

+0

Una vez usé C# y la implementación de lo que hablo fue así: 'WebClient.OpenRead(). Seek()'. –

+0

Un objeto contenedor simple que puede dar esta funcionalidad usando la cabecera gama http: http://stackoverflow.com/questions/7829311/is-there-a-library-for-retrieving-a-file-from-a-remote- zip/7852229 # 7852229 – retracile

Respuesta

22

No estoy seguro de cómo funciona la aplicación C#, pero, como corrientes de Internet en general, no son buscable, supongo que descarga todos los datos a un archivo local o un objeto en memoria y busca desde allí. El equivalente de Python de esto sería hacer lo que Abafei sugirió y escribir los datos en un archivo o StringIO y buscar desde allí.

Sin embargo, si, como sugiere su comentario sobre la respuesta de Abafei, desea recuperar solo una parte particular del archivo (en lugar de buscar hacia atrás y hacia adelante a través de los datos devueltos), existe otra posibilidad. urllib2 se puede utilizar para recuperar cierta sección (o 'rango' en el lenguaje HTTP) de una página web, siempre que el servidor admita este comportamiento.

El range cabecera

Cuando se envía una solicitud a un servidor, los parámetros de la petición se dan en diferentes cabeceras. Uno de estos es el encabezado Range, definido en section 14.35 of RFC2616 (la especificación que define HTTP/1.1). Este encabezado le permite hacer cosas tales como recuperar todos los datos a partir del octeto número 10.000 o los datos entre los bytes 1.000 y 1.500.

apoyo servidor

No existe ningún requisito para un servidor para apoyar la recuperación de rango. Algunos servidores devolverán el encabezado Accept-Ranges (section 14.5 of RFC2616) junto con una respuesta para informar si admiten rangos o no. Esto podría verificarse utilizando una solicitud HEAD. Sin embargo, no hay una necesidad particular de hacer esto; si un servidor no admite rangos, devolverá toda la página y luego podremos extraer la porción de datos deseada en Python como antes.

Verificando una gama se devuelve

Si el servidor devuelve un rango, se debe enviar la cabecera Content-Range (section 14.16 of RFC2616) junto con la respuesta. Si esto está presente en los encabezados de la respuesta, sabemos que se devolvió un rango; si no está presente, se devolvió la página completa.

Ejecución con urllib2

urllib2 nos permite añadir cabeceras a una solicitud, lo que nos permite pedir al servidor para un rango en lugar de toda la página. El siguiente script toma una URL, una posición de inicio y (opcionalmente) una longitud en la línea de comando, e intenta recuperar la sección dada de la página.

import sys 
import urllib2 

# Check command line arguments. 
if len(sys.argv) < 3: 
    sys.stderr.write("Usage: %s url start [length]\n" % sys.argv[0]) 
    sys.exit(1) 

# Create a request for the given URL. 
request = urllib2.Request(sys.argv[1]) 

# Add the header to specify the range to download. 
if len(sys.argv) > 3: 
    start, length = map(int, sys.argv[2:]) 
    request.add_header("range", "bytes=%d-%d" % (start, start + length - 1)) 
else: 
    request.add_header("range", "bytes=%s-" % sys.argv[2]) 

# Try to get the response. This will raise a urllib2.URLError if there is a 
# problem (e.g., invalid URL). 
response = urllib2.urlopen(request) 

# If a content-range header is present, partial retrieval worked. 
if "content-range" in response.headers: 
    print "Partial retrieval successful." 

    # The header contains the string 'bytes', followed by a space, then the 
    # range in the format 'start-end', followed by a slash and then the total 
    # size of the page (or an asterix if the total size is unknown). Lets get 
    # the range and total size from this. 
    range, total = response.headers['content-range'].split(' ')[-1].split('/') 

    # Print a message giving the range information. 
    if total == '*': 
     print "Bytes %s of an unknown total were retrieved." % range 
    else: 
     print "Bytes %s of a total of %s were retrieved." % (range, total) 

# No header, so partial retrieval was unsuccessful. 
else: 
    print "Unable to use partial retrieval." 

# And for good measure, lets check how much data we downloaded. 
data = response.read() 
print "Retrieved data size: %d bytes" % len(data) 

Usando esto, puedo recuperar los últimos 2.000 bytes de la página Python:

[email protected]:~$ python retrieverange.py http://www.python.org/ 17387 
Partial retrieval successful. 
Bytes 17387-19386 of a total of 19387 were retrieved. 
Retrieved data size: 2000 bytes 

o 400 bytes desde el medio de la página de inicio:

[email protected]:~$ python retrieverange.py http://www.python.org/ 6000 400 
Partial retrieval successful. 
Bytes 6000-6399 of a total of 19387 were retrieved. 
Retrieved data size: 400 bytes 

Sin embargo, el Google Página de inicio no admite rangos:

[email protected]:~$ python retrieverange.py http://www.google.com/ 1000 500 
Unable to use partial retrieval. 
Retrieved data size: 9621 bytes 

En este caso, sería necesario extraer los datos de interés en Python antes de cualquier procesamiento posterior.

3

Puede funcionar mejor simplemente escribir los datos en un archivo (o incluso en una cadena, usando StringIO), y buscar en ese archivo (o cadena).

+3

Digamos de una respuesta del 1MB la primera 900KB son inútiles para mí, así que es una oportunidad para acelerar el proceso y no para descargarlos. –

1

Ver

Python seek on remote file using HTTP

La solución basada en el apoyo gama HTTP tal como se define en el RFC 2616.

+0

¿No está limitado el alcance con la compatibilidad del servidor? –

0

No encontré ninguna implementación existente de una interfaz de tipo archivo con seek() para las URL HTTP, así que lancé mi propia versión simple: https://github.com/valgur/pyhttpio. Depende de urllib.request, pero probablemente podría modificarse fácilmente para usar requests, si es necesario.

El código completo:

import cgi 
import time 
import urllib.request 
from io import IOBase 
from sys import stderr 


class SeekableHTTPFile(IOBase): 
    def __init__(self, url, name=None, repeat_time=-1, debug=False): 
     """Allow a file accessible via HTTP to be used like a local file by utilities 
     that use `seek()` to read arbitrary parts of the file, such as `ZipFile`. 
     Seeking is done via the 'range: bytes=xx-yy' HTTP header. 

     Parameters 
     ---------- 
     url : str 
      A HTTP or HTTPS URL 
     name : str, optional 
      The filename of the file. 
      Will be filled from the Content-Disposition header if not provided. 
     repeat_time : int, optional 
      In case of HTTP errors wait `repeat_time` seconds before trying again. 
      Negative value or `None` disables retrying and simply passes on the exception (the default). 
     """ 
     super().__init__() 
     self.url = url 
     self.name = name 
     self.repeat_time = repeat_time 
     self.debug = debug 
     self._pos = 0 
     self._seekable = True 
     with self._urlopen() as f: 
      if self.debug: 
       print(f.getheaders()) 
      self.content_length = int(f.getheader("Content-Length", -1)) 
      if self.content_length < 0: 
       self._seekable = False 
      if f.getheader("Accept-Ranges", "none").lower() != "bytes": 
       self._seekable = False 
      if name is None: 
       header = f.getheader("Content-Disposition") 
       if header: 
        value, params = cgi.parse_header(header) 
        self.name = params["filename"] 

    def seek(self, offset, whence=0): 
     if not self.seekable(): 
      raise OSError 
     if whence == 0: 
      self._pos = 0 
     elif whence == 1: 
      pass 
     elif whence == 2: 
      self._pos = self.content_length 
     self._pos += offset 
     return self._pos 

    def seekable(self, *args, **kwargs): 
     return self._seekable 

    def readable(self, *args, **kwargs): 
     return not self.closed 

    def writable(self, *args, **kwargs): 
     return False 

    def read(self, amt=-1): 
     if self._pos >= self.content_length: 
      return b"" 
     if amt < 0: 
      end = self.content_length - 1 
     else: 
      end = min(self._pos + amt - 1, self.content_length - 1) 
     byte_range = (self._pos, end) 
     self._pos = end + 1 
     with self._urlopen(byte_range) as f: 
      return f.read() 

    def readall(self): 
     return self.read(-1) 

    def tell(self): 
     return self._pos 

    def __getattribute__(self, item): 
     attr = object.__getattribute__(self, item) 
     if not object.__getattribute__(self, "debug"): 
      return attr 

     if hasattr(attr, '__call__'): 
      def trace(*args, **kwargs): 
       a = ", ".join(map(str, args)) 
       if kwargs: 
        a += ", ".join(["{}={}".format(k, v) for k, v in kwargs.items()]) 
       print("Calling: {}({})".format(item, a)) 
       return attr(*args, **kwargs) 

      return trace 
     else: 
      return attr 

    def _urlopen(self, byte_range=None): 
     header = {} 
     if byte_range: 
      header = {"range": "bytes={}-{}".format(*byte_range)} 
     while True: 
      try: 
       r = urllib.request.Request(self.url, headers=header) 
       return urllib.request.urlopen(r) 
      except urllib.error.HTTPError as e: 
       if self.repeat_time is None or self.repeat_time < 0: 
        raise 
       print("Server responded with " + str(e), file=stderr) 
       print("Sleeping for {} seconds before trying again".format(self.repeat_time), file=stderr) 
       time.sleep(self.repeat_time) 

un potencial de uso de ejemplo:

url = "https://www.python.org/ftp/python/3.5.0/python-3.5.0-embed-amd64.zip" 
f = SeekableHTTPFile(url, debug=True) 
zf = ZipFile(f) 
zf.printdir() 
zf.extract("python.exe") 

Editar: En realidad, hay una casi idénticas, aunque un poco más mínima, la aplicación en esta respuesta: https://stackoverflow.com/a/7852229/2997179

Cuestiones relacionadas