Requests: keine Möglichkeit, unkomprimierte Inhalte als dateiähnliches Objekt zu lesen

Erstellt am 29. Feb. 2012  ·  44Kommentare  ·  Quelle: psf/requests

Laut Dokumentation gibt es drei Möglichkeiten, den Inhalt der Antwort zu lesen: .text , .content und .raw . Die ersten beiden berücksichtigen die Übertragungscodierung und dekomprimieren den Stream automatisch, wenn sie ihr In-Memory-Ergebnis erzeugen. Insbesondere für den Fall, dass das Ergebnis groß ist, gibt es derzeit jedoch keine einfache Möglichkeit, an das dekomprimierte Ergebnis in Form eines dateiähnlichen Objekts zu gelangen, zB um es direkt an einen XML- oder Json-Parser zu übergeben.

Warum sollte sich ein Benutzer aus der Sicht einer Bibliothek, die HTTP-Anforderungen benutzerfreundlich machen soll, um etwas so Low-Level wie den Komprimierungstyp des Streams kümmern müssen, der intern zwischen dem Webserver und der Bibliothek ausgehandelt wurde? Schließlich ist es der "Fehler" der Bibliothek, wenn sie einen solchen Stream standardmäßig akzeptiert. In diesem Licht ist der .raw Stream für meinen Geschmack etwas zu roh.

Vielleicht bietet eine vierte Eigenschaft wie .stream eine bessere Abstraktionsebene?

Hilfreichster Kommentar

Ich habe bereits erklärt, warum dies ein Design-Bug und kein Feature-Request ist: Die vorhandene API verwendet die falsche Abstraktion und gibt Verhandlungsdetails der Verbindung in den Benutzerbereich, die der Gegenseite ausgeliefert sind, und die der Benutzer daher nicht sollte kümmern müssen. Das macht die aktuelle Unterstützung für das Lesen von Rohdatenströmen schwer zu verwenden. Im Wesentlichen handelt es sich hierbei um eine Anfrage zur Behebung einer defekten Funktion, nicht um eine Anfrage für eine neue Funktion.

Alle 44 Kommentare

Response.iter_content

Ähm, nein, das ist ein Iterator. Ich habe nach einem dateiähnlichen Objekt gefragt, dh etwas, aus dem Dokumentprozessoren direkt lesen können.

Es wäre ziemlich einfach, ein dateiähnliches Objekt mit iter_content erstellen

Danke für die schnelle Antwort, BTW.

Ich stimme zu. Dennoch wäre es für requests noch einfacher, diese Funktionalität bereitzustellen. Mein Punkt ist, dass .raw für die meisten Anwendungsfälle, die aus dem Stream lesen möchten, die falsche Abstraktionsebene ist, da sie Details der Übertragungsebene offenlegt.

Persönlich sehe ich keinen Hauptanwendungsfall für das Iterieren von Zeile für Zeile oder auch nur Stück für Stück über das Ergebnis einer HTTP-Anfrage, aber ich sehe mehrere wichtige Anwendungsfälle für das Parsen von diesem als dateiähnliches Objekt, insbesondere Antwortformate die einen Dokumentparser erfordern, wie HTML, XML, Json usw.

Beachten Sie auch, dass es viel einfacher ist, einen Iterator zu schreiben, der ein dateiähnliches Objekt umschließt, als ein dateiähnliches Objekt, das einen Iterator umschließt.

Ich bin auf folgenden Code gekommen. Es behandelt alle notwendigen Fälle, aber ich finde es ziemlich komplex. Deshalb habe ich gesagt, dass ich so etwas als Teil der Bibliothek haben möchte. Benutzer sollten dies nicht selbst herausfinden müssen.

Ich denke, dass der Code in der Datei models.py von Requests hier die falsche Abstraktion verwendet. Es sollte den Rohdatenstrom dekomprimieren, _bevor_ er mit seiner Iterationsmaschinerie beginnt, nicht während der Iteration. Von einem dateiähnlichen zu einem Iterator zu wechseln, nur um zu einem dateiähnlichen zurückzukehren, ist einfach dumm. Eine einzige API-Transformation ist mehr als genug und die meisten Benutzer werden sich sowieso nicht um Inhaltsiteratoren kümmern.

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

Warum bauen Sie das dateiähnliche Objekt nicht wie vorgeschlagen aus content_iter ? Das könnte so aussehen:

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

