Requests: no hay forma de leer contenido sin comprimir como un objeto similar a un archivo

Creado en 29 feb. 2012  ·  44Comentarios  ·  Fuente: psf/requests

Según la documentación, hay tres formas de leer el contenido de la respuesta: .text , .content y .raw . Los dos primeros consideran la codificación de transferencia y descomprimen la secuencia automáticamente cuando producen su resultado en memoria. Sin embargo, especialmente para el caso de que el resultado sea grande, actualmente no existe una forma sencilla de obtener el resultado descomprimido en forma de un objeto similar a un archivo, por ejemplo, pasarlo directamente a un analizador XML o Json.

Desde el punto de vista de una biblioteca que tiene como objetivo hacer que las solicitudes HTTP sean fáciles de usar, ¿por qué un usuario debería preocuparse por algo tan bajo como el tipo de compresión del flujo que se negoció internamente entre el servidor web y la biblioteca? Después de todo, es "culpa" de la biblioteca si acepta de forma predeterminada dicha transmisión. En este sentido, la secuencia .raw es demasiado cruda para mi gusto.

¿Quizás una cuarta propiedad como .stream podría proporcionar un mejor nivel de abstracción?

Comentario más útil

Ya expliqué por qué esto es un error de diseño y no una solicitud de función: la API existente usa la abstracción incorrecta y filtra detalles de negociación de la conexión al espacio de usuario que están a merced del sitio remoto y, por lo tanto, que el usuario no debería tiene que preocuparme. Eso hace que el soporte de lectura de flujo sin procesar actual sea difícil de usar. Básicamente, se trata de una solicitud de reparación de una función que no funciona, no de una solicitud de una nueva función.

Todos 44 comentarios

Response.iter_content

Erm, no, eso es un iterador. Estaba pidiendo un objeto similar a un archivo, es decir, algo que los procesadores de documentos puedan leer directamente.

Sería bastante simple hacer un objeto similar a un archivo con iter_content

Gracias por la rápida respuesta, por cierto.

Estoy de acuerdo. Aún así, sería aún más fácil para requests proporcionar esta funcionalidad. Mi punto es que .raw es el nivel incorrecto de abstracción para la mayoría de los casos de uso que desean leer de la transmisión, porque expone los detalles del nivel de transferencia.

Personalmente, no veo un caso de uso importante para iterar línea por línea o incluso fragmento por fragmento sobre el resultado de una solicitud HTTP, pero veo varios casos de uso importantes para analizarlo como un objeto similar a un archivo, específicamente formatos de respuesta que requieren un analizador de documentos, como HTML, XML, Json, etc.

Tenga en cuenta también que es mucho más fácil escribir un iterador que envuelve un objeto similar a un archivo, que un objeto similar a un archivo que envuelve un iterador.

Se me ocurrió el siguiente código. Maneja todos los casos necesarios, pero lo encuentro bastante complejo. Por eso dije que quería algo como esto como parte de la biblioteca. Los usuarios no deberían tener que resolver esto por sí mismos.

Creo que el código dentro de las solicitudes 'models.py usa la abstracción incorrecta aquí. Debe descomprimir el flujo sin procesar _antes_ de que comience con su maquinaria de iteración, no durante la iteración. Pasar de un archivo similar a un iterador solo para volver a un archivo similar es simplemente estúpido. Una sola transformación de API es más que suficiente y, de todos modos, a la mayoría de los usuarios no les importan los iteradores de contenido.

class FileLikeDecompressor(object):
    """
    File-like object that wraps and decompresses an HTTP stream transparently.
    """
    def __init__(self, stream, mode='gzip'):
        self.stream = stream
        zlib_mode = 16 + zlib.MAX_WBITS if mode == 'gzip' else -zlib.MAX_WBITS  # magic
        self.dec = zlib.decompressobj(zlib_mode)
        self.data = ''

    def read(self, n=None):
        if self.dec is None:
            return '' # all done
        if n is None:
            data = self.data + self.dec.decompress(self.stream.read())
            self.data = self.dec = None
            return data
        while len(self.data) < n:
            new_data = self.stream.read(n)
            self.data += self.dec.decompress(new_data)
            if not new_data:
                self.dec = None
                break
        if self.data:
            data, self.data = self.data[:n], self.data[n:]
            return data
        return ''

