Requests: aucun moyen de lire le contenu non compressé en tant qu'objet de type fichier

CrĂ©Ă© le 29 fĂ©vr. 2012  Â·  44Commentaires  Â·  Source: psf/requests

Selon la documentation, il existe trois façons de lire le contenu de la rĂ©ponse : .text , .content et .raw . Les deux premiers prennent en compte l'encodage du transfert et dĂ©compressent le flux automatiquement lors de la production de leur rĂ©sultat en mĂ©moire. Cependant, en particulier dans le cas oĂč le rĂ©sultat est volumineux, il n'existe actuellement aucun moyen simple d'obtenir le rĂ©sultat dĂ©compressĂ© sous la forme d'un objet de type fichier, par exemple pour le passer directement dans un analyseur XML ou Json.

Du point de vue d'une bibliothĂšque qui vise Ă  rendre les requĂȘtes HTTP conviviales, pourquoi un utilisateur devrait-il se soucier de quelque chose d'aussi bas niveau que le type de compression du flux qui a Ă©tĂ© nĂ©gociĂ© en interne entre le serveur Web et la bibliothĂšque ? AprĂšs tout, c'est la "faute" de la bibliothĂšque si elle accepte par dĂ©faut un tel flux. Dans cette optique, le stream .raw est un peu trop cru Ă  mon goĂ»t.

Peut-ĂȘtre qu'une quatriĂšme propriĂ©tĂ© comme .stream pourrait fournir un meilleur niveau d'abstraction ?

Commentaire le plus utile

J'ai déjà expliqué pourquoi il s'agit d'un bogue de conception et non d'une demande de fonctionnalité : l'API existante utilise la mauvaise abstraction et diffuse les détails de négociation de la connexion dans l'espace utilisateur qui sont à la merci du site distant, et donc, que l'utilisateur ne doit pas avoir à se soucier. Cela rend le support de lecture de flux brut actuel difficile à utiliser. Il s'agit essentiellement d'une demande de réparation d'une fonctionnalité défaillante, et non d'une demande de nouvelle fonctionnalité.

Tous les 44 commentaires

Response.iter_content

Euh, non, c'est un itérateur. Je demandais un objet de type fichier, c'est-à-dire quelque chose que les processeurs de documents peuvent lire directement.

Il serait assez simple de créer un objet de type fichier avec iter_content

Merci pour la réponse rapide, BTW.

Je suis d'accord. Pourtant, il serait encore plus facile pour requests de fournir cette fonctionnalité. Mon argument est que .raw n'est pas le bon niveau d'abstraction pour la plupart des cas d'utilisation qui souhaitent lire à partir du flux, car il expose les détails du niveau de transfert.

Personnellement, je ne vois pas de cas d'utilisation majeur pour l'itĂ©ration ligne par ligne ou mĂȘme morceau par morceau sur le rĂ©sultat d'une requĂȘte HTTP, mais je vois plusieurs cas d'utilisation majeurs pour l'analyser en tant qu'objet de type fichier, en particulier les formats de rĂ©ponse qui nĂ©cessitent un analyseur de document, tel que HTML, XML, Json, etc.

Notez également qu'il est beaucoup plus facile d'écrire un itérateur qui encapsule un objet de type fichier qu'un objet de type fichier qui encapsule un itérateur.

Je suis venu avec le code suivant. Il gĂšre tous les cas nĂ©cessaires, mais je le trouve assez complexe. C'est pourquoi j'ai dit que je voulais quelque chose comme ça dans le cadre de la bibliothĂšque. Les utilisateurs ne devraient pas avoir Ă  le dĂ©couvrir eux-mĂȘmes.

Je pense que le code Ă  l'intĂ©rieur de models.py des requĂȘtes utilise la mauvaise abstraction ici. Il doit dĂ©compresser le flux brut _avant_ de dĂ©marrer avec sa machinerie d'itĂ©ration, pas pendant l'itĂ©ration. Passer d'un type de fichier Ă  un itĂ©rateur juste pour revenir Ă  un type de fichier est tout simplement stupide. Une seule transformation d'API est plus que suffisante et la plupart des utilisateurs ne se soucieront pas des itĂ©rateurs de contenu de toute façon.

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

Pourquoi ne pas construire l'objet de type fichier à partir de content_iter comme proposé. Cela pourrait ressembler à :

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

