详解Python在使用JSON时需要注意的编码问题
写这篇文章的缘由是我使用reqeusts库请求接口的时候,直接使用请求参数里的json字段发送数据,但是服务器无法识别我发送的数据,排查了好久才知道requests内部是使用json.dumps将字符串转成json的,而json.dumps默认情况下会将非ASCII字符转义,也就是我发送数据中的中文被转义了,所以服务器无法识别.这篇文章虽然是json.dumps问题的总结,但也会涉及到字符编码问题,所以就简单先说一下字符编码.
Python中的字符编码
在Python3中,字符在内存中是使用Unicode存储的,常规的字符使用两个字节表示,一些很生僻的字符就需要四个字节.默认使用Unicode存储是什么意思呢,那就是例子来解释一下,在PythonShell中输入以下字符串'\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才能正常显示,但是这跟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到底是怎么对字符进行转义的.在json.dumps源码中仔细调试的话会发现,它调用的是JSONEncoder.encode方法,而encode中的代码片段如下:
ifself.ensure_ascii: returnencode_basestring_ascii(o) else: returnencode_basestring(o)
它会根据ensure_ascii的值选择调用函数.而encode_basestring_ascii的值是(c_encode_basestring_asciiorpy_encode_basestring_ascii),也就是默认是用C实现的版本,其次使用Python实现的版本,既然有Python版本,当然要看一下是怎么实现的,py_encode_basestring_ascii可以直接使用fromjson.encoderimportpy_encode_basestring_ascii导入,直接在其内部就可以调试.下面是其源码:
defpy_encode_basestring_ascii(s): """ReturnanASCII-onlyJSONrepresentationofaPythonstring """ defreplace(match): s=match.group(0) try: returnESCAPE_DCT[s] exceptKeyError: n=ord(s) ifn<0x10000: return'\\u{0:04x}'.format(n) #return'\\u%04x'%(n,) else: #surrogatepair 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'([\\"]|[^\-~])')
其中([\\"]用于匹配\\和",而[^\-~]表示\-~取反(这里的反斜杠貌似是对空格进行转义,我不是很理解,不进行转义依旧可以匹配到),在ASCII表里,空格字符对应十进制是40,~是176,这是所有的可打印字符,取反就是所有编码不在40~176的字符,所以中文就会被匹配到,下面为ASCII表:
对于匹配到的字符,会传入回调函数replace做转义.replace函数中的ESCAPE_DCT为:
ESCAPE_DCT={ '\\':'\\\\', '"':'\\"', '\b':'\\b', '\f':'\\f', '\n':'\\n', '\r':'\\r', '\t':'\\t', }
会对常用字符进行转义,如果失败就获取它的Unicode码点,然后判断是否为小于0x10000即是否为两字节字符(两字节最大为0xFFFF),如果是就格式化为Unicode码,如果不是就使用四字节表示.
总结
记得使用requests发送JSON数据时将中文编码.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。