urllib.urlencode doesn't like unicode values: how about this workaround?
如果我有这样的东西:
1 | d = {'a':1, 'en': 'hello'} |
…然后我可以把它传给
1 2 | percent_escaped = urlencode(d) print percent_escaped |
但如果我试图传递一个值为
1 2 3 | d2 = {'a':1, 'en': 'hello', 'pt': u'olá'} percent_escaped = urlencode(d2) print percent_escaped # This fails with a UnicodeEncodingError |
因此,我的问题是如何准备一个对象传递给
我想到了这个函数,只需遍历对象并对string或unicode类型的值进行编码:
1 2 3 4 5 | def encode_object(object): for k,v in object.items(): if type(v) in (str, unicode): object[k] = v.encode('utf-8') return object |
这似乎有效:
1 2 3 | d2 = {'a':1, 'en': 'hello', 'pt': u'olá'} percent_escaped = urlencode(encode_object(d2)) print percent_escaped |
然后输出
但我的
另一方面,我对那个if语句感到紧张。还有其他类型的我应该考虑吗?
是否将某物的
1 | type(v) in (str, unicode) # not so sure about this... |
谢谢!
你真的应该紧张。在某些数据结构中混合使用字节和文本的整个想法是可怕的。它违反了处理字符串数据的基本原则:在输入时解码,仅使用Unicode,在输出时编码。
更新以响应评论:
您将要输出某种HTTP请求。这需要准备为一个字节字符串。如果您的dict中有序号大于等于128的Unicode字符,那么urllib.urlencode无法正确准备该字节字符串,这一事实确实令人遗憾。如果您的dict中混合了字节字符串和Unicode字符串,则需要小心。让我们检查一下urlencode()的作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | >>> 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) |
最后两个测试演示了URLEncode()的问题。现在我们来看看str测试。
如果您坚持要混合使用,那么至少应该确保str对象是以utf-8编码的。
'x80'可疑--它不是任何有效的unicode.encode('utf8')字符串的结果。'xe2x82xac'正常;它是u'u20ac'的结果。编码('utf8')。"1"是正常的——所有的ASCII字符在输入到urlencode()时都是正常的,如果需要,它将进行百分比编码,如"%"。
这里有一个建议的转换器功能。它不会改变输入dict并返回它(就像您的一样);它返回一个新的dict。如果值是str对象但不是有效的utf-8字符串,则强制执行异常。顺便说一句,您对它不处理嵌套对象的关注有点误导——您的代码只适用于dict,而嵌套dict的概念并没有真正流行起来。
1 2 3 4 5 6 7 8 9 10 | 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 |
这里是输出,使用相同的测试,以相反的顺序(因为这个讨厌的测试在前面):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | >>> 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 >>> |
这有帮助吗?
我对德语"umlaute"也有同样的问题。解决方案非常简单:
在python 3+中,urlencode允许指定编码:
1 2 3 4 5 6 | from urllib import urlencode args = {} args = {'a':1, 'en': 'hello', 'pt': u'olá'} urlencode(args, 'utf-8') >>> 'a=1&en=hello&pt=ol%3F' |
似乎这是一个比看起来更广泛的话题,尤其是当你必须处理更复杂的字典值时。我找到了3种解决问题的方法:
修补urllib.py以包含编码参数:
1 | def urlencode(query, doseq=0, encoding='ascii'): |
把所有的
显然不好,因为它很难再分配,更难维护。
更改这里描述的默认python编码。这个博客的作者非常清楚地描述了这个解决方案中的一些问题,谁知道这些问题中有多少隐藏在阴影中。所以对我来说也不好。
因此,我个人最终得到了这个可憎的结果,它将所有Unicode字符串编码成任何(合理的)复杂结构中的utf-8字节字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 |
你可以这样使用:
为了编码密钥,可以用
似乎无法将Unicode对象传递给URLENCODE,因此在调用它之前,应该对每个Unicode对象参数进行编码。在我看来,如何以正确的方式进行这项工作非常依赖于上下文,但在代码中,您应该始终知道何时使用Unicode Python对象(Unicode表示)以及何时使用编码对象(字节串)。
另外,对str值进行编码是"多余的":编码/解码之间的区别是什么?
除了指出URLENCODE算法并不复杂之外,没有什么新的需要添加的。与其只处理一次数据然后对其调用URLENCODE,不如做以下事情:
1 2 3 4 5 6 7 8 | 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)) |
查看urllib模块(python 2.6)的源代码,它们的实现不会做更多的工作。有一个可选的特性,其中参数中的值本身是2个元组,它们被转换成单独的键值对,这有时很有用,但是如果您知道不需要,那么上面的特性就可以了。
如果你知道你不需要处理2元组和dict的列表,你甚至可以去掉
我用这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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') |
特征:
- "get"可以是dict或(key,value)对的列表。
- 订单未丢失
- 值可以是整数或其他简单的数据类型。
欢迎反馈。
这条线在我的情况下工作得很好-->
1 | urllib.quote(unicode_string.encode('utf-8')) |
谢谢@iancleland和@pavellasov
为什么答案这么长?