json序列化 对双引号不加转义_NumPy的反序列化问题

本文介绍了如何处理因未转义双引号导致的NumPy数组序列化问题,包括使用正则表达式和JSON模块进行字符串转换,以及如何在反序列化时保持array的数据类型。讨论了array的序列化方法,如转化为list或自定义Encoder,并提供了反序列化的不同方法,如遍历处理、定义Decoder和使用后处理钩子。
摘要由CSDN通过智能技术生成

0e738958be00c83f931510e9536d9b9b.png

引言

如果要将下面这个数据序列化后存入磁盘

import numpy as np
data = {
    'n': None, 't': True, 'f': False,
    'l': [1, 2, 3],
    'd': {'a': 1, 'b': 2},
    'np': [np.array([1, 2]), 
           np.array(['a', 'b'])]
}

有些人图省事,直接用str转化为字符串,没有考虑以后怎么还原的问题

s = str(data)
s
# "{'n': None, 't': True, 'f': False, 'l': [1, 2, 3], 'd': {'a': 1, 'b': 2}, 'np': [array([1, 2]), array(['a', 'b'], dtype='<U1')]}"

当我们拿到这样的字符串想要还原python对象时,就会比较棘手。本文将将介绍两种方式对这种字符串进行还原,最后给出更好的序列化方式。本文分为如下几个部分

  • eval法还原
  • json法还原
  • array 的json序列化方法

其中json法还原部分涉及较复杂的正则表达式和json模块的细节与进阶内容,对于想熟悉这两部分操作的读者来说,是个不错的练习题目。

eval法还原

"{'n': None, 't': True, 'f': False, 'l': [1, 2, 3], 'd': {'a': 1, 'b': 2}, 'np': [array([1, 2]), array(['a', 'b'], dtype='<U1')]}"

看到这样的字符串,我最初不知道这个字符串是用str生成的,因此我脑中第一反应是用json来还原,但由于array的存在,这个还原方法有些复杂,所以我开始寻求他法。

这时,我看array([1, 2])觉得很眼熟,就回想什么时候数组会用这种形式展示,这让我想到了str。而str序列化后的字符串,一般可以用eval进行还原,如下所示

s = "{'n': None, 't': True, 'f': False, 'l': [1, 2, 3], 'd': {'a': 1, 'b': 2}}"
eval(s)
# {'n': None, 't': True, 'f': False, 'l': [1, 2, 3], 'd': {'a': 1, 'b': 2}}

但有array,直接这样做就不行。eval处理不了array,所以我们可以用字符串替换方法去掉array部分,变成list再执行eval

import re

def parse_str(s):
    pattern_dtype = r'(?:,s*dtype.*?)*'
    pattern = r'array(([.*?]){})'.format(pattern_dtype)
    s = re.sub(pattern, r'1', s)
    return eval(s)

s = "{'n': None, 't': True, 'f': False, 'l': [1, 2, 3], 'd': {'a': 1, 'b': 2}, 'np': [array([1, 2]), array(['a', 'b'], dtype='<U1')]}"
parse_str(s)

# {'n': None,
#  't': True,
#  'f': False,
#  'l': [1, 2, 3],
#  'd': {'a': 1, 'b': 2},
#  'np': [[1, 2], ['a', 'b']]}

这样做比较简单,但 array 全变成 list 了。

json法还原

json.loads能处理的字符串是像下面这样的

'{"n": null, "t": true, "f": false, "l": [1, 2, 3], "d": {"a": 1, "b": 2}}'

json.loads直接处理本文开头的那个字符串,会有几点问题

  • 单引号需要换成双引号
  • None, True, False都要换
  • 无法识别array

前两条可以用replace换过来

s = "{'n': None, 't': True, 'f': False, 'l': [1, 2, 3], 'd': {'a': 1, 'b': 2}, 'np': [array([1, 2]), array(['a', 'b'], dtype='<U1')]}"