Vous voudrez peut-ĂȘtre relire mon commentaire, en particulier le paragraphe qui prĂ©cĂšde le code que j'ai postĂ©.

Oui, mais cette solution est toujours plus propre (et IMO plus facile) que de faire la dĂ©compression Ă  un deuxiĂšme endroit car cela est dĂ©jĂ  intĂ©grĂ© dans les requĂȘtes.

Mais je suis d'accord avec vous en général, un r.file (ou quelque chose comme ça) a beaucoup plus de cas d'utilisation que r.raw . J'aimerais donc que cela soit également inclus dans les demandes. @kennethreitz

"response.stream" me semble ĂȘtre un bon nom.

C'est à ça que sert response.raw :)

C'est aussi ce que j'ai pensé intuitivement quand je l'ai vu. Mais ensuite, j'ai réalisé que response.raw est cassé car il expose des détails internes de la couche de transport sous-jacente dont les utilisateurs ne devraient pas avoir à se soucier.

La seule méthode dont ils devraient avoir besoin est raw.read ?

Eh bien, oui - sauf que raw.read() se comporte différemment selon les négociations internes entre le client et le serveur. Il renvoie parfois les données attendues et parfois il renvoie des octets compressés nus.

Fondamentalement, response.raw est une fonctionnalité agréable que la plupart des utilisateurs ignoreraient volontiers et que certains utilisateurs expérimentés pourraient trouver utile, alors qu'un response.stream indépendant

+1

+1

Ce bug de conception va-t-il ĂȘtre corrigĂ© ?

Je ne sais pas à quel point cette méthode est correcte ou efficace, mais pour moi, ce qui suit fonctionne :

>>> 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 : C'est une chose bizarre à faire. response.content est déjà une chaßne d'octets, donc ce que vous faites ici est de décoder le contenu avec le codec choisi par Python, puis de le ré-encoder en utf-8.

Ce n'est _pas_ un bogue, et ce n'est certainement pas le bogue que vous avez suggéré. Si vous avez vraiment besoin d'un objet de type fichier, je recommande StringIO et BytesIO.

@Lukasa a raison. content doit toujours ĂȘtre une chaĂźne d'octets (en Python 3, c'est une chaĂźne d'octets explicite ; en Python 2 str == octets). Le seul Ă©lĂ©ment qui n'est pas une chaĂźne d'octets est text .

@kennethreitz des nouvelles à ce sujet ? Il s'agit d'un bug de conception assez sérieux et il est préférable de le régler tÎt. Plus le code est écrit pour le contourner, plus il devient coûteux pour tout le monde.

Ce n'est pas un bug de conception, c'est juste une demande de fonctionnalité. Et comme les demandes ont un gel des fonctionnalités, je suppose que cela ne sera pas dans les demandes de sitÎt (voire pas du tout) ...

Je ne pense pas que redéclarer un bug de conception de longue date une "fonctionnalité manquante"
le fait disparaĂźtre si facilement. J'ai entendu dire que l'auteur pense Ă 
faire des "requĂȘtes" une partie de la stdlib Python. ce serait une bonne
opportunité de corriger cela.

J'ai entendu dire que l'auteur pense Ă 
faire des "requĂȘtes" une partie de la stdlib Python.

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

Ce n'est pas un bug, c'est une demande de fonctionnalitĂ©. Requests ne fait rien de mal, il ne fait tout simplement pas quelque chose qui est facultatif. C'est la dĂ©finition mĂȘme d'une fonctionnalitĂ©.

De plus, la prĂ©paration de la stdlib est exactement la raison pour laquelle Requests est en gel de fonctionnalitĂ©s. Une fois que Requests est dans la stdlib, il devient trĂšs difficile de corriger les bogues en temps voulu. En consĂ©quence, si l'ajout de la nouvelle fonctionnalitĂ© ajoute des bogues ou rĂ©gresse le comportement, la version dans stdlib ne peut pas ĂȘtre corrigĂ©e avant la prochaine version mineure. Ce serait mauvais.

Marc Schlaich, 19.03.2013 08:41:

J'ai entendu dire que l'auteur pense Ă 
faire des "requĂȘtes" une partie de la stdlib Python.

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

Je l'ai lu ici :

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

Stéphane