Vielleicht möchten Sie meinen Kommentar noch einmal lesen, insbesondere den Absatz, der dem von mir geposteten Code vorangeht.

Ja, aber diese Lösung ist immer noch sauberer (und IMO einfacher) als die Dekompression an zweiter Stelle, da dies bereits in Anfragen integriert ist.

Aber ich stimme Ihnen im Allgemeinen zu, ein r.file (oder so ähnlich) hat viel mehr Anwendungsfälle als r.raw . Daher würde ich mir wünschen, dass dies auch in Anfragen enthalten ist. @kennethreitz

"response.stream" klingt für mich nach einem guten Namen.

Dafür ist response.raw da :)

Das dachte ich auch intuitiv, als ich es sah. Aber dann wurde mir klar, dass response.raw defekt ist, weil es interne Details der zugrunde liegenden Transportschicht offenlegt, um die sich die Benutzer nicht kümmern sollten.

Die einzige Methode, die sie benötigen sollten, ist raw.read ?

Nun ja - außer dass sich raw.read() je nach den internen Verhandlungen zwischen Client und Server unterschiedlich verhält. Manchmal werden die erwarteten Daten zurückgegeben und manchmal nur komprimierte Bytes.

Grundsätzlich ist response.raw eine nette Funktion, die die meisten Benutzer gerne ignorieren würden und einige Power-User könnten hilfreich sein, während ein komprimierungsunabhängiges response.stream eine Funktion ist, die die meisten Streaming-Benutzer verwenden würden wollen.

+1

+1

Wird dieser Designfehler behoben?

Ich bin mir nicht sicher, wie richtig oder effizient dieser Weg ist, aber für mich funktioniert Folgendes :

>>> 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 : Das ist eine bizarre Sache. response.content ist bereits ein Bytestring, also decodieren Sie hier den Inhalt mit dem, was auch immer Python wählt, und codieren ihn dann neu als utf-8.

Dies ist _kein_ ein Fehler, und es ist ganz sicher nicht der Fehler, den Sie vorgeschlagen haben. Wenn Sie wirklich ein dateiähnliches Objekt benötigen, empfehle ich StringIO und BytesIO.

@Lukasa hat content sollte immer ein Bytestring sein (in Python 3 ist es ein expliziter Bytestring; in Python 2 str == bytes). Das einzige Element, das kein Bytestring ist, ist text .

@kennethreitz irgendwelche Neuigkeiten dazu? Dies ist ein ziemlich schwerwiegender Designfehler und es ist am besten, ihn frühzeitig zu beheben. Je mehr Code geschrieben wird, um ihn zu umgehen, desto teurer wird es für alle.

Dies ist kein Designfehler, es ist nur eine Funktionsanfrage. Und da Anfragen einen Feature-Freeze haben, gehe ich davon aus, dass dies in absehbarer Zeit nicht in Anfragen vorkommen wird (wenn überhaupt) ...

Ich glaube nicht, dass ein seit langem bestehender Designfehler als "fehlendes Feature" bezeichnet wird.
lässt es so einfach verschwinden. Ich habe gehört, dass der Autor darüber nachdenkt
"Requests" als Teil der Python-Stdlib machen. Das wäre gut
Gelegenheit, dies zu beheben.

Ich habe gehört, dass der Autor darüber nachdenkt
"Requests" als Teil der Python-Stdlib machen.

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

Dies ist kein Fehler, sondern eine Funktionsanfrage. Requests macht nichts falsch, es macht einfach nichts, was optional ist. Das ist die Definition eines Features.

Darüber hinaus ist die Vorbereitung auf die stdlib genau der Grund, warum Requests im Feature-Freeze ist. Sobald Requests in der stdlib enthalten ist, wird es sehr schwierig, zeitnahe Fehlerbehebungen vorzunehmen. Wenn das Hinzufügen des neuen Features Fehler hinzufügt oder das Verhalten rückgängig macht, kann die Version in stdlib daher erst in der nächsten Nebenversion behoben werden. Das wäre schlecht.

Marc Schlaich, 19.03.2013 08:41:

Ich habe gehört, dass der Autor darüber nachdenkt
"Requests" als Teil der Python-Stdlib machen.

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

Ich habe es hier gelesen:

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

Stefan

