当前位置 主页 > 服务器问题 > Linux/apache问题 >

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

    栏目:Linux/apache问题 时间:2019-12-06 22:05

    写这篇文章的缘由是我使用 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 解码就可以了.