详解Python在使用JSON时需要注意的编码问题


Posted in Python onDecember 06, 2019

写这篇文章的缘由是我使用 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 才能正常显示, 但是这跟 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 中的代码片段如下:

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表:

详解Python在使用JSON时需要注意的编码问题

对于匹配到的字符, 会传入回调函数 replace 做转义. replace 函数中的 ESCAPE_DCT 为:

ESCAPE_DCT = {
  '\\': '\\\\',
  '"': '\\"',
  '\b': '\\b',
  '\f': '\\f',
  '\n': '\\n',
  '\r': '\\r',
  '\t': '\\t',
}

会对常用字符进行转义, 如果失败就获取它的 Unicode 码点, 然后判断是否为小于 0x10000 即是否为 两字节 字符(两字节最大为0xFFFF) , 如果是就格式化为 Unicode 码, 如果不是就使用 四字节 表示.

总结

记得使用 requests 发送 JSON 数据时将中文编码.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python网络编程学习笔记(七):HTML和XHTML解析(HTMLParser、BeautifulSoup)
Jun 09 Python
Python爬虫DOTA排行榜爬取实例(分享)
Jun 13 Python
Python获取本机所有网卡ip,掩码和广播地址实例代码
Jan 22 Python
Python无损音乐搜索引擎实现代码
Feb 02 Python
Django基于ORM操作数据库的方法详解
Mar 27 Python
python操作kafka实践的示例代码
Jun 19 Python
python pyinstaller打包exe报错的解决方法
Nov 02 Python
python使用opencv在Windows下调用摄像头实现解析
Nov 26 Python
Python实现动态给类和对象添加属性和方法操作示例
Feb 29 Python
python和js交互调用的方法
Jun 23 Python
浅谈Python 中的复数问题
May 19 Python
python神经网络 tf.name_scope 和 tf.variable_scope 的区别
May 04 Python
用python求一重积分和二重积分的例子
Dec 06 #Python
解决Numpy中sum函数求和结果维度的问题
Dec 06 #Python
numpy按列连接两个维数不同的数组方式
Dec 06 #Python
使用Python实现分别输出每个数组
Dec 06 #Python
Python 获取numpy.array索引值的实例
Dec 06 #Python
python的json中方法及jsonpath模块用法分析
Dec 06 #Python
python输出数组中指定元素的所有索引示例
Dec 06 #Python
You might like
控制PHP的输出:缓存并压缩动态页面
2013/06/11 PHP
基于Laravel5.4实现多字段登录功能方法示例
2017/08/11 PHP
PHP5.5新特性之yield理解与用法实例分析
2019/01/11 PHP
PHP数组Key强制类型转换实现原理解析
2020/09/01 PHP
选择TreeView控件的树状数据节点的JS方法(jquery)
2010/02/06 Javascript
iScroll中事件点击触发两次解决方案
2015/03/11 Javascript
jQuery获取浏览器类型和版本号的方法
2016/07/05 Javascript
javascript动画之模拟拖拽效果篇
2016/09/26 Javascript
js获取当前时间(昨天、今天、明天)
2016/11/23 Javascript
微信小程序 高德地图SDK详解及简单实例(源码下载)
2017/01/11 Javascript
React学习之事件绑定的几种方法对比
2017/09/24 Javascript
详解如何用VUE写一个多用模态框组件模版
2018/09/27 Javascript
vue 解决移动端弹出键盘导致页面fixed布局错乱的问题
2019/11/06 Javascript
javascript 内存模型实例详解
2020/04/18 Javascript
原生js canvas实现鼠标跟随效果
2020/08/02 Javascript
JavaScript交换变量常用4种方法解析
2020/09/02 Javascript
[04:42]2015国际邀请赛CDEC战队晋级之路
2015/08/13 DOTA
[52:32]完美世界DOTA2联赛PWL S2 Magma vs LBZS 第三场 11.18
2020/11/18 DOTA
Python实现类继承实例
2014/07/04 Python
Python中list初始化方法示例
2016/09/18 Python
Python只用40行代码编写的计算器实例
2017/05/10 Python
Python内置函数delattr的具体用法
2017/11/23 Python
Python多线程编程之多线程加锁操作示例
2018/09/06 Python
python实现QQ邮箱发送邮件
2020/03/06 Python
基于html5 DeviceOrientation 实现微信摇一摇功能
2015/09/25 HTML / CSS
Schecker荷兰:狗狗用品和配件
2019/06/06 全球购物
Moss Bros官网:英国排名第一的西装店
2020/02/26 全球购物
长曲棍球装备:Lacrosse Monkey
2020/12/02 全球购物
春节联欢晚会主持词范文
2014/03/24 职场文书
机电一体化专业求职信
2014/07/22 职场文书
毕业生找工作求职信
2014/08/05 职场文书
婚前保证书范文
2015/02/28 职场文书
超市采购员岗位职责
2015/04/07 职场文书
left join、inner join、right join的区别
2021/04/05 MySQL
关于使用Redisson订阅数问题
2022/01/18 Redis
vue自定义右键菜单之全局实现
2022/04/09 Vue.js