Ich habe bereits erklärt, warum dies ein Design-Bug und kein Feature-Request ist: Die vorhandene API verwendet die falsche Abstraktion und gibt Verhandlungsdetails der Verbindung in den Benutzerbereich, die der Gegenseite ausgeliefert sind, und die der Benutzer daher nicht sollte kümmern müssen. Das macht die aktuelle Unterstützung für das Lesen von Rohdatenströmen schwer zu verwenden. Im Wesentlichen handelt es sich hierbei um eine Anfrage zur Behebung einer defekten Funktion, nicht um eine Anfrage für eine neue Funktion.

Lassen Sie mich das sauber zusammenfassen. Der Fehler besteht darin, dass jede reale Verwendung der Raw-Stream-Reading-Funktion einen Teil der Bibliothek neu implementieren muss, insbesondere den gesamten bedingten Stream-Dekomprimierungsteil, da die Funktion ohne sie nutzlos ist, sobald der Client die Komprimierung zulässt. Wir sprechen hier von Code, der bereits in "Anfragen" vorhanden ist - er wird nur an der falschen Stelle verwendet. Es sollte unterhalb der Rohleseebene verwendet werden, nicht darüber, da der Client nicht kontrollieren kann, ob der Server den Accept-Header berücksichtigt oder nicht. Die Komprimierung sollte ein transparentes Verhandlungsdetail der Verbindung sein, nicht etwas, das jedem Benutzer schadet, der den entsprechenden Header aktiviert.

Ich kann mir keinen Anwendungsfall vorstellen, bei dem der Client an dem komprimierten Stream interessiert wäre, insbesondere wenn er nicht vorhersagen kann, ob der Stream wirklich komprimiert wird oder nicht, da der Server den Wunsch des Clients gerne ignorieren kann. Es ist ein reines Verhandlungsdetail. Aus diesem Grund verwendet das Lesen von Rohdatenströmen die falsche Abstraktion, indem der äußerst unwahrscheinliche Anwendungsfall dem häufigsten vorgezogen wird.

Ich kann. Was wäre beispielsweise, wenn Sie eine große textbasierte Datei herunterladen und diese komprimiert halten möchten? Ich könnte dieser Änderung mit einem neuen 'Design-Bug' mit dem Titel No way to save ursprünglich komprimierte Daten auf der Festplatte folgen.

Diese Idee ist absichtlich banal und dumm, aber ich versuche, einen Punkt zu veranschaulichen, und zwar folgender: Requests ist nicht verpflichtet, jedem genau den Interaktionsmechanismus anzubieten, den er sich wünscht. Tatsächlich würde dies dem Hauptziel von Requests, der Einfachheit der API, direkt zuwiderlaufen. Es gibt eine lange, lange, _lange_ Liste vorgeschlagener Änderungen an Anfragen, gegen die Einspruch erhoben wurde, weil sie die API komplizieren, obwohl sie nützliche Funktionen hinzugefügt haben. Requests zielt nicht darauf ab, urllib2 für alle Anwendungsfälle zu ersetzen, sondern zielt darauf ab, die häufigsten Fälle zu vereinfachen.

In diesem Fall geht Requests davon aus, dass die meisten Benutzer keine dateiähnlichen Objekte wünschen und schlägt daher die folgenden Interaktionen vor:

  • Response.text und Response.content : Sie möchten alle Daten auf einmal.
  • Response.iter_lines() und Response.iter_content() : Sie möchten nicht alle Daten auf einmal.
  • Response.raw : Sie sind mit den anderen beiden Optionen nicht zufrieden, also machen Sie es selbst.

Diese wurden ausgewählt, weil sie überwiegend die gebräuchlichen Verwendungszwecke von Anfragen darstellen. Sie haben gesagt, "die meisten Benutzer interessieren sich sowieso nicht für Content-Iteratoren " und " response.stream ist eine Funktion, die sich die meisten Streaming-Benutzer wünschen würden ". Die Erfahrung mit diesem Projekt führt dazu, dass ich anderer Meinung bin: Sehr viele Leute verwenden die Content-Iteratoren und nicht viele wollen unbedingt dateiähnliche Objekte.

Ein letzter Punkt: Wenn die Komprimierung ein transparentes Verhandlungsdetail der Verbindung sein soll, sollten Sie den entsprechenden Fehler gegen urllib3 melden, das unsere Verbindungslogik behandelt.

Es tut mir leid, dass Sie der Meinung sind, dass Requests für Ihren Anwendungsfall unangemessen ist.

