urllib.urlencode不支持unicode值:有什么解决方法?
如果我有一个这样的对象:
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 个回答
这个话题看起来比想象中要复杂,特别是当你需要处理更复杂的字典值时。我找到了解决这个问题的三种方法:
修改urllib.py,加入编码参数:
def urlencode(query, doseq=0, encoding='ascii'):
并把所有的
str(v)
转换替换成类似v.encode(encoding)
的形式。显然,这种方法不好,因为它几乎无法重新分发,而且维护起来也很困难。
按照这里的描述,改变Python的默认编码。博客的作者清楚地指出了这个解决方案的一些问题,谁知道还有多少潜在的问题呢?所以我觉得这个方法也不太靠谱。
所以,我个人最终采用了这个不太优雅的办法,它可以把所有的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')]
,不过对我来说,这有点复杂。
我之前也遇到过德语中的“变音符号”问题。
解决方法其实很简单:
在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'
你确实应该感到紧张。因为在某些数据结构中混合使用字节和文本的想法是非常可怕的。这违反了处理字符串数据的基本原则:在输入时解码,专门使用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
>>>
这样有帮助吗?