def decompressed(response):
    """
    Return a file-like object that represents the uncompressed HTTP response data.
    For compressed HTTP responses, wraps the stream in a FileLikeDecompressor.
    """
    stream = response.raw
    mode = response.headers.get('content-encoding')
    if mode in ('gzip', 'deflate'):
        return FileLikeDecompressor(stream, mode)
    return stream

¿Por qué no construye el objeto similar a un archivo a partir de content_iter como se propone? Esto podría verse así:

class FileLikeFromIter(object):
    def __init__(self, content_iter):
        self.iter = content_iter
        self.data = ''

    def __iter__(self):
        return self.iter

    def read(self, n=None):
        if n is None:
            return self.data + '\n'.join(l for l in self.iter)
        else:
            while len(self.data) < n:
                try:
                    self.data = '\n'.join((self.data, self.iter.next()))
                except StopIteration:
                    break
            result, self.data = self.data[:n], self.data[n:]
            return result

Es posible que desee leer mi comentario nuevamente, específicamente el párrafo que precede al código que publiqué.

Sí, pero esta solución es aún más limpia (y en mi opinión más fácil) que realizar la descompresión en un segundo lugar porque esto ya está integrado en las solicitudes.

Pero estoy de acuerdo contigo en general, un r.file (o algo como esto) tiene muchos más casos de uso que r.raw . Así que me gustaría que esto también se incluyera en las solicitudes. @kennethreitz

"response.stream" me suena como un buen nombre.

Para esto es response.raw :)

Eso fue también lo que pensé intuitivamente cuando lo vi. Pero luego me di cuenta de que response.raw está roto porque expone detalles internos de la capa de transporte subyacente que los usuarios no deberían tener que preocuparse.

El único método que deberían necesitar es raw.read ?

Bueno, sí, excepto que raw.read () se comporta de manera diferente dependiendo de las negociaciones internas entre el cliente y el servidor. A veces devuelve los datos esperados y, a veces, devuelve bytes comprimidos desnudos.

Básicamente, response.raw es una característica agradable que la mayoría de los usuarios ignorarían felizmente y que algunos usuarios avanzados podrían encontrar útil, mientras que un response.stream independiente de la compresión es una característica que la mayoría de los usuarios de transmisión desear.

+1

+1

¿Se solucionará este error de diseño?

No estoy seguro de cuán correcta o eficiente es esta forma, pero para mí, lo siguiente funciona :

>>> import lxml  # a parser that scorns encoding
>>> unicode_response_string = response.text
>>> lxml.etree.XML(bytes(bytearray(unicode_response_string, encoding='utf-8')))  # provided unicode() means utf-8
<Element html at 0x105364870>

@kernc : Eso es algo extraño. response.content ya es una cadena de bytes, así que lo que estás haciendo aquí es decodificar el contenido con el códec que elija Python y luego volver a codificarlo como utf-8.

Esto _no_ es un error, y definitivamente no es el error que sugirió. Si realmente necesita un objeto similar a un archivo, le recomiendo StringIO y BytesIO.

@Lukasa tiene razón. content siempre debe ser una cadena de bytes (en Python 3 es una cadena de bytes explícita; en Python 2 str == bytes). El único elemento que no es una cadena de bytes es text .

@kennethreitz ¿ alguna noticia sobre esto? Este es un error de diseño bastante serio y es mejor solucionarlo pronto. Cuanto más código se escriba para solucionarlo, más costoso será para todos.

Esto no es un error de diseño, es solo una solicitud de función. Y como las solicitudes tienen una función congelada , supongo que esto no se incluirá en las solicitudes en el corto plazo (si es que lo hace) ...

No creo que volver a declarar un error de diseño de larga data sea una "característica faltante"
hace que desaparezca tan fácilmente. Escuché que el autor está pensando en
haciendo que las "solicitudes" formen parte del stdlib de Python. Eso seria bueno
oportunidad de arreglar esto.

Escuché que el autor está pensando en
haciendo que las "solicitudes" formen parte del stdlib de Python.

