在 Python 中使用 JSON 时需要注意的编码问题
写这篇文章的原因是我在使用reqeusts库请求接口时,直接使用请求参数中的json字段发送数据,但是服务器无法识别我发送的数据,
找了半天发现requests内部使用json.dumps将字符串转成json,json.dumps默认会转义非ASCII字符,即
我发送的数据中的中文被转义了,所以服务器无法识别。这篇文章虽然是对json.dumps问题的总结,但是也会涉及到字符编码问题,所以比较简单
先说一下 字符编码.
Python 中的字符编码
在 Python3 中,字符在内存中以 Unicode 存储。常规字符用两个字节表示,一些非常罕见的字符需要四个字节。默认,
Unicode存储是什么意思,那是举例说明,在Python Shell中输入如下字符串'\u4e2d\u6587',观察其输出:
In [51]: '\u4e2d\u6587'
Out[51]: '中文'
输出是“中文”。实际上,\u4e2d和\u6587分别代表了“中文”和“文”的Unicode编码(称为码位)的十六进制表示。在 Python3
以\u开头的字符串被解析为Unicode字符,然后通过其十六进制码位解析具体字符,所以中文的内存表示为\u4e2d\u6587。
获取字符 Unicode 码点
标准库提供了ord函数获取一个字符的Unicode码位,使用chr函数将码位转换为字符,下面是一个例子:
In [54]: ord('中')
Out[54]: 20013
In [56]: chr(20013)
Out[56]: '中'
输出码点以十进制表示,可以使用如下代码将十进制数格式化为十六进制字符串:
'{0:04x}'.format(20013)
使用 json.dumps
有了前面的铺垫,我们就可以说说json.dumps了。让我们从一个例子开始:
In [121]: json.dumps('中文', ensure_ascii=True)
Out[121]: '"\\u4e2d\\u6587"'
In [122]: json.dumps('中文', ensure_ascii=False)
Out[122]: '"中文"'
可以看到当ensure_ascii为True时,中文被编码成Unicode,为False时可以正常显示,但为什么参数名是ensure_ascii呢?
来看一下 官方文档 对这个参数的解释:
如果 ensure_ascii 是 true (即默认值),输出保证将所有输入的非 ASCII 字符转义。如果 ensure_ascii 是 false,这些字符会
原样输出。
现在有点明白了,当ensure_ascii为True时,如果字符串中有非ASCII字符,就会进行转义。根据结果我们可以知道这个字符被转义为
Unicode编码格式化成字符串,注意“\\u4e2d\\u6587”与“\u4e2d\u6587”不同,前者是长度为12的字符串,后者将是
Python直接解析成中文,长度为2。这是我一开始遇到的问题。直接在网络上传输转义字符串可能无法识别。比如中文被转换
定义为\\u4e2d\\u6587,如果服务器不知道它是转义字符串,那么就是普通字符串,长度为12,肯定会识别错误。
ensure_ascii 设为 False 就不会进行转义, 使用原始字符.
识别转义字符
如果服务端收到数据,发现已经转换,如何识别呢?其实转义后的字符串是先用unicode_escape编码,再用utf-8编码
行解码的结果一致, 代码如下:
In [129]: msg
Out[129]: '中文'
In [130]: msg.encode('unicode_escape').decode('utf-8')
Out[130]: '\\u4e2d\\u6587'
所以识别只需要反向使用utf-8编码,然后使用unicode_escape解码即可。
转义是如何进行的
下面我们来看看json.dumps是如何转义字符的。如果你在json.dumps的源码中仔细调试,你会发现它调用了
JSONEncoder.encode 方法, 而 encode 中的代码片段如下:
if self.ensure_ascii:
return encode_basestring_ascii(o)
else:
return encode_basestring(o)
它会根据 ensure_ascii 的值选择调用函数. 而 encode_basestring_ascii 的值是 (c_encode_basestring_ascii or
py_encode_basestring_ascii),即默认C实现的版本,后面是Python实现的版本。既然有Python版本,当然要看看如何
实现的, py_encode_basestring_ascii 可以直接使用 from json.encoder import py_encode_basestring_ascii 导入, 直接在其内部就可
以调试. 下面是其源码:
def py_encode_basestring_ascii(s):
"""Return an ASCII-only JSON representation of a Python string
"""
def replace(match):
s = match.group(0)
try:
return ESCAPE_DCT[s]
except KeyError:
n = ord(s)
if n < 0x10000:
return '\\u{0:04x}'.format(n)
#return '\\u%04x' % (n,)
else:
# surrogate pair
n -= 0x10000
s1 = 0xd800 | ((n >> 10) & 0x3ff)
s2 = 0xdc00 | (n & 0x3ff)
return '\\u{0:04x}\\u{1:04x}'.format(s1, s2)
return '"' + ESCAPE_ASCII.sub(replace, s) + '"'
从上次的return可以看出,其实就是正则匹配替换然后前后加双引号。 ESCAPE_ASCII的定义如下:
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
其中([\\"]是用来匹配\\和"的,而[^\ -~]表示\ -~是反的(这里的反斜杠好像是对空格进行转义,不是很懂,不是转换仍然可以匹配
to),在ASCII表中,空格符对应十进制的40,~是176,都是可打印的字符,倒过来就是编码不在40到176之间的所有字符,所以中文
就会被匹配到, 下面为 ASCII表:
对于匹配到的字符,会传入回调函数replace进行转义。替换函数中的 ESCAPE_DCT 是:
ESCAPE_DCT = {
'\\': '\\\\',
'"': '\\"',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
}
先从ESCAPE_DCT中获取制表符、换行符等常用字符的转义,如果失败则获取其Unicode码位,然后判断是否小于0x10000
不是,是一个双字节的字符(双字节是 0xFFFF ),如果是,就格式化成Unicode码,如果不是,就用四个字节表示。
总结
所以在使用requests的时候,如果数据需要使用json传输,并且有中文,就需要手动dump字典。
本文为原创文章,版权归知行编程网所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ wb在python中有什么用11/16
- ♥ 一篇文章带你了解Python的协程12/13
- ♥ 如何解决python文件中的中文乱码12/04
- ♥ python 输入成绩求平均分08/16
- ♥ Python静态函数与普通方法的区别12/14
- ♥ 分析python split()方法的用法和示例代码09/29
内容反馈