9.4 使用JSON进行转储和加载
JSON是什么?摘自www.json.org网页中的一段描述:
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScript Programming Language,Standard ECMA-262 3rd Edition-December 1999的一个子集。JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C、C++、C#、Java、JavaScript、Perl和Python等)。这些属性使JSON成为理想的数据交换语言。
这种格式被广泛地用于各种语言和框架,而且像 CouchDB 这样的数据库会直接保存JSON对象,简化了应用程序间数据的转化。JSON文档具有模糊查找的优势,在这点上,和Python中的list和dict是相似的。它们都具有很强的可读性,并且修改起来很容易。
json模块可以与Python中内置的类型有效地工作。可它不能与自定义的类一起工作,除非做一些额外的工作。接下来会介绍有关这些扩展的技巧,对于如下几种Python类型,都对应了JSON中所使用的javascript类型。Python类型 | JSON |
|---|---|
dict | object |
list, tuple | array |
str | string |
int, float | number |
True | true |
False | false |
None | null |
import json以下是输出:
print( json.dumps(travel.asdict(), indent=4) )
{以上输出演示了不同的对象是如何从Python对象转化为JSON格式的。以上代码优雅的部分是,Python对象被写成了标准化的格式,可以与其他应用共享,也可以将它们写入磁盘文件并保存。此外,JSON这种数据表现形式也有几点不方便的地方。 - 如果必须将Python对象重写为字典,应该提供一种更好的转换方式,不必额外地创建字典对象。 - 当我们加载这个JSON数据时,并不能够轻易地恢复之前的Blog和Post对象。当我们使用json.load()时,并不会得到Blog或Post对象,仅仅能够得到dict和集合对象。在创建Blog和Post对象时,我们需要额外的信息。 - 对于对象内dict中的一些值我们并不希望保存,例如Post中下划线的文字。 除了内置的JSON编码外,还需要做些更复杂的工作。 9.4.1 在类中支持JSON 为了正确地支持JSON,在使用类之前,需要先考虑编码与解码。为了将对象编码为JSON,需要提供一个函数,将对象转换为Python中的基本类型。这个函数被称为default函数,为已知类的对象提供了默认的编码方式。 为了将对象从JSON中解码,需要提供一个函数,用于将由Python基本类型组成的字典转换为一个对象。这个函数被称为object hook函数,用于将dict转换为一个自定义对象。 json模块文档中包含了如何使用类的提示。Python文档包含了一个对JSON-RPC第1版本的参考引用。可以参见http://json-rpc.org/wiki/specification。这个建议是将自定义类的对象编码为字典形式,如下所示。
"entries": [
{
"title": "Hard Aground",
"underline": "——————",
"tagtext": "#RedRanger #Whitby42 #ICW",
"rsttext": "Some embarrassing revelation. Including \
u2639 and \u2693",
"date": "2013-11-14 17:25:00"
},
{
"title": "Anchor Follies",
"underline": "———————",
"tagtext": "#RedRanger #Whitby42 #Mistakes",
"rsttext": "Some witty epigram. Including < & >
characters.",
"date": "2013-11-18 15:30:00"
}
],
"title": "Travel"
}
{"jsonclass": ["class name", [param1,…]] }与"jsonclass"值相关的值包含了两项:类名称和所需的参数列表,它们用于创建类的实例。还可以包含更多的功能,但都与Python无关。 为了将一个对象从JSON字典中解码,作为一种提示,我们可以查找"jsonclass"键,用于创建自定义类中的其中一个,不是Python内置的对象。类名可被映射为类对象,而参数序列可用于创建实例。 当讨论更复杂的JSON编码器(例如其中一个内置于Django Web框架)时,可以看到它们为自定义类提供了一些更复杂的编码。它们包括了类、数据库的主键以及属性值。这些规则将表现为一些简单的函数,并以插件的形式加入JSON的编码与解码的函数中。 9.4.2 自定义JSON编码 类的提示可以提供3部分信息:一个class键,作为目标类的命名;args键,将提供一个位置参数值的序列;一个kw值,将提供一个关键字参数值的字典,包含了init()的所有选项。以下是这种设计的编码器的一个实现示例。
def blogencode( object ):这个函数演示了对3个类对象编码操作的两种不同风格。 - 将datetime.datetime对象编码为一个字典,其中包含了独立的字段值。 - 将Post对象编码为一个字典,也包含了独立的字段值。 - 将Blog实例编码为由标题和文章组成的一个序列。 如果不能处理这个类,将使用默认的编码器进行编码。这将能够有效地处理内置的类。可以使用这个函数来像如下这样来编码。
if isinstance(object, datetime.datetime):
return dict(
class= "datetime.datetime",
args= [],
kw= dict(
year= object.year,
month= object.month,
day= object.day,
hour= object.hour,
minute= object.minute,
second= object.second,
)
)
elif isinstance(object, Post):
return dict(
class= "Post",
args= [],
kw= dict(
date= object.date,
title= object.title,
rsttext= object.rsttext,
tags= object.tags,
)
)
elif isinstance(object, Blog):
return dict(
class= "Blog",
args= [
object.title,
object.entries,
],
kw= {}
)
else:
return json.JSONEncoder.default(o)
text= json.dumps(travel, indent=4, default=blogencode)为了调用json.dumps()函数,提供了我们的函数,blogencode()作为default=关键字的参数。这个函数由JSON编码器用来决定对象的编码方式。这个编码器的使用导致JSON对象的结构将如下代码所示。
{我们拿掉了第2条博客记录,因为输出结果太长了。一个Blog对象由dict完成封装,它提供了类和两个位置参数值。相似地,Post和datetime对象的类名和关键字参数值也被封装了起来。 9.4.3 自定义JSON解码 为了对一个JSON对象进行解码,我们需要在JSON结构的解析上下工夫。自定义类的对象被编码为简单的dicts。这意味着每个由JSON解码器解码的dict都可能是自定义类中的一个,或者它只是一个dict。 JSON解码器中“对象钩子”是一个被dict调用的函数,用于检验是否被正确地表达成了自定义对象。如果dict没有被hook函数识别,那么它仅是一个字典并且应当被直接返回。以下是一个对象钩子函数。
"args": [
"Travel",
[
{
"args": [],
"kw": {
"tags": [
"#RedRanger",
"#Whitby42",
"#ICW"
],
"rsttext": "Some embarrassing revelation.
Including \u2639 and \u2693",
"date": {
"args": [],
"kw": {
"minute": 25,
"hour": 17,
"day": 14,
"month": 11,
"year": 2013,
"second": 0
},
"class": "datetime.datetime"
},
"title": "Hard Aground"
},
"class": "Post"
},
.
.
.
"_kw": {},
"__class": "Blog"
}
def blog_decode( some_dict ):每当这个函数被调用时,它检查所有用于编码的键。如果有3个键存在,则调用这个函数,并传入它的参数和关键字。可使用对象钩子完成JSON对象的解析,如下代码所示。
if set(some_dict.keys()) == set( ["__class", "__args", "
kw"] ):
class= eval(somedict['class'])
return class( somedict['_args'], *somedict['kw'] )
else:
return somedict
blog_data= json.loads(text, object_hook= blog_decode)这将完成JSON格式文本块的解码,使用了blog_decode()函数来将dict转换为适当的Blog和Post对象。 9.4.4 安全性和eval() 一些程序员会反对在blog_decode()函数中使用eval()函数,声称这是一个普遍的安全问题。认为eval()是一个普遍问题的说法是愚蠢的。如果有恶意的天才程序员(Evil Genius Programmer,EGP)将恶意代码写入对象的JSON表达式中,这的确是一个潜在的安全问题。一个EGP是可以获取Python源代码的,但为什么仅仅是折腾JSON文件,而不去直接编辑Python源代码? 实际上,我们需要关注JSON文件在网络上的传输过程;这是一个实际的安全性问题。然而,这个过程一般不会注意到eval()。 有时,一个文件在网络传输过程中可能被中间人篡改成为不可靠的文件,对于这样的场景需要制定一些规则。这样的话,每当一个JSON文件在通过一个Web接口时就需要被验证,它有可能是一个不可靠的服务器代理。SSL通常是解决这类问题的首选方案。 如果有必要,可以将eval()替换为一个字典,完成从名字到类的映射。我们可以将eval(some_dict['__class'])改为{"Post":Post, "Blog":Blog, "datetime. datetime":datetime.datetime:
}[somedict['_class']]
这样就可以避免当JSON文件传输时使用的是非SSL加密的连接。这个做法也需要增加相应的维护成本,当应用的设计改变时,这里的映射也要改变。
9.4.5 重构编码函数
理想情况下,我们会希望重构编码函数,为了提高对每个定义的类进行编码的职责的内聚性。我们当然不希望将大量的编码规则分布在不同的函数中。
如果要修改类库中类的编码行为,例如datetime,就需要在应用程序中对datetime. datetime进行扩展。这样一来,就需要确定在我们的应用中使用了扩展版本而不是类库版本,而完全避免使用内置的datetime类并不是非常容易。通常情况下,需要在自定义与类库之间找到一个平衡点。以下是两个类的扩展,用于创建带有JSON编码能力的类。可以为Blog添加一个特性。
@property
def json( self ):
return dict( class= self.class.name,
kw= {},
_args= [ self.title, self.entries ]
)
这个特性被用来提供解码函数所使用的初始化参数。可以向Post添加这两个特性。
@property
def json( self ):
return dict(
class= self.class.name,
kw= dict(
date= self.date,
title= self.title,
rsttext= self.rst_text,
tags= self.tags,
),
__args= []
)
对于 Blog,这个特性将用来提供初始化参数。它们会被解码函数使用,可以修改编码器,使它更简洁一些,以下是修改后的版本。
def blogencode2( object ):
if isinstance(object, datetime.datetime):
return dict(
class= "datetime.datetime",
args= [],
__kw= dict(
year= object.year,
month= object.month,
day= object.day,
hour= object.hour,
minute= object.minute,
second= object.second,
)
)
else:
try:
encoding= object._json()
except AttributeError:
encoding= json.JSONEncoder.default(o)
return encoding
你或许纠结于是否要使用类库中的datetime模块。在本例中,选择不引入子类,而是将编码作为一个特例。
9.4.6 日期字符串的标准化
日期格式化并没有使用被广泛使用的ISO标准文本格式的日期。为了更好地与其他语言兼容,应当用标准字符串对datetime对象进行适当的编码,并能够解析一个标准字符串。
因为把日期当作了特例,从扩展性上来说,这样做是明智的。无需对编码和解码进行太多的改动就能够完成。如下是对编码改动后的一种实现:
if isinstance(object, datetime.datetime):
fmt= "%Y-%m-%dT%H:%M:%S"
return dict(
class= "datetime.datetime.strptime",
args= [ object.strftime(fmt), fmt ],
kw= {}
)
经过编码的输出中包含了静态方法datetime.datetime.strptime()并提供了与datetime编码的参数,以及用于解码的格式。一篇文章的输出可能如下代码段所示。
{
"args": [],
"class": "PostJ",
"kw": {
"title": "Anchor Follies",
"tags": [
"#RedRanger",
"#Whitby42",
"#Mistakes"
],
"rsttext": "Some witty epigram.",
"date": {
"args": [
"2013-11-18T15:30:00",
"%Y-%m-%dT%H:%M:%S"
],
"class": "datetime.datetime.strptime",
"__kw": {}
}
}
}
这意味着我们现在使用的是ISO格式的日期,而不是独立的字段,并且不再使用类名来创建对象。class的值被扩展成为了一个类名或静态方法名。
9.4.7 将JSON写入文件
当我们写JSON文件时,通常会像如下代码这样实现。
with open("temp.json", "w", encoding="UTF-8") as target:
json.dump( travel3, target, separators=(',', ':'), default=blog_
j2_encode )
以所需的编码格式打开文件,给json.dump()方法传入文件对象。当读JSON文件时,可以使用一个类似以下这样的技巧。
with open("some_source.json", "r", encoding="UTF-8") as source:
objects= json.load( source, object_hook= blog_decode)
思路是,将JSON作为文本的表示方式与字节转换的过程进行分离。在JSON中有一些格式化的方式可以选择。在之前的例子中缩进了4个空格是为了生成更美观的JSON输出。还有一种选择,可使得输出更紧凑而不使用缩进。可以使分隔符更简洁,从而对其进一步压缩。如下是temp.json生成的输出。
{"class":"BlogJ","args":["Travel",[{"class":"PostJ","
args":[],"kw":{"rsttext":"Some embarrassing revelati
on.","tags":["#RedRanger","#Whitby42","#ICW"],"title":"Hard
Aground","date":{"class":"datetime.datetime.strptime","
args":["2013-11-14T17:25:00","%Y-%m-%dT%H:%M:%S"],"
kw":{}}}},{"class":"PostJ","_args":[],"__kw":{"rst
text":"Some witty epigram.","tags":["#RedRanger","#Whitby42","#Mistak
es"],"title":"Anchor Follies","date":{"class":"datetime.datetime.
strptime","args":["2013-11-18T15:30:00","%Y-%m-%dT%H:%M:%S"],"
kw":{}}}}]],"kw":{}}
