urllib.urlencode no le gustan los valores de Unicode: ¿qué tal esta solución?

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 

Así que mi pregunta es sobre una forma confiable de preparar un objeto para pasarlo a urlencode .

Se me ocurrió esta función en la que simplemente itero a través del objeto y codifico los valores de tipo cadena 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 da como resultado a=1&en=hello&pt=%C3%B3la , listo para pasar a una llamada POST o lo que sea.

Pero mi función encode_object simplemente se ve muy inestable para mí. Por un lado, no maneja objetos nesteds.

Por otra parte, estoy nervioso por eso si statement. ¿Hay algún otro tipo que deba 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!

De hecho deberías estar nervioso. La idea general de que puede tener una mezcla de bytes y texto en alguna estructura de datos es aterradora. Viola el principio fundamental de trabajar con datos de cadena: decodificar en el momento de la entrada, trabajar exclusivamente en Unicode, codificar en el tiempo de salida.

Actualización en respuesta al comentario:

Estás a punto de generar algún tipo de solicitud HTTP. Esto debe ser preparado 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 dictado es realmente desafortunado. Si tiene una mezcla de cadenas de bytes y cadenas Unicode en su dictado, debe tener cuidado. Examinemos lo que 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 "", line 2, in  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 dos últimas pruebas demuestran el problema con urlencode (). Ahora veamos las pruebas de str.

Si insiste en tener una mezcla, al menos debe 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 a urlencode (), que se codificarán en porcentaje, como ‘%’ si es necesario.

Aquí hay una función de conversión sugerida. No muta el dictado de entrada ni lo devuelve (como el suyo); Vuelve 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 por no manejar objetos nesteds es un poco mal dirigida: su código solo funciona con dictados, y el concepto de dictados nesteds realmente no funciona.

 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 "", line 2, in  File "", 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?

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

En Python 3+, urlencode permite especificar la encoding:

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

Parece que es un tema más amplio de lo que parece, especialmente cuando tienes que lidiar con valores de diccionarios más complejos. Encontré 3 maneras de resolver el problema:

  1. Parche urllib.py para incluir el parámetro de encoding:

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

    y reemplace todas las conversiones de str(v) a algo como v.encode(encoding)

    Obviamente no es bueno, ya que es difícilmente redistribuible y aún más difícil de mantener.

  2. Cambie la encoding predeterminada de Python como se describe aquí . El autor del blog describe bastante bien algunos problemas con esta solución y quién sabe cómo más de ellos podrían estar merodeando en las sombras. Así que tampoco me parece bien.

  3. Así que, personalmente, terminé con esta abominación, que codifica todas las cadenas Unicode a cadenas de bytes UTF-8 en cualquier estructura (razonablemente) compleja:

     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 

    Puedes usarlo así: urllib.urlencode(encode_obj(complex_dictionary))

    También para codificar claves, out_dict[k] puede reemplazarse por out_dict[k.encode('utf-8')] , pero fue demasiado para mí.

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 lo hace de manera adecuada me parece muy dependiente del contexto, pero en su código siempre debe saber cuándo usar el objeto Python de Unicode (la representación de Unicode) y cuándo usar el objeto codificado (bytring).

Además, codificar los valores de str es “superfluo”: ¿Cuál es la diferencia entre codificar / decodificar?

No hay nada nuevo que agregar, excepto para señalar que el algoritmo de urlencode no es nada complicado. En lugar de procesar sus datos una vez y luego llamar a urlencode, sería perfectamente correcto 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)) 

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

Incluso puedes deshacerte de if hasattr('items', params): si sabes que no necesitarás manejar listas de 2-tuplas así como también dados.

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:

  • “obtener” puede ser un dict o una lista de pares (clave, valor)
  • Orden no se pierde
  • Los valores pueden ser enteros u otros tipos de datos simples.

Comentarios bienvenidos.

Esta línea funciona bien en mi caso ->

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

gracias @IanCleland y @PavelVlasov

¿Por qué respuestas tan largas?

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