J'ai déjà expliqué pourquoi il s'agit d'un bogue de conception et non d'une demande de fonctionnalité : l'API existante utilise la mauvaise abstraction et diffuse les détails de négociation de la connexion dans l'espace utilisateur qui sont à la merci du site distant, et donc, que l'utilisateur ne doit pas avoir à se soucier. Cela rend le support de lecture de flux brut actuel difficile à utiliser. Il s'agit essentiellement d'une demande de réparation d'une fonctionnalité défaillante, et non d'une demande de nouvelle fonctionnalité.

Permettez-moi de rĂ©sumer cela proprement. Le bogue est que toute utilisation rĂ©elle de la fonctionnalitĂ© de lecture de flux brut devra rĂ©implĂ©menter une partie de la bibliothĂšque, en particulier toute la partie de dĂ©compression conditionnelle de flux, car la fonctionnalitĂ© est inutile sans elle, dĂšs que le client autorise la compression. Nous parlons ici de code qui est dĂ©jĂ  lĂ , dans "requests" - il est simplement utilisĂ© au mauvais endroit. Il doit ĂȘtre utilisĂ© en dessous du niveau de lecture brut, pas au-dessus, car le client ne peut pas contrĂŽler si le serveur respecte l'en-tĂȘte d'acceptation ou non. La compression doit ĂȘtre un dĂ©tail de nĂ©gociation transparent de la connexion, et non quelque chose qui blesse un utilisateur qui active l'en-tĂȘte correspondant.

Je ne peux penser Ă  aucun cas d'utilisation oĂč le client serait intĂ©ressĂ© par le flux compressĂ©, surtout s'il ne peut pas prĂ©dire si le flux sera vraiment compressĂ© ou non, car le serveur peut ignorer avec plaisir le souhait du client. C'est un pur dĂ©tail de nĂ©gociation. C'est pourquoi la lecture de flux bruts utilise la mauvaise abstraction en prĂ©fĂ©rant le cas d'utilisation extrĂȘmement improbable au plus courant.

Je peux. Par exemple, que se passe-t-il si vous téléchargez un fichier texte volumineux et que vous souhaitez le conserver compressé ? Je pourrais suivre ce changement avec un nouveau "bug de conception" intitulé Aucun moyen d'enregistrer les données compressées à l'origine sur le disque .

Cette idĂ©e est intentionnellement banale et stupide, mais j'essaie d'illustrer un point, qui est celui-ci : Requests n'est pas obligĂ© d'offrir Ă  chacun exactement le mĂ©canisme d'interaction qu'il souhaite. En fait, cela irait directement Ă  l'encontre de l'objectif principal de Requests, qui est la simplicitĂ© de l'API. Il existe une longue, longue, _longue_ liste de modifications proposĂ©es aux demandes qui ont fait l'objet d'une objection car elles compliquent l'API, mĂȘme si elles ajoutent des fonctionnalitĂ©s utiles. Requests ne vise pas Ă  remplacer urllib2 pour tous les cas d'utilisation, il vise Ă  simplifier les cas les plus courants.

Dans ce cas, Requests suppose que la plupart des utilisateurs ne veulent pas d'objets de type fichier et propose donc les interactions suivantes :

  • Response.text et Response.content : Vous voulez toutes les donnĂ©es en une seule fois.
  • Response.iter_lines() et Response.iter_content() : Vous ne voulez pas toutes les donnĂ©es en une seule fois.
  • Response.raw : Vous n'ĂȘtes pas satisfait des deux autres options, alors faites-le vous-mĂȘme.

Ceux-ci ont Ă©tĂ© choisis parce qu'ils reprĂ©sentent en trĂšs grande majoritĂ© les utilisations courantes des requĂȘtes. Vous avez dit " la plupart des utilisateurs ne se soucieront pas des itĂ©rateurs de contenu de toute façon " et " response.stream est une fonctionnalitĂ© que la plupart des utilisateurs de streaming voudraient ". L'expĂ©rience sur ce projet m'amĂšne Ă  ne pas ĂȘtre d'accord : un grand nombre de personnes utilisent les itĂ©rateurs de contenu, et peu veulent dĂ©sespĂ©rĂ©ment des objets de type fichier.

Un dernier point : si la compression doit ĂȘtre un dĂ©tail de nĂ©gociation transparent de la connexion, alors vous devez signaler le bogue appropriĂ© contre urllib3, qui gĂšre notre logique de connexion.

Je suis désolé que vous ayez l'impression que Requests est inapproprié pour votre cas d'utilisation.

