引言
本篇想从序列化开始,讲述python的xml、json以及protobuf文件格式,并实现xml到json以及json到protobuf之间格式的互转与xml增删改查操作。另外关于ujson与bjson等格式类型,会在最后进行总结。
序列化与反序列化
互联网的产生带来了机器间通讯的需求,而互联通讯的双方需要采用约定的协议,序列化和反序列化属于通讯协议的一部分。通讯协议往往采用分层模型,不同模型每层的功能定义以及颗粒度不同,例如:TCP/IP协议是一个四层协议,而OSI模型却是七层协议模型。在OSI七层协议模型中展现层(Presentation Layer)的主要功能是把应用层的对象转换成一段连续的二进制串,或者反过来,把二进制串转换成应用层的对象–这两个功能就是序列化和反序列化。一般而言,TCP/IP协议的应用层对应与OSI七层协议模型的应用层,展示层和会话层,所以序列化协议属于TCP/IP协议应用层的一部分。本文对序列化协议的讲解主要基于OSI七层协议模型。
上一段引用自美团2015年的 序列化和反序列化 . 不知道为啥看的人很少,但我觉得这篇文章概念都介绍得挺深的,我看得也受益匪浅,所以本篇的很多概念都会出自于这篇和几篇外文文献,我会在最后的参考文献里标注出来。那么经过上面这一段话,我们就能总结出序列化和反序列化的原理:
- 序列化: 将数据结构或对象转换成二进制串的过程
- 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
在python中,序列化即是将内存中的字典、列表、集合以及各种对象保存到一个二进制串或者一个文件中,前者叫序列化,后者叫序列化加持久化,而只有根据序列化后的数据流,才能在网络传输中比较快的进行解析与通信。
再举个通俗点的例子: 你在打游戏过程中,打累了,停下来,想过两天再玩,两天之后,游戏又从你上次停止的地方继续运行,你上次游戏的进度肯定保存到硬盘上了,那么是以何种形式呢?游戏过程中产生的很多临时数据是不规律的,可能在你关掉游戏时正好是10个列表,3个嵌套字典的数据集合在内存里面,需要存下来,你如何存?把列表变成文件里的多行多列形式?那嵌套字典呢?根本没法存吧,所以,若是有种办法可以直接把内存数据存到硬盘上,下次程序再启动,再从硬盘上读出来,还是原来的格式,那是最好的,所以这就是我们要说的序列化。
python有提供非常多的序列化工具,比较常用的便是下表所见:
要实现的功能 | 可以使用的api |
---|---|
将Python数据类型转换为(json)字符串 | json.dumps() |
将json字符串转换为Python数据类型 | json.loads() |
将Python数据类型以json形式保存到本地磁盘 | json.dump() |
将本地磁盘文件中的json数据转换为Python数据类型 | json.load() |
将Python数据类型转换为Python特定的二进制格式 | pickle.dumps() |
将Python特定的的二进制格式数据转换为Python数据类型 | pickle.loads() |
将Python数据类型以Python特定的二进制格式保存到本地磁盘 | pickle.dump() |
将本地磁盘文件中的Python特定的二进制格式数据转换为Python数据类型 | pickle.load() |
以类型dict的形式将Python数据类型保存到本地磁盘或读取本地磁盘数据并转换为数据类型 | shelve.open() |
将python字典数据转换为xml格式 | xmltodict.unparse() |
将xml文件格式转换为python字典数据 | xmltodict.parse() |
其中xmltodict不是python内置包,需要pip install xmltodict,首先开始介绍xml格式的书写规范。
xml介绍与比较
本节我可能会用一段不小的篇幅,以及几段代码来总结一下xml,因为最近接触这个的比较多,如果不感兴趣的可以跳过,因为后面基本也不会再提到,我只是想写点笔记,如果以后有用到可以回头再看。
XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。 XML历史悠久,其1.0版本早在1998年就形成标准,并被广泛使用至今。XML的最初产生目标是对互联网文档(Document)进行标记,所以它的设计理念中就包含了对于人和机器都具备可读性。 但是,当这种标记文档的设计被用来序列化对象的时候,就显得冗长而复杂(Verbose and Complex)。 XML本质上是一种描述语言,并且具有自我描述(Self-describing)的属性,所以XML自身就被用于XML序列化的IDL。 标准的XML描述格式有两种:DTD(Document Type Definition)和XSD(XML Schema Definition)。作为一种人眼可读(Human-readable)的描述语言,XML被广泛使用在配置文件中,例如O/R mapping、 Spring Bean Configuration File 等。
我记得我刚开始学编程的时候,其实接触xml是比较多的,那个时候没有一条明确的主线瞎学,很多应用提供的配置文件还是xml,我就只能照葫芦画瓢模仿着来搭建,但随着技术的提升,以及选定python,似乎开始与xml渐行渐远,而直到最近,因为一个图片标注信息是由csv保存而需要转换成xml,之后还要对标注信息修改,让我意识到xml的存在必要性。即使是今天,xml看似已经被淘汰,但不会消亡,因为它的易于扩展性以及安全性。具体可以看知乎某帖:为什么XML这么笨重的数据结构仍在广泛应用?
XML全称EXtensible Markup Language,翻译为可扩展置标语言,在python中专门有包为xml,实例为:
import xml.etree.ElementTree as ET
new_xml = ET.Element("namelist")
name = ET.SubElement(new_xml, "name", attrib={"enrolled": "yes"})
age = ET.SubElement(name, "age", attrib={"checked": "no"})
sex = ET.SubElement(name, "sex")
sex.text = '33'
name2 = ET.SubElement(new_xml, "name", attrib={"enrolled": "no"})
age = ET.SubElement(name2, "age")
age.text = '19'
et = ET.ElementTree(new_xml) # 生成文档对象
et.write("test.xml", encoding="utf-8", xml_declaration=True)
ET.dump(new_xml) # 打印生成的格式
"""
<namelist><name enrolled="yes"><age checked="no" /><sex>33</sex></name><name enrolled="no"><age>19</age></name></namelist>
"""
输出的结果基本就是标准xml格式数据了,了解了python怎么生成xml的,我们还可以就此xml进行json转换:
import xmltodict
import json
a = """
<namelist><name enrolled="yes"><age checked="no" /><sex>33</sex></name><name enrolled="no"><age>19</age></name></namelist>
"""
order_dict = xmltodict.parse(a)
json_data = json.dumps(order_dict)
print(json_data)
"""
{"namelist": {"name": [{"@enrolled": "yes", "age": {"@checked": "no"}, "sex": "33"}, {"@enrolled": "no", "age": "19"}]}}
"""
将json或者dict转xml也是基于xmltodict.unparse就行,可以对照上面的表格,具体的操作我就不再列举了。另外我想记录一下之前我业务根据csv转xml的例子:
from xml.etree.ElementTree import Element,ElementTree,tostring
import json,csv
def csvtoxml(fname):
with open(fname,'r') as f:
reader=csv.reader(f)
header=next(reader)
root=Element('Daaa')
print('root',len(root))
for row in reader:
erow=Element('Row')
root.append(erow)
for tag,text in zip(header,row):
e=Element(tag)
e.text=text
erow.append(e)
beatau(root)
return ElementTree(root)
def beatau(e,level=0):
if len(e)>0:
e.text='\n'+'\t'*(level+1)
for child in e:
beatau(child,level+1)
child.tail=child.tail[:-1]
e.tail='\n' + '\t'*level
et=csvtoxml(r'C:\Temp\ff.csv')
et.write(r'C:\Temp\fff.xml')
引用自python csv文件转换成xml, 构建新xml文件
当时有尝试自己写一个版本,但我拿到的是csv版本的txt文本,给我的思路加大了难度,中间做了很多复杂度很高的操作,发现还是上面这个版本代码好用,之后也考虑写一篇关于csv以及xlrd操作的博文。接下来看那么增查都已经做出来了,还有删改没有,这里会存在一个问题在于,xml其实转成json的话对字段名或者标签的判断都是一致的,举个例子:
"""
<response><account>160381</account><terminal>16038101</terminal><signValue>6AEFA3FB8A732E1F5B8455729B8EB6A43CE9AA6011E990A57168338617A20211</signValue><results>00</results><details>Success</details><count>2</count><totalpages>1</totalpages><pages>1</pages><orderInfo><account>160381</account><terminal>16038101</terminal><payment_id>190526021737865038128</payment_id><order_number>B2560364</order_number><order_currency>USD</order_currency><order_amount>51.36</order_amount><methods>Credit Card</methods><payment_dateTime>2019-05-26 02:17:37</payment_dateTime></orderInfo><orderInfo><account>160381</account><terminal>16038101</terminal><payment_id>190525045009257038198</payment_id><order_number>B2560361</order_number><order_currency>USD</order_currency><order_amount>42.50</order_amount><methods>Credit Card</methods><payment_dateTime>2019-05-25 04:50:09</payment_dateTime></orderInfo></response>
"""
用上面代码xml转成json:
"""
{"response": {"account": "160381", "terminal": "16038101", "signValue": "6AEFA3FB8A732E1F5B8455729B8EB6A43CE9AA6011E990A57168338617A20211", "results": "00", "details": "Success", "count": "2", "totalpages": "1", "pages": "1", "orderInfo": [{"account": "160381", "terminal": "16038101", "payment_id": "190526021737865038128", "order_number": "B2560364", "order_currency": "USD", "order_amount": "51.36", "methods": "Credit Card", "payment_dateTime": "2019-05-26 02:17:37"}, {"account": "160381", "terminal": "16038101", "payment_id": "190525045009257038198", "order_number": "B2560361", "order_currency": "USD", "order_amount": "42.50", "methods": "Credit Card", "payment_dateTime": "2019-05-25 04:50:09"}]}}
"""
我们可以发现如果要从父节点开始删起,有多个orderInfo一样的情况下,内部会将这算成一个值,转成json里都是列表里的字典,它们本身没有索引值,如果根据条件去删除orderInfo节点,会造成问题是乱序,这跟读出的sql表删除行但没有标识是一样的,所以可以先进行修改再删除:
# 修改
doc = ET.parse(filepath)
root = doc.getroot()
for elem in root.iter(tag='orderinfo'):
flag = 0 # 条件判断标识符
if flag:
pass
else:
elem.tag = "deleted"
doc.write(filepath + 'test.xml')
# 删除
doc=ET.parse(filepath)
root = doc.getroot()
for jobNames in root.findall('deleted'):
root.remove(jobNames)
doc.write(filepath + 'test.xml')
JSON和XML的可读性可谓不相上下,一边是简易的语法,一边是规范的标签形式,很难分出胜负。不过JSON在随着Javascript开始发力后,再前后端分离中都起到了很大作用,并且可以存储Javascript复合对象,有着xml不可比拟的优势,而xml的复杂性确实也是不再采用的一个原因,但正因为复杂,所以安全性与严格协定更多胜任在了yaml或者mybatis等应用上。
json介绍与比较
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,它使得人们很容易的进行阅读和编写。同时也方便了机器进行解析和生成。适用于进行数据交互的场景,比如网站前台与后台之间的数据交互。原理介绍完,这里不再对这种数据结构进行代码介绍,具体方法都列在了上面的表格中,我这节想探讨的是为什么会选用json。
从我的理解来看,json其实是一个协议族,它可以看成是一种无状态的数据格式,加载进内存,所以如果数据量大的话其实是比较占网络带宽的,这里将引出下面的protobuf(协议缓冲区),但这里对比的对象并不是这个,而是在于json的选择。
经过上面的介绍我们可以总结得到python的特点,其中优点为:
- 众所周知且广泛使用的标准
- 无模式
- 自描述
- 可读可写
- Python的stdlib
缺点为:
- 与其他格式相比相对慢
- 没有二进制支持(通常对二进制字段使用base64编码)
- 序列化的有效负载相对较大(尤其是对于数字字段)
那基于缺点中的第一点,就有非常多的人基于此开发出了ujson、bjson,cjson,jsjon,orjson、restframework-json。。。等等各种各样的json格式,上面的格式说白了都可以从第一个字母看出是根据什么语言做的格式,那有人就基于这些做了一个benchmark测试,测试方法的代码有非常多,也有针对各种序列化 / 反序列化还有保存到文件比较的,测试的都为时间,这里可以写一个最简单的demo为:
import time
import json
import orjson
import rapidjson
import ujson
m = {
"timestamp": 1556283673.1523004,
"task_uuid": "0ed1a1c3-050c-4fb9-9426-a7e72d0acfc7",
"task_level": [1, 2, 1],
"action_status": "started",
"action_type": "main",
"key": "value",
"another_key": 123,
"and_another": ["a", "b"],
}
def benchmark(name, dumps):
start = time.time()
for i in range(1000000):
dumps(m)
print(name, time.time() - start)
benchmark("Python", json.dumps)
# orjson only outputs bytes, but often we need unicode:
benchmark("orjson", lambda s: str(orjson.dumps(s), "utf-8"))
benchmark("rapidjson", rapidjson.dumps)
benchmark("ujson", ujson.dumps)
"""
Python 7.131036043167114
orjson 1.4195716381072998
rapidjson 3.384244203567505
ujson 2.337538957595825
"""
可以看到的是在dumps阶段,orjson是最快的,ujson和rapidjson次之,关于这三种结构,ujson 是用纯 C 写的,RapidJSON 是 C++ 写的,而orjson是Rust,这其实跟语言的速度排名基本一致。更详细的数据有人做过统计图测试:
使用JSON的次数越多,遇到JSON编码或解码的瓶颈的可能性就越大。Python的内置库不错,但是有多个更快的JSON库可用:如何选择要使用的库?
事实上这只能根据相关业务去进行尝试,而没有统一答案:
- “快速JSON库”对不同的人来说意味着不同的事物,因为他们的使用方式是不同的。
- 速度并非决定一切,可能还会关心其他事情,例如安全性和自定义。
其中的安全性和自定义指:
- 安全/防崩溃:日志消息可以包含来自不受信任来源的数据。如果JSON编码器因错误数据而崩溃,那么对可靠性或安全性都不利。
- 自定义编码:Eliot支持自定义JSON编码,因此您可以序列化其他种类的Python对象。一些JSON库支持此功能,而另一些则不支持。
- 跨平台:可在Linux,macOS,Windows上运行。
- 维护:我不想依赖没有得到积极支持的库。
这同样是一个让人头疼的问题,在写之前,我去GitHub与谷歌看到了很多使用者提出了非常多的问题,比如conda装不了ujson,还有在Django中用哪种json格式会报错,rapid速度其实没有测试时这样。。。等等,另外还翻到一篇比较强的文章,从json解析器开始剖析原理:
引用自Parsing JSON is a Minefield
protobuf介绍与比较
Protocol Buffers usually referred to as Protobuf, was internally developed by Google with the goal to provide a better way, compared to XML, for data serialization -deserialization. So they focused on making it simpler, smaller, faster and more maintainable then XML. But, this protocol even surpassed JSON with better performance, better maintainability, and smaller size.
关于Protocol Buffers我的理解并不是很多,我也是之前学go的时候再官网看到并写下如今的记录,到如今,protobuf支持格式如下图所示:
而我之前看的博客是 Go Protobuf 简明教程 .
相比之下,python对于这种数据格式的支持确实不是太好,从GitHub上的star就可以看出来,但它的优势在于,可以发送大容量的数据作为接口传输给服务或者客户端,比如说一个数据流,经手于算法,传送给前端,这时候单单用json,可能一条数据就会有7 / 8M,不论io频繁不频繁,但这是非常消耗带宽的,在客户量比较多的网站的后台服务器,带宽是比服务器还烧钱的东西,有这闲钱为啥不再去买一台服务器做集群呢?
protobuf的数据格式为:
//A simple Proto file - Polyline.proto
syntax = “proto2”;
message Point {
required int32 x = 1;
required int32 y = 2;
optional string label = 3;
}
message Line {
required Point start = 1;
required Point end = 2;
optional string label = 3;
}
message Polyline {
repeated Point point = 1;
optional string label = 2;
}
//File source: https://en.wikipedia.org/wiki/Protocol_Buffers
它和xml一样,是有数据模式的,依托于proto文件,并且需要编译,这可能就是python比较少用的原因。它的转换函数可以看Google提供的例子:
import simplejson
from google.protobuf.descriptor import FieldDescriptor as FD
class ConvertException(Exception):
pass
def dict2pb(cls, adict, strict=False):
"""
Takes a class representing the ProtoBuf Message and fills it with data from
the dict.
"""
obj = cls()
for field in obj.DESCRIPTOR.fields:
if not field.label == field.LABEL_REQUIRED:
continue
if not field.has_default_value:
continue
if not field.name in adict:
raise ConvertException('Field "%s" missing from descriptor dictionary.'
% field.name)
field_names = set([field.name for field in obj.DESCRIPTOR.fields])
if strict:
for key in adict.keys():
if key not in field_names:
raise ConvertException(
'Key "%s" can not be mapped to field in %s class.'
% (key, type(obj)))
for field in obj.DESCRIPTOR.fields:
if not field.name in adict:
continue
msg_type = field.message_type
if field.label == FD.LABEL_REPEATED:
if field.type == FD.TYPE_MESSAGE:
for sub_dict in adict[field.name]:
item = getattr(obj, field.name).add()
item.CopyFrom(dict2pb(msg_type._concrete_class, sub_dict))
else:
map(getattr(obj, field.name).append, adict[field.name])
else:
if field.type == FD.TYPE_MESSAGE:
value = dict2pb(msg_type._concrete_class, adict[field.name])
getattr(obj, field.name).CopyFrom(value)
else:
setattr(obj, field.name, adict[field.name])
return obj
def pb2dict(obj):
"""
Takes a ProtoBuf Message obj and convertes it to a dict.
"""
adict = {}
if not obj.IsInitialized():
return None
for field in obj.DESCRIPTOR.fields:
if not getattr(obj, field.name):
continue
if not field.label == FD.LABEL_REPEATED:
if not field.type == FD.TYPE_MESSAGE:
adict[field.name] = getattr(obj, field.name)
else:
value = pb2dict(getattr(obj, field.name))
if value:
adict[field.name] = value
else:
if field.type == FD.TYPE_MESSAGE:
adict[field.name] = \
[pb2dict(v) for v in getattr(obj, field.name)]
else:
adict[field.name] = [v for v in getattr(obj, field.name)]
return adict
def json2pb(cls, json, strict=False):
"""
Takes a class representing the Protobuf Message and fills it with data from
the json string.
"""
return dict2pb(cls, simplejson.loads(json), strict)
def pb2json(obj):
"""
Takes a ProtoBuf Message obj and convertes it to a json string.
"""
return simplejson.dumps(pb2dict(obj), sort_keys=True, indent=4)
而关于什么时候用这种序列化,怎么参考选用方案,除了我上面说的带宽问题,另外参考文献里给出的一个系列问题很经典:
- Does speed really matters to us? What is the system data access pattern and expected growth in the near future?
if speed is not of a matter, usually prefer JSON.- Does the system (de)serialize large number of messages?
as rule of thumb less than several GBs / day usually not worth even thinking changing format.- Is it a new system that changes often and we want to keep flexibility of our data models?
if so, schema-less format is a requirement.- What is the serialize/deserialize ratio?
is the system write once read many (e.g. logging aggregation system) or one read one write (e.g. event based using celery)? take into account this ratio.- is the system interact with other systems with existing serialization formats?
if so think twice before changing the existing format(s)- Do messages consumed / created by humans?
human readable formats easier to manipulate, debug and understand than binary formats.- Does data mainly numeric?
some formats provides great compression ratio and speed for numeric values like HDF5 (not benchmarked here).- Does the system components are written in more than one language?
if so better to use format with well support by all of the system languages.
关于benchmark对比:每个库的对象大小,平均1M对象的字节数(无压缩):
参考与推荐:
[1]. 序列化与反序列化
[2]. json vs simplejson vs ujson
[3]. Choosing a faster JSON library for Python