Realmente no: http://docs.python-requests.org/en/latest/dev/philosophy/#standard -library

Esto no es un error, es una solicitud de función. Las solicitudes no están haciendo nada mal, simplemente no están haciendo algo que sea opcional. Esa es la definición misma de una característica.

Además, la preparación para stdlib es exactamente la razón por la que Requests está congelada. Una vez que Requests está en stdlib, resulta muy difícil corregir los errores a tiempo. Como resultado, si agregar la nueva función agrega errores o hace retroceder el comportamiento, la versión en stdlib no se puede arreglar hasta la próxima versión menor. Eso sería malo.

Marc Schlaich, 19.03.2013 08:41:

Escuché que el autor está pensando en
haciendo que las "solicitudes" formen parte del stdlib de Python.

Realmente no: http://docs.python-requests.org/en/latest/dev/philosophy/#standard -library

Lo leo aqui:

http://python-notes.boredomandlaziness.org/en/latest/conferences/pyconus2013/20130313-language-summit.html

Stefan

Ya expliqué por qué esto es un error de diseño y no una solicitud de función: la API existente usa la abstracción incorrecta y filtra detalles de negociación de la conexión al espacio de usuario que están a merced del sitio remoto y, por lo tanto, que el usuario no debería tiene que preocuparme. Eso hace que el soporte de lectura de flujo sin procesar actual sea difícil de usar. Básicamente, se trata de una solicitud de reparación de una función que no funciona, no de una solicitud de una nueva función.

Permítanme resumir esto claramente. El error es que cualquier uso en el mundo real de la función de lectura de flujo sin formato tendrá que volver a implementar una parte de la biblioteca, específicamente toda la parte de descompresión de flujo condicional, porque la función es inútil sin ella, tan pronto como el cliente permita la compresión. Estamos hablando de código aquí que ya está allí, en "solicitudes", simplemente se usa en el lugar equivocado. Debe usarse por debajo del nivel de lectura sin procesar, no por encima de él, porque el cliente no puede controlar si el servidor respeta el encabezado de aceptación o no. La compresión debe ser un detalle de negociación transparente de la conexión, no algo que lastime a cualquier usuario que habilite el encabezado correspondiente.

No puedo pensar en ningún caso de uso en el que el cliente esté interesado en el flujo comprimido, especialmente si no puede predecir si el flujo realmente se comprimirá o no, ya que el servidor puede ignorar felizmente el deseo del cliente. Es un puro detalle de negociación. Es por eso que la lectura de flujo sin procesar utiliza la abstracción incorrecta al preferir el caso de uso extremadamente improbable sobre el más común.

Yo puedo. Por ejemplo, ¿qué pasaría si estuviera descargando un archivo de texto de gran tamaño y quisiera mantenerlo comprimido? Podría hacer un seguimiento de este cambio con un nuevo 'error de diseño' titulado No hay forma de guardar los datos comprimidos originalmente en el disco .

Esa idea es intencionalmente trillada y estúpida, pero estoy tratando de ilustrar un punto, que es este: Requests no está obligado a ofrecer a todos exactamente el mecanismo de interacción que desean. De hecho, hacerlo iría directamente en contra del objetivo principal que tiene Requests, que es la simplicidad de la API. Hay una lista larga, larga y _larga_ de cambios propuestos a las solicitudes que se objetaron porque complican la API, a pesar de que agregaron una funcionalidad útil. Requests no tiene como objetivo reemplazar urllib2 para todos los casos de uso, tiene como objetivo simplificar los casos más comunes.

En este caso, Requests asume que la mayoría de los usuarios no quieren objetos similares a archivos y, por lo tanto, propone las siguientes interacciones:

  • Response.text y Response.content : desea todos los datos de una vez.
  • Response.iter_lines() y Response.iter_content() : No desea todos los datos de una vez.
  • Response.raw : No está satisfecho con las otras dos opciones, así que hágalo usted mismo.

