# 序列化

  • 序列化(Serialization):指将数据转换成便于传输、存储的格式,可能采用 str 或 bytes 类型。
    • 不同的程序可能采用不同的格式存储数据,为了方便在它们之间传输、存储数据,通常要将数据序列化成某种通用的格式,比如 XML、JSON、YAML 等。
  • 反序列化:指序列化的逆过程,即将数据转换成原本的格式。

# INI

:一种保存键值对数据的文本格式,只支持简单的单层结构。

  • 是 Windows 系统采用的一种配置文件格式,但类 Unix 系统中也存在类似的文件格式,只是没有明确的标准。

# 语法

  • INI 格式的文本文件的扩展名为 .ini 。
  • 用分号 ; 声明单行注释,有的解析器也支持用 #
  • 每行声明一对键值对参数,用等号 = 连接 key 和 value 。
    • key 、 value 都是字符串类型,有的解析器也能识别 true、false 等特殊值。
    • key 、 value 前后的空格会被忽略,除非用双引号作为定界符包住。
  • 用中括号 [ ] 声明一个段(Section)。
    • 中括号内的字符串会作为段名,不会忽略空格。
    • 每个段中, key 不能重复。

#

[Section 1]
id = 1
tips = Hello World

[MySQL]
runner = root
work_dir = /opt/mysql

# XML

:可扩展标记语言(X Extensive Markup Language),一种序列化数据的文本格式。

  • 由于 HTML 不支持自定义标签,通用性低,不适合保存结构化数据。1998 年,W3C 发布了 XML 1.0 标准。
    • XML 主要使用与 HTML 标签类似的语法来保存数据,但比较繁琐,可读性差。
    • 虽然都是文本格式,但 HTML 是用于在网页上显示数据,而 XML、JSON、YAML 是用于将数据序列化,方便传输、存储。

# 语法

  • XML 格式的文本文件的扩展名为 .xml 。
  • 区分大小写。
  • 空格、换行符不会被忽略。
  • <!-- ... --> 声明注释。
  • 必须在第一行声明 XML 的版本:
    <?xml version='1.1' encoding='UTF-8'?>
    

# 元素

  • XML 以元素为单位存储数据,每个元素是用一对标签包住一个值,格式如下:

    <tag>value</tag>
    
    • 标签用尖括号标记。
    • 标签、值都是字符串,但是不必加引号作为定界符。
  • 每个元素有两个标签,前一个标签称为开始标签,前一个标签称为结束标签。如果元素没有值,则可以合并成一个标签,如下:

    <name />
    <name id='1' />
    
  • 可以在开始标签中加上属性。属性是键值对格式,且它的值必须要用双引号作为定界符。如下:

    <note date="20201001" time="12:00">
    Hello World
    </note>
    
  • 一个元素的值可以是一个或多个子元素,构成嵌套的层级结构。如下:

    <student>
      <id>1</id>
      <name>one</name>
    </student>
    
    • 一个 XML 文档中最外层的元素称为根元素。

# 转义字符

  • 在 XML 文本中,以下左侧字符需要替换成右侧的转义字符:
    <     &lt;
    >     &gt;
    &     &amp;
    '     &apos;
    "     &quot;
    

# import xml

:Python 的标准库,用于解析或导出 XML 格式的文本。

  • 官方文档 (opens new window)

  • 创建元素:

    >>> from xml.etree import ElementTree
    >>> root = ElementTree.Element('test')  # 创建一个元素
    >>> root
    <Element 'test' at 0x7fe42c2d1e08>
    >>> root.tag                            # 获取元素的标签名(str 类型)
    'test'
    >>> root.text = 'Hello World'           # 设置元素的值(str 类型)
    >>> root.text
    'Hello World'
    
    • 将 XML 元素映射为 xml.etree.ElementTree.Element 对象。
  • 操作元素的属性:

    >>> root.set('date', '20191001')        # 设置一个属性(如果该属性已存在则覆盖其值)
    >>> root.set('time', '12:00')
    >>> root.attrib                         # 获取元素的全部属性(dict 类型)
    {'date': '20191001', 'time': '12:00'}
    
  • 操作子元素:

    >>> sub = ElementTree.Element('sub')
    >>> root.append(sub)        # 添加一个子元素(可以添加多个)
    >>> root.append(sub)
    >>> root[:]                 # Element 对象支持 list 的大部分操作方法
    [<Element 'sub' at 0x7efd36b9a278>, <Element 'sub' at 0x7efd36b9a278>]
    >>> root.find('sub')        # 根据 tag 名查找一个子元素,返回第一个匹配的,没找到则返回 None
    <Element 'sub' at 0x7fe4247fb278>
    >>> root.findall('sub')     # 根据 tag 名查找所有匹配的子元素,没找到则返回[]
    [<Element 'sub' at 0x7fe4247fb278>, <Element 'sub' at 0x7fe4247fb278>]
    >>> root.remove(sub)        # 删除一个子元素
    >>> root.clear()            # 删除 text、子元素、attrib
    
  • 与字符串的转换:

    >>> root = ElementTree.XML('<test />')            # 解析 XML 格式的字符串(返回的是根元素)
    >>> root
    <Element 'test' at 0x7fe42c2d1e08>
    
    >>> ElementTree.tostring(root, encoding='utf-8')  # 导出 XML 格式(返回的是 bytes 类型)
    b'<test />'
    

