浅析json dump的字符编码问题
说到python json dump的字符编码问题之前,要先了解一个老生常谈的问题,"unicode" 和 "str" 有什么区别
1. unicode 和 str
unicode是通用的字符编码,它被不同的编码方式(如utf-8, gbk...)编码后,变成由不同byte组成的str
Unicode 提供了所有我们需要的字符的空间,但是计算机的传输只能通过bytes 。我们需要一种用 bytes 来表示 Unicode 的方法这样才可以存储和传播他们,这个过程就是encoding
在python中,unicode通过encode转换成str,str通过decode转换成unicode
>>> a = "测试" >>> type(a), a (<type 'str'>, '\xe6\xb5\x8b\xe8\xaf\x95') # a是utf-8编码的str >>> b = a.decode("utf-8") >>> type(b), b (<type 'unicode'>, u'\u6d4b\u8bd5') # b是a用utf-8解码后的unicode >>> c = b.encode("gbk") >>> type(c), c (<type 'str'>, '\xb2\xe2\xca\xd4') # c是b用gbk编码后的str,可以看到,编码后的c和a已经不同了,虽然它在gbk终端下显示出来的仍然是中文"测试"
python2对unicode和str会做一些隐式操作,允许二者混用
当你进行unicode 和 str 拼接的时候,python会对str做decode操作,隐式转换成unicode进行拼接
>>> a = "test" >>> b = u"test" >>> type(a), type(b) (<type 'str'>, <type 'unicode'>) >>> type(a + b) <type 'unicode'> # 拼接后的结果为unicode,因为python帮你完成了b(str)到b(unicode)的转换
python默认用ascii编码来对str做decode,这种转换,在字符串是全英文时没有任何问题;但是当字符串存在中文时,一旦编码不符,这种隐式转换就会报错
>>> a = "测试" >>> b = u"测试" >>> type(a), type(b) (<type 'str'>, <type 'unicode'>) >>> c = a + b Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128) # 这里的报错原因是:"测试"是utf-8终端编码输入的str,在用ascii编码方式做decode时,会出现解码错误
理论上,str是编码后的字符串,只允许做解码(decode);unicode是解码后的字符串,只允许做编码(encode)。但是实际上,python的隐式操作使得二者可以任意编解码
>>> a = "test" >>> type(a) <type 'str'> >>> a.encode("utf-8") # python底层处理为a.decode("ascii").encode("utf-8") 'test' >>> a.decode("utf-8") u'test' >>> b = u"test" >>> type(b) <type 'unicode'> >>> b.encode("utf-8") 'test' >>> b.decode("utf-8") # python底层处理为a.encode("ascii").decode("utf-8") u'test' # 同样,当字符串中存在中文时,这种通过ascii编码方式做的隐式转换,在编码不符时就会报错 >>> a = "测试" >>> type(a) <type 'str'> >>> a.encode("utf-8") Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)
python2这种隐式转换的存在,看起来是让程序员在写程序的时候不用考虑unicode和str的类型;但实际上看看上面演示的那些情况,当你的接口处理过程中存在中文字符,而你又忽略了unicode和str的区别,在python2做隐式转换时,程序就会出错
最安全的做法是,在程序处理返回时,对字符串采用统一的编码;不同编码的str按照各自编码decode成unicode后,再采用统一的编码方式encode成str来进行返回
>>> a = u"测试".encode("utf-8") >>> b = u"测试".encode("gbk") >>> c = (a.decode("utf-8") + b.decode("gbk")).encode("utf-8") >>> print c 测试测试
2. json处理中的字符编码问题
python的官方json包用于做字典<=>字符串之间的转换工作,然而这个转换的过程中,对字符编码的处理上有一些需要额外注意的地方
>>> a = {"data": "测试"}
>>> print json.dumps(a)
{"data": "\u6d4b\u8bd5"}
可以看到,当通过json.dumps()将字典a转换为字符串的过程中,对字典中的元素返回的是unicode的结果("\u6d4b\u8bd5")。我们知道,unicode是通过str解码来的。在调用json.dumps()时,可以指定一个参数encoding
,这个参数默认为utf-8,也就是默认以utf-8编码方式来进行解码,官方包里对encoding的描述如下:
``encoding`` is the character encoding for str instances, default is UTF-8.
因此,如果你需要解析一个gbk编码的字典对象,就需要指定encoding="gbk"
>>> a = {"data": u"测试".encode("gbk")}
>>> print json.dumps(a) # 没有指定编码方式时,默认用utf-8解码,会报错
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/Cellar/python/2.7.13_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.py", line 244, in dumps
return _default_encoder.encode(obj)
File "/usr/local/Cellar/python/2.7.13_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.py", line 207, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/usr/local/Cellar/python/2.7.13_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.py", line 270, in iterencode
return _iterencode(o, 0)
UnicodeDecodeError: 'utf8' codec can't decode byte 0xb2 in position 0: invalid start byte
>>> print json.dumps(a, encoding="gbk") # 指定encoding="gbk"后,可以正常输出json字符串
{"data": "\u6d4b\u8bd5"}
不过,此时输出的json字符串中,中文字符是unicode的,不能正常显示;当你的接口想要返回一个可正常显示的含有中文字符的json字符串时,需要在调用json.dumps()时指定ensure_ascii参数为False
>>> a = {"data": u"测试".encode("gbk")}
>>> print json.dumps(a, encoding="gbk", ensure_ascii=False)
{"data": "测试"}
虽然可以正常显示了,但是这时候引入了一个新的问题
>>> a = {"data": u"测试".encode("gbk")}
>>> b = json.dumps(a, encoding="gbk")
>>> c = json.dumps(a, encoding="gbk", ensure_ascii=False)
>>> print type(b), b
<type 'str'> {"data": "\u6d4b\u8bd5"} # 原始json.dumps()的结果类型为str
>>> print type(c), c
<type 'unicode'> {"data": "测试"} # 设置ensure_ascii后json.dumps()的结果类型为unicode
要知道,当结果中存在中文字符的时候,是需要格外注意字符串类型是unicode还是str的,否则会在程序处理过程中出现问题。比如我们的接口中需要处理一个含有gbk编码元素的字典,然后返回utf-8编码的json结果
>>> a = {"data": u"测试".encode("gbk")}
>>> b = json.dumps(a, encoding="gbk", ensure_ascii=False)
>>> b.decode("gbk").encode("utf-8") # 进行gbk解码,并进行utf-8编码返回
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 10-11: ordinal not in range(128)
# 由于设置ensure_ascii=False时,json.dumps()返回的结果从str变成了unicode;因此当我们对一个unicode的结果进行decode解码时,就会报错
可能有人会说,那我碰到ensure_ascii=False的情况时,既然都知道返回的和原来不一样,是个unicode了;那我直接对它做encode,不按照原来处理str的方法(先decode成unicode,再encode成目标编码的str),不就行了吗?但是事实上并没有那么简单。当我们的原始编码是utf-8,目标编码是gbk时,情况又不一样了
>>> a = {"data": u"测试".encode("utf-8")}
>>> b = json.dumps(a, encoding="utf-8", ensure_ascii=False)
>>> b.encode("gbk")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 10: ordinal not in range(128)
当ensure_ascii=False的时候,json.dumps()返回的结果不是unicode吗?对一个unicdoe的字符串,我们应该是可以encode成任意str的,怎么上面又报错了呢?我们来看一下现在json.dumps()的结果类型
>>> type(b)
<type 'str'>
说好的unicode,在utf-8编码下又变回了str。我们来看下这个ensure_ascii的官方注解
可以看到,json.dumps()当设定ensure_ascii=False
,且使用了encoding
参数时,才会返回unicode。encoding=utf-8
时之所以返回的是str,是因为json.dumps()方法默认的encoding=utf-8
,相当于encoding parameter is not used
,因此仍然保持原str返回。
2.1. 总结
- 当使用json.dumps()处理含有中文字符的字典时,需要格外注意编码问题
- 如果源输入是utf-8编码,直接使用json.dumps(a, ensure_ascii=False)即可,默认encoding=utf-8,返回的就是一个utf-8编码的str
- 如果源输入是其他编码,如gbk,则使用json.dumps(a, ensure_ascii=False, encoding="gbk"),返回的是一个unicode,可以根据需要encode成目标编码的str进行传输
参考链接: