关于python:urllib.urlencode不喜欢unicode值:这个解决方法怎么样?

urllib.urlencode doesn't like unicode values: how about this workaround?

如果我有这样的东西:

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

…然后我可以把它传给urllib.urlencode,没问题:

1
2
percent_escaped = urlencode(d)
print percent_escaped

但如果我试图传递一个值为unicode的对象,游戏结束:

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

因此,我的问题是如何准备一个对象传递给urlencode

我想到了这个函数,只需遍历对象并对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

然后输出a=1&en=hello&pt=%C3%B3la,准备传递到一个post call或者其他什么。

但我的encode_object功能在我看来确实很不稳定。首先,它不处理嵌套对象。

另一方面,我对那个if语句感到紧张。还有其他类型的我应该考虑吗?

是否将某物的type()与本机对象进行比较,如这种良好做法?

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

    把所有的str(v)转换换成类似v.encode(encoding)的东西。

    显然不好,因为它很难再分配,更难维护。

  • 更改这里描述的默认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

    你可以这样使用:urllib.urlencode(encode_obj(complex_dictionary))

    为了编码密钥,可以用out_dict[k.encode('utf-8')]替换out_dict[k],但对我来说有点太多了。


  • 似乎无法将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的列表,你甚至可以去掉if hasattr('items', params):


    我用这个add_get_to_url()方法解决了它:

    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


    为什么答案这么长?

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