# import lxml

:Python 的第三方库,用于解析或导出 XML、HTML 格式的文本。

  • 官方文档 (opens new window)

  • 安装:pip install lxml

  • 兼容标准库 xml 的大部分操作方法,且功能更强,比如支持自动缩进。

  • 创建元素:

    >>> from lxml import etree
    >>> root = etree.Element('test')    # 创建一个元素
    >>> root
    <Element test at 0x243576e9d40>
    >>> root.tag                        # 获取元素的标签名(str 类型)
    'test'
    >>> root.text = 'Hello World'       # 设置元素的值(str 类型)
    >>> root.text
    'Hello World'
    
    • 将 XML 元素映射为 lxml.etree.Element 对象。
  • 操作元素的属性:

    >>> root.set('date', '20191001')    # 设置一个属性(如果该属性已存在则覆盖其值)
    >>> root.set('time', '12:00')
    >>> root.attrib                     # 获取元素的全部属性(dict 类型)
    {'date': '20191001', 'time': '12:00'}
    
  • 操作子元素:

    >>> sub = etree.Element('sub')
    >>> root.append(sub)                # 添加一个子元素(可以添加多个)
    >>> root.append(sub)
    >>> root[:]                         # Element 对象支持 list 的大部分操作方法
    [<Element sub at 0x243576e9e40>]    # 重复添加同一个 Element 对象作为子元素时,只会生效一次
    
  • 与字符串的转换:

    >>> root = etree.XML('<root><id>1</id></root>')    # 解析 XML 格式的字符串(返回的是根元素)
    >>> root
    <Element root at 0x243573c3ac0>
    
    >>> etree.tostring(root, encoding='utf-8')    # 导出 XML 格式(返回的是 bytes 类型)
    b'<root><id>1</id></root>'
    >>> etree.tostring(root, encoding='utf-8', xml_declaration=True)      # 开头加上声明语句
    b"<?xml version='1.0' encoding='utf-8'?>\n<root><id>1</id></root>"
    

    加上缩进:

    >>> root.text                           # 处理前,元素值为空
    >>> etree.indent(root, space='  ')
    >>> root.text                           # 处理后,元素值加上了缩进
    '\n  '
    >>> print(etree.tostring(root, encoding='utf-8').decode())
    <root>
      <id>1</id>
    </root>
    

    处理 HTML 格式:

    >>> root = etree.XML('<html><head/><body><p>Hello<br/>World</p></body></html>')
    >>> etree.tostring(root, method='xml')
    b'<html><head/><body><p>Hello<br/>World</p></body></html>'
    >>> etree.tostring(root, method='html')
    b'<html><head></head><body><p>Hello<br>World</p></body></html>'   # HTML 标准的标签有些许差异
    

# JSON

:JSON(JavaScript Object Notation),一种序列化数据的文本格式。

  • JSON 原本是 JavaScript 的对象表示格式,后来发展成一种独立于语言、平台的文本格式,常用于在不同编程语言之间传递数据。

# 语法

  • JSON 格式的文本文件的扩展名为 .json 。
  • JavaScript 中可以用 // 声明单行注释,但 JSON 中不允许使用注释,否则会导致解析出错。
  • JSON 中可以记录多个键值对(key-value)格式的字段,每个键值对之间用逗号分隔。
    • key 必须是字符串类型,用双引号包住,且同一作用域下不能重名。
    • key 与 value 之间用冒号 : 连接。
    • 例如,JSON 中的字段 "name": "Hello" 相当于 JavaScript 中的变量 name = "Hello"
    • value 可以是以下数据类型:
      number    # 数值,取值为整数或浮点数
      boolean   # 布尔,取值为小写的 true 或 false
      string    # 字符串,用双引号作为定界符
      array     # 数组,用中括号包住,有序地存储多个键值对
      object    # 对象,用花括号包住,无序地存储多个键值对
      null      # 空,代表 value 不存在
      
  • 为了方便阅读,JSON 文本通常采用 4 个空格作为缩进。
    • 字段之间可以插入任意个空字符,不影响语法。
  • 例:
    {
        "name": "Leo",
        "age": 12,
        "emails": [
            "[email protected]",
            "[email protected]"
        ]
    }
    

