Si tengo un objeto como:

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

... entonces puedo pasarlo a urllib.urlencode, no hay problema:

percent_escaped = urlencode(d)
print percent_escaped

Pero si trato de pasar un objeto con un valor de tipo unicode, el juego termina:

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

Entonces, mi pregunta es sobre una forma confiable de preparar un objeto para pasarlo a urlencode.

Se me ocurrió esta función donde simplemente itero a través del objeto y codifico valores de tipo string o 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

Esto parece funcionar:

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

Y eso genera a=1&en=hello&pt=%C3%B3la, listo para pasar a una llamada POST o lo que sea.

Pero mi función encode_object me parece realmente inestable. Por un lado, no maneja objetos anidados.

Por otro lado, estoy nervioso por esa declaración if. ¿Hay otros tipos que debería tener en cuenta?

¿Y está comparando el type() de algo con el objeto nativo como esta buena práctica?

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

¡Gracias!

49
user18015 26 jun. 2011 a las 01:43

7 respuestas

La mejor respuesta

Deberías estar nervioso. La idea de que podría tener una mezcla de bytes y texto en alguna estructura de datos es horrible. Viola el principio fundamental de trabajar con datos de cadena: decodificar en tiempo de entrada, trabajar exclusivamente en unicode, codificar en tiempo de salida.

Actualización en respuesta al comentario:

Está a punto de generar algún tipo de solicitud HTTP. Esto debe prepararse como una cadena de bytes. El hecho de que urllib.urlencode no sea capaz de preparar adecuadamente esa cadena de bytes si hay caracteres unicode con ordinal> = 128 en su dict es realmente lamentable. Si tiene una mezcla de cadenas de bytes y cadenas unicode en su dict, debe tener cuidado. Examinemos qué hace 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)

Las últimas dos pruebas demuestran el problema con urlencode (). Ahora echemos un vistazo a las pruebas str.

Si insiste en tener una mezcla, debe al menos asegurarse de que los objetos str estén codificados en UTF-8.

'\ x80' es sospechoso: no es el resultado de any_valid_unicode_string.encode ('utf8').
'\ xe2 \ x82 \ xac' está bien; es el resultado de u '\ u20ac'.encode (' utf8 ').
'1' está bien: todos los caracteres ASCII están bien en la entrada de urlencode (), que codificará en porcentaje como '%' si es necesario.

Aquí hay una función de conversión sugerida. No muta el dict de entrada ni lo devuelve (como lo hace el suyo); devuelve un nuevo dict. Fuerza una excepción si un valor es un objeto str pero no es una cadena UTF-8 válida. Por cierto, su preocupación sobre si no maneja objetos anidados está un poco mal dirigida: su código solo funciona con dictos, y el concepto de dictados anidados realmente no vuela.

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

Y aquí está la salida, usando las mismas pruebas en orden inverso (porque la desagradable está al frente esta vez):

>>> 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
>>>

¿Eso ayuda?

67
John Machin 26 jun. 2011 a las 08:16

No hay nada nuevo que agregar, excepto señalar que el algoritmo urlencode no es nada complicado. En lugar de procesar sus datos una vez y luego llamar a urlencode, estaría perfectamente bien hacer algo como:

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))

Mirando el código fuente del módulo urllib (Python 2.6), su implementación no hace mucho más. Hay una característica opcional donde los valores en los parámetros que son 2-tuplas se convierten en pares clave-valor separados, lo que a veces es útil, pero si sabe que no lo necesitará, lo anterior servirá.

Incluso puede deshacerse de if hasattr('items', params): si sabe que no necesitará manejar listas de 2 tuplas y dictados.

2
ejm 17 nov. 2011 a las 11:36

¿Por qué respuestas tan largas?

urlencode(unicode_string.encode('utf-8'))

-4
Pavel Vlasov 27 may. 2012 a las 08:33

Parece que no puede pasar un objeto Unicode a urlencode, por lo que, antes de llamarlo, debe codificar cada parámetro de objeto Unicode. La forma en que hace esto de manera adecuada me parece muy dependiente del contexto, pero en su código siempre debe saber cuándo usar el objeto python unicode (la representación unicode) y cuándo usar el objeto codificado (cadena de bytes).

Además, la codificación de los valores str es "superflua": ¿Cuál es la diferencia entre codificar / decodificar?

5
Community 23 may. 2017 a las 12:18

Lo resolví con este método 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')

Caracteristicas:

  • "get" puede ser un dict o una lista de pares (clave, valor)
  • El orden no se pierde
  • Los valores pueden ser enteros u otros tipos de datos simples.

Comentarios bienvenidos.

1
guettli 25 feb. 2016 a las 11:22

Tuve el mismo problema con el alemán "Umlaute". La solución es bastante simple:

En Python 3+, urlencode permite especificar la codificación:

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
Community 1 dic. 2017 a las 05:22

Esta línea funciona bien en mi caso ->

urllib.quote(unicode_string.encode('utf-8'))

Gracias @IanCleland y @PavelVlasov

-1
fredy kardian 11 jul. 2017 a las 03:07