Estos se eligieron porque representan de manera abrumadora los usos comunes de las solicitudes. Ha dicho que "a la mayoría de los usuarios no les importan los iteradores de contenido de todos modos " y " response.stream es una característica que la mayoría de los usuarios de streaming querrían ". La experiencia en este proyecto me lleva a estar en desacuerdo: mucha gente usa los iteradores de contenido y no muchos quieren desesperadamente objetos similares a archivos.

Un último punto: si la compresión debería ser un detalle de negociación transparente de la conexión, entonces debería generar el error apropiado contra urllib3, que maneja nuestra lógica de conexión.

Lamento que sienta que las solicitudes no son adecuadas para su caso de uso.

Entiendo su punto de que response.raw está roto en la implementación actual e incluso parcialmente de acuerdo con eso (al menos debería poder obtener detalles de compresión sin analizar los encabezados).

Sin embargo, su propuesta sigue siendo una solicitud de función ...

@Lukasa
Realmente no puedo ver cómo presentar el error contra urllib3 arreglaría la API de solicitudes, al menos no por sí solo.

Y estoy de acuerdo en que su "caso de uso" está controlado. Como dije, si el cliente no puede controlar positivamente la compresión en el lado del servidor (y lo deshabilita, pero no lo habilita de manera confiable), entonces confiar en él para poder guardar un archivo comprimido en el disco no es, bueno, tan interesante .

@schlamar
Estoy de acuerdo en que se puede leer como tal. Les aseguro que estoy bien con cualquier cosa que resuelva este problema. Si se requiere abrir un nuevo boleto para llegar allí, que así sea.

Si se requiere abrir un nuevo boleto para llegar allí, que así sea.

Sigo pensando que Kenneth rechazará esto debido a la congelación de funciones.

Estoy bien con cualquier cosa que resuelva este problema.

  1. Envuelva iter_content como un objeto similar a un archivo o
  2. Analice los encabezados y descomprima response.raw si corresponde

Ambas soluciones están en los comentarios anteriores, la última publicada por usted. ¿Por qué es un problema que esto no estará directamente en las solicitudes?

Seamos 100% claros aquí: básicamente, no hay posibilidad de que esto entre en las Solicitudes mientras está en función de congelación. Nada está roto, la API simplemente no es perfecta para sus necesidades. Como no hay nada roto, lo único que importa es si Kenneth lo quiere. Solicitudes no es una democracia, es un voto por un hombre. Kenneth es el hombre, tiene el voto . Kenneth cerró este problema hace 8 meses, por lo que parece bastante claro que no lo quiere.

Realmente no puedo ver cómo presentar el error contra urllib3 arreglaría la API de solicitudes, al menos no por sí solo.

Parchear urllib3 para devolver siempre el archivo-objeto sin comprimir debería resolver esto por sí solo (no se dice que sea una buena idea).

Oh, aquí está la solución número 3 (no probada):

response.raw.read = functools.partial(response.raw.read, decode_content=True)

Ver https://github.com/shazow/urllib3/blob/master/urllib3/response.py#L112

Interesante, no sabía que existía a estas alturas. Eso hace que sea mucho más fácil envolver la función, seguro.

Aunque, ¿eso realmente funciona? Es decir, ¿los descompresores son incrementales y con estado? La segunda llamada a read (123) ya no devolverá el inicio válido de un archivo gzip, por ejemplo.

Aunque, ¿eso realmente funciona? Es decir, ¿los descompresores son incrementales y con estado?

Oh, no lo parece. No leí la cadena de documentos.

Sin embargo, aquí está mi propuesta:

  1. Parchea urllib3 para que HTTPResponse.read funcione con amt y decode_content mismo tiempo.
  2. Haga de HTTPResponse._decode_content un miembro público (para que pueda hacer response.raw.decode_content = True lugar de parchear el método read ).
  3. Elimine la descompresión en las solicitudes por completo utilizando decode_content=True en iter_content

@Lukasa Creo que esto no violará la función congelada, ¿verdad?

@schlamar : En principio, claro. Mientras la API permanezca sin cambios, los cambios internos _deberían_ estar bien, y yo haría +1 en este. Sin embargo, ten en cuenta que no soy el BDFL, =)

stream_decompress en solicitudes está roto de todos modos: # 1249

+1

¿Fue útil esta página
0 / 5 - 0 calificaciones