详解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读写txt文本文件的操作方法全解析
Jun 26 Python
对python实时得到鼠标位置的示例讲解
Oct 14 Python
python 定时器,轮询定时器的实例
Feb 20 Python
python sort、sort_index方法代码实例
Mar 28 Python
Opencv-Python图像透视变换cv2.warpPerspective的示例
Apr 11 Python
python中pip的使用和修改下载源的方法
Jul 08 Python
Tensorflow实现神经网络拟合线性回归
Jul 19 Python
Python使用贪婪算法解决问题
Oct 22 Python
python3 xpath和requests应用详解
Mar 06 Python
Django模板之基本的 for 循环 和 List内容的显示方式
Mar 31 Python
解决pyinstaller打包运行程序时出现缺少plotly库问题
Jun 02 Python
Python中字符串对象语法分享
Feb 24 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往XML中添加节点的方法
2015/03/12 PHP
php运行报错Call to undefined function curl_init()的最新解决方法
2016/11/20 PHP
PHP实现蛇形矩阵,回环矩阵及数字螺旋矩阵的方法分析
2017/05/29 PHP
php微信开发之关键词回复功能
2018/06/13 PHP
Prototype的Class.create函数解析
2011/09/22 Javascript
12款经典的白富美型—jquery图片轮播插件—前端开发必备
2013/01/08 Javascript
ie下jquery.getJSON的缓存问题的处理方法
2013/03/29 Javascript
PhotoShop给图片自动添加边框及EXIF信息的JS脚本
2015/02/15 Javascript
JavaScript驾驭网页-CSS与DOM
2016/03/24 Javascript
js实现页面a向页面b传参的方法
2016/05/29 Javascript
jquery checkbox无法用attr()二次勾选问题的解决方法
2016/07/22 Javascript
JQuery ZTree使用方法详解
2017/01/07 Javascript
react.js CMS 删除功能的实现方法
2017/04/17 Javascript
jquery+css实现下拉列表功能
2017/09/03 jQuery
Angular 4根据组件名称动态创建出组件的方法教程
2017/11/01 Javascript
JavaScript或jQuery 获取option value值方法解析
2020/05/12 jQuery
[52:09]2014 DOTA2华西杯精英邀请赛 5 25 NewBee VS DK第二场
2014/05/26 DOTA
python通过装饰器检查函数参数数据类型的方法
2015/03/13 Python
Python 将RGB图像转换为Pytho灰度图像的实例
2017/11/14 Python
Flask配置Cors跨域的实现
2019/07/12 Python
PyTorch中 tensor.detach() 和 tensor.data 的区别详解
2020/01/06 Python
python orm 框架中sqlalchemy用法实例详解
2020/02/02 Python
python安装cx_Oracle和wxPython的方法
2020/09/14 Python
CSS3制作苹果风格键盘特效
2015/02/26 HTML / CSS
HTML5中input输入框默认提示文字向左向右移动的示例代码
2020/09/10 HTML / CSS
米兰网婚纱礼服法国网上商店:Milanoo法国
2016/08/20 全球购物
巴塞罗那观光通票:Barcelona Pass
2019/10/30 全球购物
药剂专业个人求职信范文
2014/04/29 职场文书
廉洁家庭事迹材料
2014/05/15 职场文书
关于青春的演讲稿800字
2014/08/22 职场文书
个人四风对照检查材料
2014/09/26 职场文书
委托公证书格式
2015/01/26 职场文书
销售员自我评价
2015/03/11 职场文书
《只有一个地球》教学反思
2016/02/16 职场文书
Python调用腾讯API实现人脸身份证比对功能
2022/04/04 Python
面试官问我Mysql的存储引擎了解多少
2022/08/05 MySQL