web-dev-qa-db-fra.com

urllib.urlencode n'aime pas les valeurs unicode: que diriez-vous de cette solution de contournement?

Si j'ai un objet comme:

d = {'a':1, 'en': 'hello'}

... alors je peux le passer à urllib.urlencode, pas de problème:

percent_escaped = urlencode(d)
print percent_escaped

Mais si j'essaye de passer un objet avec une valeur de type unicode, game over:

d2 = {'a':1, 'en': 'hello', 'pt': u'olá'}
percent_escaped = urlencode(d2)
print percent_escaped # This fails with a UnicodeEncodingError

Ma question porte donc sur un moyen fiable de préparer un objet à passer à urlencode.

Je suis venu avec cette fonction où j'itère simplement à travers l'objet et encode des valeurs de type chaîne ou unicode:

def encode_object(object):
  for k,v in object.items():
    if type(v) in (str, unicode):
      object[k] = v.encode('utf-8')
  return object

Cela semble fonctionner:

d2 = {'a':1, 'en': 'hello', 'pt': u'olá'}
percent_escaped = urlencode(encode_object(d2))
print percent_escaped

Et cela génère a=1&en=hello&pt=%C3%B3la, Prêt à passer à un appel POST ou autre).

Mais ma fonction encode_object Me semble vraiment fragile. D'une part, il ne gère pas les objets imbriqués.

Pour un autre, je suis nerveux à propos de cette déclaration if. Y a-t-il d'autres types que je devrais prendre en compte?

Et est-ce que comparer la type() de quelque chose à l'objet natif comme cette bonne pratique?

type(v) in (str, unicode) # not so sure about this...

Merci!

48
user18015

Vous devez en effet être nerveux. L'idée que vous pourriez avoir un mélange d'octets et de texte dans une structure de données est horrible. Il viole le principe fondamental de travailler avec des données de chaîne: décoder au moment de l'entrée, travailler exclusivement en unicode, coder au moment de la sortie.

Mise à jour en réponse au commentaire:

Vous êtes sur le point de sortir une sorte de requête HTTP. Cela doit être préparé comme une chaîne d'octets. Le fait que urllib.urlencode ne soit pas capable de préparer correctement cette chaîne d'octets s'il y a des caractères unicode avec ordinal> = 128 dans votre dict est en effet regrettable. Si vous avez un mélange de chaînes d'octets et de chaînes unicode dans votre dict, vous devez être prudent. Examinons ce que fait urlencode ():

>>> import urllib
>>> tests = ['\x80', '\xe2\x82\xac', 1, '1', u'1', u'\x80', u'\u20ac']
>>> for test in tests:
...     print repr(test), repr(urllib.urlencode({'a':test}))
...
'\x80' 'a=%80'
'\xe2\x82\xac' 'a=%E2%82%AC'
1 'a=1'
'1' 'a=1'
u'1' 'a=1'
u'\x80'
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "C:\python27\lib\urllib.py", line 1282, in urlencode
    v = quote_plus(str(v))
UnicodeEncodeError: 'ascii' codec can't encode character u'\x80' in position 0: ordinal not in range(128)

Les deux derniers tests montrent le problème avec urlencode (). Voyons maintenant les tests str.

Si vous insistez pour avoir un mélange, vous devez au moins vous assurer que les objets str sont encodés en UTF-8.

'\ x80' est suspect - ce n'est pas le résultat de any_valid_unicode_string.encode ('utf8').
'\ xe2\x82\xac' est OK; c'est le résultat de u '\ u20ac'.encode (' utf8 ').
'1' est OK - tous les caractères ASCII sont OK à l'entrée de urlencode (), qui sera encodé en pourcentage tel que '%' si nécessaire.

Voici une fonction de convertisseur suggérée. Il ne mute pas le dict d'entrée ni le renvoie (comme le vôtre); il renvoie un nouveau dict. Il force une exception si une valeur est un objet str mais n'est pas une chaîne UTF-8 valide. Soit dit en passant, votre préoccupation de ne pas gérer les objets imbriqués est un peu mal dirigée - votre code ne fonctionne qu'avec des dict, et le concept de dict imbriqués ne vole pas vraiment.

def encoded_dict(in_dict):
    out_dict = {}
    for k, v in in_dict.iteritems():
        if isinstance(v, unicode):
            v = v.encode('utf8')
        Elif isinstance(v, str):
            # Must be encoded in UTF-8
            v.decode('utf8')
        out_dict[k] = v
    return out_dict

et voici la sortie, en utilisant les mêmes tests dans l'ordre inverse (parce que le méchant est à l'avant cette fois):

>>> for test in tests[::-1]:
...     print repr(test), repr(urllib.urlencode(encoded_dict({'a':test})))
...
u'\u20ac' 'a=%E2%82%AC'
u'\x80' 'a=%C2%80'
u'1' 'a=1'
'1' 'a=1'
1 'a=1'
'\xe2\x82\xac' 'a=%E2%82%AC'
'\x80'
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 8, in encoded_dict
  File "C:\python27\lib\encodings\utf_8.py", line 16, in decode
    return codecs.utf_8_decode(input, errors, True)
