写这篇文章的原因是我使用 reqeusts
库请求接口的时候, 直接使用请求参数里的 json
字段发送数据, 可是服务器没法识别我发送的数据, 排查了很久才知道 requests
内部是使用 json.dumps
将字符串转成 json
的, 而 json.dumps
默认状况下会将 非ASCII
字符转义, 也就是我发送数据中的中文被转义了, 因此服务器没法识别. 这篇文章虽然是 json.dumps
问题的总结, 但也会涉及到 字符编码
问题, 因此就简单先说一下 字符编码
.html
Python
中的字符编码在 Python3
中, 字符
在内存中是使用 Unicode
存储的, 常规的字符使用 两个字节
表示, 一些很生僻的字符就须要 四个字节
. 默认使用 Unicode
存储是什么意思呢, 那就是例子来解释一下, 在 Python Shell
中输入如下字符串 '\u4e2d\u6587'
, 观察其输出:python
In [51]: '\u4e2d\u6587'
Out[51]: '中文'
复制代码
输出的为 中文
两个字. 其实 \u4e2d
和 \u6587
分别表示 中
和 文
的 Unicode
编码(术语称为 码点
)的 十六进制
表示, 在 Python3
中以 \u
开头的字符串被解析为 Unicode
字符, 而后经过其十六进制 码点
解析出具体的字符, 因此 中文
的内存表示即为 \u4e2d\u6587
.json
Unicode
码点标准库提供了 ord
函数获取一个字符的 Unicode
码点, 使用 chr
函数将 码点
转换成 字符
, 下面是示例:bash
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
进行解码的结果一致, 代码以下:spa
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'([\\"]|[^\ -~])')
复制代码
其中 ([\\"]
用于匹配 \\
和 "
, 而 [^\ -~]
表示 \ -~
取反(这里的反斜杠貌似是对空格进行转义, 我不是很理解, 不进行转义依旧能够匹配到), 在 ASCII
表里, 空格字符
对应十进制是 40
, ~
是 176
, 这是全部的 可打印字符
, 取反就是全部编码不在 40 ~ 176
的字符, 因此中文就会被匹配到, 下面为 ASCII表
:
对于匹配到的字符, 会传入回调函数 replace
作转义. replace
函数中的 ESCAPE_DCT
为:
ESCAPE_DCT = {
'\\': '\\\\',
'"': '\\"',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
}
复制代码
先从 ESCAPE_DCT
中获取 制表符
、换行符
等经常使用字符的转义, 若是失败就获取它的 Unicode
码点, 而后判断是否为小于 0x10000
便是否为 两字节
字符(两字节最大为 0xFFFF
) , 若是是就格式化为 Unicode
码, 若是不是就使用 四字节
表示.
因此在使用 requests
时, 若是数据要使用 json
传输而且有 中文
, 那么须要手动将 字典
进行 dump
.