# import json

:Python 的标准库,用于生成、解析 JSON 格式的文本。

  • 官方文档 (opens new window)

  • 使用 json.dumps() 可将 Python 基本数据类型的对象转换成 JSON 中的数据类型。转换关系如下:

    int,  long, float —> number
    str,  unicode     —> string
    list, tuple       —> array
    dict  —> object
    True  —> true
    False —> false
    None  —> null
    
  • 例:JSON 序列化

    >>> import json
    >>> dic1 = {'a': 1, 'b': True}
    >>> json.dumps(dic1)
    '{"a": 1, "b": true}'
    >>> print(json.dumps(dic1, indent=4, sort_keys=True)) # 默认 indent=None ,在输出时不换行、不缩进。这里设置 4 个空格的缩进,并对 key 排序
    {
        "a": 1,
        "b": true
    }
    
    >>> json.dumps(json)                        # 非基本数据类型的对象,在序列化时会报错
    TypeError: Object of type module is not JSON serializable
    >>> json.dumps('你好')                      # Unicode 字符会按 str.encode('unicode_escape') 编码
    '"\\u4f60\\u597d"'
    >>> json.dumps("你好", ensure_ascii=False)  # 取消将输出的字符全部转换成 ASCII 码,从而保留 Unicode 字符
    '"你好"'
    
  • 例:JSON 反序列化

    >>> json.loads('true')
    True
    >>> json.loads('{"你好": 1}')
    {'你好': 1}
    

# YAML

:一种序列化数据的文本格式。与 JSON 的数据结构相似,但更简洁,容易供人阅读。

  • 发音为 /ˈjæməl/

# 语法

  • YAML 格式的文本文件的扩展名为 .yaml 或 .yml 。

  • 区分大小写。

  • 用 # 声明单行注释。

  • 必须用缩进表示层级关系。

    • 每层缩进通常为 2 个空格,不支持 Tab 等空字符。
    • 缩进的空格数可以任意,但同一层级的空格数要相同。
  • 可以在一个文件中保存多个 YAML 文档,每个文档用一行 --- 表示开头、用一行 ... 表示结尾。

    • 也可只在每个文档之间用一行 --- 分隔。
  • 例:

    debug: true
    env: test
    server:             # value 为数组类型
      - 10.0.0.1
      - 10.0.0.2
    environment:        # value 为字典类型
      var1: 1
      var2: Hello
    
  • YAML 中可以记录多个键值对(key-value)格式的字段。

    • key 必须是字符串类型,且同一作用域下不能重名。
    • key 与 value 之间用 : 连接。
      • 注意冒号 : 右侧必须存在空格,否则冒号左右的内容会被看作一个字符串。
    • value 为字符串类型时,可以采用单引号、双引号作为定界符,也可以省略。
    • value 为数组类型时,有两种写法:
      env: ['dev', 'test']
      
      env:
        - dev       # 数组元素多一层缩进,每个元素以 - 开头
        - test
      
    • value 为空时,写作 null~ 。为空字符串时,写作 ''""

# import yaml

:Python 的第三方库,用于生成、解析 YAML 格式的文本。

  • 官方文档 (opens new window)

  • 安装:pip install PyYAML

  • 例:序列化

    >>> import yaml
    >>> dic1 = {'a': 1, 'b': True}
    >>> yaml.dump(dic1)
    'a: 1\nb: true\n'
    
    >>> yaml.dump('你好')   # Unicode 字符会按 str.encode('unicode_escape') 编码
    '"\\u4F60\\u597D"\n'
    >>> yaml.dump('你好', allow_unicode=True) # 保留 Unicode 字符
    '你好\n...\n'
    
    • 序列化时,默认会将 Python 字典的 key 按升序排列。
  • 例:反序列化

    >>> yaml.load('a: 1\nb: true\n', Loader=yaml.CLoader)
    {'a': 1, 'b': True}
    

# pickle

:Python 的标准库,可以将 Python 对象序列化成二进制格式。

