2009-12-28 19 views
10

¿Cómo busco una posición particular en un archivo remoto (HTTP) para poder descargar solo esa parte?Python buscar en un archivo remoto usando HTTP

permite decir los bytes de un archivo remoto fueron: 1234567890

Quiero buscar a 4 y descarga 3 bytes desde allí, así que tendría: 456

y también, cómo puedo comprobar si un control remoto ¿El archivo existe? Intenté, os.path.isfile() pero devuelve False cuando paso una URL de archivo remoto.

+2

¿Qué quieres decir con "remoto"? –

+0

¿Qué protocolo estás usando? HTTP? FTP? NFS? SFTP? –

+0

por control remoto me refiero a http – Marconi

Respuesta

15

Si está descargando el archivo remoto a través de HTTP, debe configurar el encabezado Range.

Compruebe in this example cómo se puede hacer. Se ve así:

myUrlclass.addheader("Range","bytes=%s-" % (existSize)) 

EDITAR: I just found a better implementation. Esta clase es muy simple de usar, como se puede ver en la docstring.

class HTTPRangeHandler(urllib2.BaseHandler): 
"""Handler that enables HTTP Range headers. 

This was extremely simple. The Range header is a HTTP feature to 
begin with so all this class does is tell urllib2 that the 
"206 Partial Content" reponse from the HTTP server is what we 
expected. 

Example: 
    import urllib2 
    import byterange 

    range_handler = range.HTTPRangeHandler() 
    opener = urllib2.build_opener(range_handler) 

    # install it 
    urllib2.install_opener(opener) 

    # create Request and set Range header 
    req = urllib2.Request('http://www.python.org/') 
    req.header['Range'] = 'bytes=30-50' 
    f = urllib2.urlopen(req) 
""" 

def http_error_206(self, req, fp, code, msg, hdrs): 
    # 206 Partial Content Response 
    r = urllib.addinfourl(fp, hdrs, req.get_full_url()) 
    r.code = code 
    r.msg = msg 
    return r 

def http_error_416(self, req, fp, code, msg, hdrs): 
    # HTTP's Range Not Satisfiable error 
    raise RangeError('Requested Range Not Satisfiable') 

actualización: El "mejor aplicación" se ha trasladado a github: excid3/urlgrabber en el archivo byterange.py.

+0

+1 para la actualización con una mejor implementación. –

+0

justo lo que necesitaba. Gracias. – Marconi

1

Creo que la clave de tu pregunta es que dijiste "url de archivo remoto". Esto implica que está utilizando una URL HTTP para descargar un archivo con una operación HTTP "get".

Así que acabo de hacer una búsqueda en Google de "HTTP GET" y me encontré esto para usted:

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35

Parece que se puede especificar un rango de bytes en un HTTP GET.

Por lo tanto, debe usar una biblioteca HTTP que le permita especificar el rango de bytes. Y mientras escribía esto, jbochi publicó un enlace a un ejemplo.

4

AFAIK, esto no es posible con fseek() o similar. Debe usar el encabezado del rango HTTP para lograr esto. Este encabezado puede o no ser admitido por el servidor, por lo que su millaje puede variar.

import urllib2 

myHeaders = {'Range':'bytes=0-9'} 

req = urllib2.Request('http://www.promotionalpromos.com/mirrors/gnu/gnu/bash/bash-1.14.3-1.14.4.diff.gz',headers=myHeaders) 

partialFile = urllib2.urlopen(req) 

s2 = (partialFile.read()) 

EDIT: Esto es, por supuesto, asumiendo que al archivo remoto que quiere decir un archivo almacenado en un servidor HTTP ...

Si el archivo que desea está en un servidor FTP, FTP sólo permite a especifique un start offset y no un rango. Si esto es lo que quiere, entonces el siguiente código debería hacerlo (no probado!)

import ftplib 
fileToRetrieve = 'somefile.zip' 
fromByte = 15 
ftp = ftplib.FTP('ftp.someplace.net') 
outFile = open('partialFile', 'wb') 
ftp.retrbinary('RETR '+ fileToRetrieve, outFile.write, rest=str(fromByte)) 
outFile.close() 
+0

También debe tratar los 206 códigos de respuesta, porque podrían ser aceptables si usa el encabezado de rango HTTP. – jbochi

+0

Lo suficientemente justo. Sin embargo, tu respuesta lo hace :) –

5

Le recomiendo usar la biblioteca requests. Es fácilmente la mejor biblioteca HTTP que he usado. En particular, para lograr lo que usted ha descrito, que haría algo como:

import requests 

url = "http://www.sffaudio.com/podcasts/ShellGameByPhilipK.Dick.pdf" 

# Retrieve bytes between offsets 3 and 5 (inclusive). 
r = requests.get(url, headers={"range": "bytes=3-5"}) 

# If a 4XX client error or a 5XX server error is encountered, we raise it. 
r.raise_for_status() 
+0

No había ninguna biblioteca de solicitudes en ese momento, pero sí, esto hace las cosas más simples ahora. – Marconi

0

No he encontrado ningún implementaciones existentes de una interfaz de tipo fichero con seek() a las direcciones URL HTTP, por lo que laminado en mi propia sencilla versión: 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 pequeño ejemplo de uso:

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