UnicodeDecodeError: 'utf8' codec can't decode byte 0x80 in position 0: invalid start byte
>>>

Est ce que ça aide?

66
John Machin

J'ai eu le même problème avec l'allemand "Umlaute". La solution est assez simple:

Dans Python 3+, urlencode permet de spécifier l'encodage:

from urllib import urlencode
args = {}
args = {'a':1, 'en': 'hello', 'pt': u'olá'}
urlencode(args, 'utf-8')

>>> 'a=1&en=hello&pt=ol%3F'
10
Saskia Vola

Il semble que ce soit un sujet plus large qu'il n'y paraît, surtout lorsque vous devez gérer des valeurs de dictionnaire plus complexes. J'ai trouvé 3 façons de résoudre le problème:

  1. Patch urllib.py pour inclure le paramètre d'encodage:

    def urlencode(query, doseq=0, encoding='ascii'):
    

    et remplacez toutes les conversions str(v) par quelque chose comme v.encode(encoding)

    Évidemment pas bon, car il est difficilement redistribuable et encore plus difficile à entretenir.

  2. Changer la valeur par défaut Python comme décrit ici . L'auteur du blog décrit assez clairement certains problèmes avec cette solution et qui sait comment d'autres pourraient se cacher dans l'ombre Donc ça ne me semble pas bien non plus.

  3. Donc, personnellement, je me suis retrouvé avec cette abomination, qui code toutes les chaînes unicode en chaînes d'octets UTF-8 dans toute structure (raisonnablement) complexe:

    def encode_obj(in_obj):
    
        def encode_list(in_list):
            out_list = []
            for el in in_list:
                out_list.append(encode_obj(el))
            return out_list
    
        def encode_dict(in_dict):
            out_dict = {}
            for k, v in in_dict.iteritems():
                out_dict[k] = encode_obj(v)
            return out_dict
    
        if isinstance(in_obj, unicode):
            return in_obj.encode('utf-8')
        Elif isinstance(in_obj, list):
            return encode_list(in_obj)
        Elif isinstance(in_obj, Tuple):
            return Tuple(encode_list(in_obj))
        Elif isinstance(in_obj, dict):
            return encode_dict(in_obj)
    
        return in_obj
    

    Vous pouvez l'utiliser comme ceci: urllib.urlencode(encode_obj(complex_dictionary))

    Pour encoder des clés également, out_dict[k] Peut être remplacé par out_dict[k.encode('utf-8')], mais c'était un peu trop pour moi.

7
ogurets

Il semble que vous ne pouvez pas passer un objet Unicode à urlencode, donc, avant de l'appeler, vous devez coder chaque paramètre d'objet unicode. La façon dont vous faites cela correctement me semble très dépendante du contexte, mais dans votre code, vous devez toujours savoir quand utiliser l'objet unicode python objet (la représentation unicode) et quand pour utiliser l'objet encodé (bytestring).

De plus, l'encodage des valeurs str est "superflu": Quelle est la différence entre encoder/décoder?

5
Javier

Rien de nouveau à ajouter sauf pour souligner que l'algorithme urlencode n'a rien de délicat. Plutôt que de traiter vos données une fois, puis d'appeler urlencode dessus, il serait parfaitement correct de faire quelque chose comme:

from urllib import quote_plus

def urlencode_utf8(params):
    if hasattr(params, 'items'):
        params = params.items()
    return '&'.join(
        (quote_plus(k.encode('utf8'), safe='/') + '=' + quote_plus(v.encode('utf8'), safe='/')
            for k, v in params))

En regardant le code source du module urllib (Python 2.6), leur implémentation ne fait pas grand-chose de plus. Il existe une fonctionnalité facultative où les valeurs des paramètres qui sont elles-mêmes 2-tuples sont transformées en paires clé-valeur distinctes, ce qui est parfois utile, mais si vous savez que vous n'en aurez pas besoin, ce qui précède fera l'affaire.

Vous pouvez même vous débarrasser de la if hasattr('items', params): si vous savez que vous n'aurez pas besoin de gérer les listes de 2-tuples ainsi que les dict.

2
ejm

Je l'ai résolu avec cette méthode add_get_to_url():

import urllib

def add_get_to_url(url, get):
   return '%s?%s' % (url, urllib.urlencode(list(encode_dict_to_bytes(get))))

def encode_dict_to_bytes(query):
    if hasattr(query, 'items'):
        query=query.items()
    for key, value in query:
        yield (encode_value_to_bytes(key), encode_value_to_bytes(value))

def encode_value_to_bytes(value):
    if not isinstance(value, unicode):
        return str(value)
    return value.encode('utf8')

Fonctionnalités:

  • "get" peut être un dict ou une liste de paires (clé, valeur)
  • La commande n'est pas perdue
  • les valeurs peuvent être des entiers ou d'autres types de données simples.

Commentaires bienvenus.

1
guettli