s = s.replace('None', 'null')
s = s.replace('True', 'true')
s = s.replace('False', 'false')
s = s.replace(''', '"')

对于array,它不是像true, null这种特定的符号,也不是列表、字典、字符串等,json不认识它,我们需要在字符串处理过程中将它变成json认识的形式。一种做法是像eval中那样变成 list;但我们这里希望还原后保留 array 的数据类型,因此我们选择将它变成字符串

  • 如果是array([1, 2]),要变成"array([1, 2])"
  • 如果是array(["a", "b"], dtype="<U1"),只在外面加双引号会与里面的双引号冲突,所以需要先将里面的双引号转义,最后应该变成"array(["a", "b"], dtype="<U1")"
def sub_quote(m):
    s = m.group(0) # 匹配到的字符串 array(...)
    s = s.replace('"', '"')
    return '"{}"'.format(s)
s = re.sub('array(.*?)', sub_quote, s)

这里re.sub替换部分传入了一个函数,在函数中又执行了一次替换,这一步是比较有技巧性的。主要逻辑是将array(...)识别出来,然后将括号里的"加双斜杠,而array()之外的"不要动。

目前的s已经可以调用json.loads了,只不过输出的 array 部分是一个字符串。我们希望将它变成真正的 array,这里有三种方法

  • 遍历后处理
  • 定义 Decoder
  • 定义钩子

无论哪一种方法,都需要定义下面这两个函数

def str2array(s):
    '''
    将 array(...) 字符串变成 array 对象
    '''
    pattern_dtype = r'(?:,s*dtypes*=s*"(.*?)")*'
    pattern = r'array(([.*?]){})'.format(pattern_dtype)
    l, dtype = re.findall(pattern, s)[0]
    if dtype:
        return np.array(eval(l), dtype=dtype)
    return np.array(eval(l))

def post_process(o):
    '''
    遍历整个字典,执行 str2array 操作
    '''
    if isinstance(o, str) and re.match('array(.*?)', o):
        return str2array(o)
    elif isinstance(o, dict):
        return {k: post_process(v) for k, v in o.items()}
    elif isinstance(o, list):
        return [post_process(v) for v in o]
    else:
        return o

方式一:遍历后处理

d = json.loads(s)
post_process(d)

方式二:定义 Decoder

class NumpyDecoder(json.JSONDecoder):
    def decode(self, s):
        result = super().decode(s)
        return post_process(result)

json.loads(s, cls=NumpyDecoder)

方式三:传入后处理钩子

json.loads(s, object_hook=post_process)

前两种方法都比较好理解,就是对生成的结果遍历一轮进行变换。设计钩子函数的目的是对json.loads的过程进行干预,每次生成一个 dict,就会传入钩子函数中处理一遍(只有 dict 才会传入),再将生成结果往下传(如果没有指定这个钩子,则直接把这个 dict 往下传)。在这个问题中,钩子这种方式不推荐,一是比较冗余,因为本来只需要从最外面往里遍历一遍就够了,而用钩子的话,里面每个 dict 都又多做了一遍;二是如果 array 不被包含于 dict 中,则处理不到。

完整代码如下

import re
import json
import numpy as np

def array_add_quote(s):
    def sub_quote(m):
        s = m.group(0) # 匹配到的字符串 array(...)
        s = s.replace('"', '"')
        return '"{}"'.format(s)
    return re.sub('array(.*?)', sub_quote, s)

def str2array(s):
    pattern_dtype = r'(?:,s*dtypes*=s*"(.*?)")*'
    pattern = r'array(([.*?]){})'.format(pattern_dtype)
    l, dtype = re.findall(pattern, s)[0]
    if dtype:
        return np.array(eval(l), dtype=dtype)
    return np.array(eval(l))

def post_process(o):
    if isinstance(o, str) and re.match('array(.*?)', o):
        return str2array(o)
    elif isinstance(o, dict):
        return {k: post_process(v) for k, v in o.items()}
    elif isinstance(o, list):
        return [post_process(v) for v in o]
    else:
        return o

def json_loads(s):
    s = s.replace('None', 'null')
    s = s.replace('True', 'true')
    s = s.replace('False', 'false')
    s = s.replace(''', '"')
    s = array_add_quote(s)
    d = json.loads(s)
    return post_process(d)

s = "{'n': None, 't': True, 'f': False, 'l': [1, 2, 3], 'd': {'a': 1, 'b': 2}, 'np': [array([1, 2]), array(['a', 'b'], dtype='<U1')]}"
json_loads(s)

这个做法其实还不是特别完善,因为事先进行字符串替换可能会误伤,比如将某字符串中的False也换掉了;所以如果要进一步完善,就要多设置一些条件来控制。

array 的json序列化方法

从上面繁琐的过程可见用str做序列化有多害人。正常保存 array 数据有如下几种方式

  • 如果是array本身保存,只需要用np.save即可
  • 如果是简单的字典形式{'a': np.array(), 'b': np.array()},可以用scipy.io.savemat
  • 如果是复杂的结构,可以简单粗暴地用pickle进行序列化
  • 如果要转化为信息可见的字符串,可以用json来做序列化,这是本节要讨论的内容

json 结构本身不支持 array,因此我们要将 array 转化为字典等形式,同时不损失信息。

一种简单的方式是将 array 变成 list 来存储。自定义 Encoder 如下所示

import json
import numpy as np

class NumpyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        elif isinstance(obj, np.floating):
            return float(obj)
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        return json.JSONEncoder.default(self, obj)

data = {
    'n': None, 't': True, 'f': False,
    'l': [1, 2, 3],
    'd': {'a': 1, 'b': 2},
    'np': [np.array([1, 2]), 
           np.array(['a', 'b'])]
}
s = json.dumps(data, cls=NumpyEncoder)
s

# '{"n": null, "t": true, "f": false, "l": [1, 2, 3], "d": {"a": 1, "b": 2}, "np": [[1, 2], ["a", "b"]]}'

这里相当于在将一个对象变成字符串前,先进行一个预处理(根据不同类型来执行)。

这个例子中,反序列化可以直接用json.loads,但原本哪个是真list,哪个是array就分不清了。

下面我们换一种 Encode 方式

import json
import numpy as np

class NumpyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.ndarray):
            return dict(__ndarray__=obj.tolist(),
                        dtype=str(obj.dtype))
        return json.JSONEncoder.default(self, obj)

s = json.dumps(data, cls=NumpyEncoder)
s

'{"n": null, "t": true, "f": false, "l": [1, 2, 3], "d": {"a": 1, "b": 2}, "np": [{"__ndarray__": [1, 2], "dtype": "int32"}, {"__ndarray__": ["a", "b"], "dtype": "<U1"}]}'

反序列化过程如下所示

def hook_np(dct):
    if isinstance(dct, dict) and '__ndarray__' in dct:
        return np.array(dct['__ndarray__'], dtype=dct['dtype'])
    return dct

json.loads(s, object_hook=hook_np)

钩子一般是这样用的,专门处理单个的 dict,不像json法还原一节中还要递归。

此外,在查资料的过程中,我还发现了一个JSON tricks库可以用于numpy对象的序列化,感兴趣的读者可以去玩一玩。

参考资料

  • array的json序列化部分参考stackoverflow
  • json法还原中后处理部分参考stackoverflow

专栏信息

专栏主页:python编程

专栏目录:目录

版本说明:软件及包版本说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值