urllib.urlencode不支持unicode值:有什么解决方法?

49 投票
8 回答
42289 浏览
提问于 2025-04-16 20:17

如果我有一个这样的对象:

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

...那么我可以把它传给 urllib.urlencode,没有问题:

percent_escaped = urlencode(d)
print percent_escaped

但是如果我试着传一个值是 unicode 类型的对象,那就麻烦了:

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

所以我想问的是,有没有可靠的方法来准备一个对象,以便传给 urlencode

我想出了这个函数,我只是遍历对象,并对字符串或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

这个方法似乎有效:

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请求或者其他用途。

但是我的 encode_object 函数看起来真的不太稳妥。首先,它不处理嵌套对象。

其次,我对那个if语句有点担心。还有其他类型我应该考虑吗?

而且像这样比较 type() 和原生对象,算不算好习惯呢?

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

谢谢!

8 个回答

7

这个话题看起来比想象中要复杂,特别是当你需要处理更复杂的字典值时。我找到了解决这个问题的三种方法:

  1. 修改urllib.py,加入编码参数:

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

    并把所有的 str(v) 转换替换成类似 v.encode(encoding) 的形式。

    显然,这种方法不好,因为它几乎无法重新分发,而且维护起来也很困难。

  2. 按照这里的描述,改变Python的默认编码。博客的作者清楚地指出了这个解决方案的一些问题,谁知道还有多少潜在的问题呢?所以我觉得这个方法也不太靠谱。

  3. 所以,我个人最终采用了这个不太优雅的办法,它可以把所有的unicode字符串编码成UTF-8字节字符串,适用于任何(合理)复杂的结构:

    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] 替换成 out_dict[k.encode('utf-8')],不过对我来说,这有点复杂。

10

我之前也遇到过德语中的“变音符号”问题。

解决方法其实很简单:

在Python 3及以上版本中,urlencode可以让你指定编码方式:

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

>>> 'a=1&en=hello&pt=ol%3F'
68

你确实应该感到紧张。因为在某些数据结构中混合使用字节和文本的想法是非常可怕的。这违反了处理字符串数据的基本原则:在输入时解码,专门使用unicode,在输出时编码。

根据评论的更新:

你即将输出某种HTTP请求。这需要准备成字节字符串。如果你的字典中有unicode字符的编码值大于等于128,urllib.urlencode就无法正确准备这个字节字符串,这确实很不幸。如果你的字典中混合了字节字符串和unicode字符串,你需要小心。我们来看看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)

最后两个测试展示了urlencode()的问题。现在我们来看一下str测试。

如果你坚持要混合使用,那么至少要确保str对象是用UTF-8编码的。

'\x80'是可疑的——它不是任何有效的unicode字符串.encode('utf8')的结果。
'\xe2\x82\xac'是可以的;它是u'\u20ac'.encode('utf8')的结果。
'1'也是可以的——所有ASCII字符在输入到urlencode()时都是可以的,如果需要,它会将其百分号编码,比如'%'。

这里有一个建议的转换函数。它不会改变输入的字典,而是返回一个新的字典。如果某个值是str对象但不是有效的UTF-8字符串,它会强制抛出异常。顺便说一下,你担心它无法处理嵌套对象的想法有点偏离主题——你的代码只处理字典,而嵌套字典的概念并不适用。

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

这是输出,使用相同的测试但顺序相反(因为这次麻烦的那个在前面):

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

这样有帮助吗?

撰写回答