引言
如果要将下面这个数据序列化后存入磁盘
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编程
专栏目录:目录
版本说明:软件及包版本说明