Je comprends que response.raw est cassĂ© dans l'implĂ©mentation actuelle et mĂȘme partiellement d'accord avec cela (vous devriez au moins pouvoir obtenir les dĂ©tails de la compression sans analyser les en-tĂȘtes).

Cependant, votre proposition est toujours une demande de fonctionnalité...

@Lukasa
Je ne vois pas vraiment comment le dĂ©pĂŽt du bogue contre urllib3 corrigerait l'API des requĂȘtes, du moins pas tout seul.

Et je suis d'accord que votre "cas d'utilisation" est inventé. Comme je l'ai dit, si le client ne peut pas contrÎler positivement la compression cÎté serveur (et qu'il la désactive, mais ne l'active pas de maniÚre fiable), alors compter sur lui pour pouvoir enregistrer un fichier compressé sur le disque n'est, eh bien, pas si intéressant .

@schlamar
Je suis d'accord qu'il peut ĂȘtre lu comme tel. Je vous assure que je suis d'accord avec tout ce qui rĂ©sout ce problĂšme. Si l'ouverture d'un nouveau billet est nĂ©cessaire pour s'y rendre, qu'il en soit ainsi.

Si l'ouverture d'un nouveau billet est nécessaire pour s'y rendre, qu'il en soit ainsi.

Je pense toujours que Kenneth rejettera cela en raison du gel des fonctionnalités.

Je suis d'accord avec tout ce qui résout ce problÚme

  1. Enveloppez iter_content tant qu'objet de type fichier ou
  2. Analysez les en-tĂȘtes et dĂ©compressez response.raw si nĂ©cessaire

Les deux solutions sont dans les commentaires ci-dessus, la derniÚre postée par vous. Pourquoi est-ce un tel problÚme que cela ne figure pas directement dans les demandes ?

Soyons clairs à 100 % ici : il n'y a pratiquement aucune chance que cela entre dans les demandes pendant le gel des fonctionnalités. Rien n'est cassé, l'API n'est tout simplement pas parfaite pour vos besoins. Parce que rien n'est cassé, la seule chose qui compte est de savoir si Kenneth le veut. Requests n'est pas une démocratie, c'est un homme une voix. Kenneth est l'homme, il a le vote . Kenneth a fermé ce problÚme il y a 8 mois, il semble donc assez clair qu'il n'en veut pas.

Je ne vois pas vraiment comment le dĂ©pĂŽt du bogue contre urllib3 corrigerait l'API des requĂȘtes, du moins pas tout seul.

Patcher urllib3 pour toujours renvoyer l'objet-fichier non compressĂ© devrait rĂ©soudre ce problĂšme par lui-mĂȘme (on ne dit pas que c'est une bonne idĂ©e).

Oh, voici la solution numéro 3 (non testée) :

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

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

Intéressant - je ne savais pas que cela existait maintenant. Cela rend beaucoup plus facile d'envelopper la fonctionnalité, bien sûr.

Cependant, cela fonctionne-t-il réellement ? C'est-à-dire que les décompresseurs sont dynamiques et incrémentiels ? Le deuxiÚme appel à read(123) ne renverra plus le début valide d'un fichier gzip, par exemple.

Cependant, cela fonctionne-t-il réellement ? C'est-à-dire que les décompresseurs sont dynamiques et incrémentiels ?

Oh, ça n'a pas l'air d'ĂȘtre le cas. Je n'ai pas lu la docstring.

Cependant, voici ma proposition :

  1. Patch urllib3 pour que HTTPResponse.read fonctionne avec amt et decode_content simultanément.
  2. Faites de HTTPResponse._decode_content un membre public (vous pouvez donc faire response.raw.decode_content = True au lieu de patcher la méthode read ).
  3. Supprimez complĂštement la dĂ©compression dans les requĂȘtes en utilisant decode_content=True dans iter_content

@Lukasa Je pense que cela ne violera pas le gel des fonctionnalités, n'est-ce pas ?

@schlamar : En principe, bien sĂ»r. Tant que l'API reste inchangĂ©e, les changements internes _devraient_ ĂȘtre ok, et je serais +1 sur celui-ci. Cependant, gardez Ă  l'esprit que je ne suis pas le BDFL, =)

stream_decompress dans les requĂȘtes est cassĂ© de toute façon : #1249

+1

Cette page vous a été utile?
0 / 5 - 0 notes