#

  • 序列化:
    >>> import pickle
    >>> pickle.dumps('Hello')   # 序列化一个字符串
    b'\x80\x04\x95\t\x00\x00\x00\x00\x00\x00\x00\x8c\x05Hello\x94.'
    >>> with open('f1', 'wb') as f:
    ...     f.write(pickle.dumps('Hello'))       # 将序列化的数据保存到文件中
    ...
    24
    
    >>> class Test:
    ...     a = 1
    ...
    >>> Test()
    <__main__.Test object at 0x0000018564081490>
    >>> pickle.dumps(_)         # 序列化自定义类型的对象
    b'\x80\x04\x95\x18\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94)\x81\x94.'
    
  • 反序列化:
    >>> with open('f1', 'rb') as f:
    ...     data = f.read()
    ...
    >>> pickle.loads(data)
    'Hello'
    

# ProtoBuf

:Protocol Buffer ,一种序列化数据的格式,属于二进制格式。

  • 官方文档 (opens new window)
  • 2008 年,由 Google 公司发布。
  • 与 JSON 相比, ProtoBuf 格式不能供人阅读,但被机器解析的速度更快、存储体积更小,提高了在不同程序之间传递数据的效率。
  • 与 pickle 相比, ProtoBuf 格式独立于语言、平台,是一种通用的二进制格式。

# 语法

  • // 声明单行注释,用 /**/ 声明多行注释。
  • 每条语句必须以分号 ; 结尾。
  • 通常在扩展名为 .proto 的文本文件中定义 ProtoBuf 的消息模型(message)。
    • message 中可以包含多个字段,支持 bool、int32、string 等多种数据类型。
    • message 支持嵌套。
    • message 中每个字段需要定义一个大于 1 的编号,不能重复,用于在二进制格式中定位。
    • message 中每个字段需要在开头加上一种修饰符:
      • optional :表示创建消息时,该字段是可选的,可以不赋值。此时采用系统的默认值,比如 int32 的默认值为 0 。
      • required :表示该字段是必须赋值的。该修饰符在 proto2 不建议使用,在 proto3 不支持。
      • repeated :表示该字段可以重复赋值,此时视为动态大小的数组。
  • 语法版本主要有两个:
    • proto2
    • proto3
      • optional 修饰符改名为 singular ,并默认采用。
      • required 修饰符被弃用。

#

  1. 下载 protoc (opens new window) ,然后安装:

    yum install -y gcc-g++
    ./configure
    make install
    
    • protoc 是用 C++ 开发的命令行工具,用于编译 .proto 文件,生成各种编程语言的源文件。
  2. 编写一个 .proto 文件,定义消息模型:

    syntax = "proto3";          // 声明语法版本
    package test;               // 声明当前文件属于一个名为 test 的包
    // import "test2.proto";    // 可通过 import 导入其它包
    
    message Person{
        string name = 1;
        int32 age = 2;
        message Email{
            string address = 1;
        }
        repeated Email emails = 3;
    }
    
  3. 用 protoc 将 .proto 文件编译成 py 文件:

    [root@CentOS ~]# protoc test.proto --python_out=.     # 还可用 -I <PATH> 指定目录用于寻找 import 的包
    [root@CentOS ~]# ls
    test_pb2.py  test.proto
    
  4. 在 Python 解释器中调用:

    >>> import test_pb2
    >>> p = test_pb2.Person()
    >>> p.name = 'Leo'
    >>> p.age = 12
    >>> p                       # 显示方便阅读的文本格式。 emails 字段此时取值为空,因此不显示
    name: "Leo"
    age: 12
    
    >>> p.SerializeToString()   # 序列化成二进制格式
    b'\n\x03Leo\x10\x0c'
    >>> p2 = test_pb2.Person()
    >>> p2.ParseFromString(p.SerializeToString())   # 反序列化,赋值给另一个消息
    
    >>> p.name = 1              # 赋值错误的数据类型时会报错
    TypeError: Cannot set test.Person.name to 1: 1 has type <class 'int'>, but expected one of: (<class 'bytes'>, <class 'str'>)
    >>> p.phone = ''            # 赋值给未定义的字段时会报错
    AttributeError: 'Person' object has no attribute 'phone'
    
    >>> p.emails                # repeated 类型字段保存为数组形式
    []
    >>> e = p.emails.add()      # 添加一个元素,返回其引用
    >>> e.address = '[email protected]'
    >>> p.emails
    [address: "[email protected]"
    ]
    
    • 需要安装第三方库 pip install protobuf