Ich verstehe Ihren Punkt, dass response.raw in der aktuellen Implementierung defekt ist und stimme dem sogar teilweise zu (Sie sollten zumindest in der Lage sein, Komprimierungsdetails zu erhalten, ohne die Header zu analysieren).

Ihr Vorschlag ist jedoch immer noch eine Feature-Anfrage...

@Lukasa
Ich kann nicht wirklich sehen, wie das Einreichen des Fehlers gegen urllib3 die API von Anfragen beheben würde, zumindest nicht ganz allein.

Und ich stimme zu, dass Ihr "Anwendungsfall" erfunden ist. Wie gesagt, wenn der Client die Komprimierung auf der Serverseite nicht positiv steuern kann (und sie deaktiviert, aber nicht zuverlässig aktiviert), ist es nicht so interessant, sich darauf zu verlassen, dass eine komprimierte Datei auf der Festplatte gespeichert werden kann .

@schlamar
Ich stimme zu, dass es als solches gelesen werden kann. Ich versichere Ihnen, dass ich mit allem einverstanden bin, was dieses Problem löst. Wenn das Öffnen eines neuen Tickets erforderlich ist, um dorthin zu gelangen, ist es so.

Wenn das Öffnen eines neuen Tickets erforderlich ist, um dorthin zu gelangen, ist es so.

Ich denke immer noch, dass Kenneth dies aufgrund des Feature-Freeze ablehnen wird.

Ich bin mit allem einverstanden, was dieses Problem löst

  1. Wrap iter_content als dateiähnliches Objekt oder
  2. Analysieren Sie die Header und dekomprimieren Sie response.raw falls erforderlich

Beide Lösungen sind in den Kommentaren oben, letztere von Ihnen gepostet. Warum ist es so ein Problem, dass dies nicht direkt in Anfragen aufgeführt wird?

Lassen Sie uns hier 100% klar sein: Es besteht im Grunde keine Chance, dass dies in die Anfragen gelangt, während es im Feature-Freeze ist. Nichts ist kaputt, die API ist einfach nicht perfekt für Ihre Bedürfnisse. Da nichts kaputt ist, zählt nur, ob Kenneth es will. Anfragen ist keine Demokratie, es ist ein Mann, eine Stimme. Kenneth ist der Mann, er hat die Stimme . Kenneth hat dieses Problem vor 8 Monaten geschlossen, es scheint also ziemlich klar zu sein, dass er es nicht will.

Ich kann nicht wirklich sehen, wie das Einreichen des Fehlers gegen urllib3 die API von Anfragen beheben würde, zumindest nicht ganz allein.

Das Patchen von urllib3, um immer das unkomprimierte Dateiobjekt zurückzugeben, sollte dies von selbst lösen (nicht gesagt, dass dies eine gute Idee ist).

Oh, hier ist Lösung Nummer 3 (ungetestet):

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

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

Interessant - ich wusste noch nicht, dass es das gibt. Das macht es natürlich viel einfacher, das Feature zu umschließen.

Obwohl, funktioniert das tatsächlich? Dh sind die Dekompressoren zustandsbehaftet und inkrementell? Der zweite Aufruf von read(123) gibt beispielsweise nicht mehr den gültigen Anfang einer gzip-Datei zurück.

Obwohl, funktioniert das tatsächlich? Dh sind die Dekompressoren zustandsbehaftet und inkrementell?

Oh, scheint nicht so. Ich habe den Docstring nicht gelesen.

Hier jedoch mein Vorschlag:

  1. Patchen Sie urllib3 so, dass HTTPResponse.read mit amt und decode_content funktioniert.
  2. Machen Sie HTTPResponse._decode_content zu einem öffentlichen Mitglied (Sie können also response.raw.decode_content = True ausführen, anstatt die Methode read patchen).
  3. Lassen Sie die Dekompression in Anfragen vollständig fallen, indem Sie decode_content=True in iter_content

@Lukasa Ich denke, dies wird nicht gegen das Einfrieren der Funktion verstoßen, oder?

@schlamar : Im Prinzip schon. Solange die API unverändert bleibt, _sollten_ interne Änderungen in Ordnung sein, und ich wäre +1 in diesem Fall. Bedenken Sie jedoch, dass ich nicht der BDFL bin, =)

stream_decompress in Anfragen ist sowieso kaputt: #1249

+1

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen