跳转至

序列化

程序通常(至少)使用两种形式的数据:

  1. 在内存中,数据保存在对象、结构体、列表、数组、散列表、树等中。 这些数据结构针对 CPU 的高效访问和操作进行了优化(通常使用指针)。
  2. 如果要将数据写入文件,或通过网络发送,则必须将其 编码(encode)/ 序列化(serialization) / 编组(marshalling) 为某种自包含的字节序列(例如,JSON 文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同。

Serialization指的是为了让数据便于传输和存储,将对象转化成特定格式的数据。Encoding是一个更宽泛的概念,将数据从一种格式转化成另一种格式都可以叫做Encoding。Marshal 不仅传输对象的状态,而且会一起传输对象的方法(相关代码)。但是实际上我们不需要严格的区分他们三者的区别。

编码方式 二进制/文本 类型 优点 缺点
Base64 文本 支持广泛,编码二进制 没有结构,浪费空间
JSON 文本 弱/自解释 简洁
XML 文本 弱/自解释 复杂
YAML 文本 弱/自解释
TOML 文本 弱/自解释 适合配置文件
CBOR 二进制 弱/自解释 编码紧凑,二进制版的JSON
CSV,TSV 表格 适合表格 缺少统一规范
Protobuf 二进制 二进制比文本节省空间 主要对Go比较好用
Borsh 二进制 可以序列化对象 使用的少,主要对Rust比较好用
Thrift 二进制
Avro 二进制 弱/自解释
pickle/ java.io.Serializable/... 二进制 语言自带,可以序列化对象 不安全,不通用

编码方式

JSON

  • 大于 \(2^{53}\) 的整数无法使用 IEEE 754 双精度浮点数精确,所以不应该使用JSON直接存储这样的数据
  • 不直接支持二进制数据,需要借助如Base64的帮助。

Python - std - json

Python 的 dict/list 和 JSON 是一个很好的对应。序列化 dumps 和反序列化 loads 的参数是字符串,dumpload 的参数是文件。

>>> import json
>>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
'["foo", {"bar": ["baz", null, 1.0, 2]}]'
>>> json.loads('["foo", {"bar": ["baz", null, 1.0, 2]}]')
['foo', {'bar': ['baz', None, 1.0, 2]}]
>>> from pathlib import Path
>>> json.load(Path("./a.json").open())   # 如果是一个简单脚本直接这样最简洁,只开不关
>>> json.loads(Path("./a.json").read_text())   # 如果是一个简单脚本直接这样最简洁

默认支持以下对象和类型,JSON 的类型都可以在 Python 中有一个很好的对应。

Python JSON
dict object
list, tuple array
str string
int, float, enum number
True true
False false
None null

Rust - crates.io - serde_json

更多关于 serde 库的内容,可以参考 Serde

在解析 JSON 文本的时候,我们可能有两种需求,以及相反的序列化的需求。

  1. 解析成一个 strongly typed Rust data structure。比如一些 API 已经规定好了返回的数据格式。
  2. 解析成一个 untyped or loosely typed representation。一些更普通的情况。

Demo For Strong Type 我们一般使用 #[derive(Serialize, Deserialize)] 来修饰要序列化/反序列化的类型。然后在 serde_json::from_str 的时候指定类型。

use serde::{Deserialize, Serialize};
use serde_json::Result;

#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

fn typed_example() -> Result<()> {
    // Some JSON input data as a &str. Maybe this comes from the user.
    let data = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

    // Parse the string of data into a Person object. This is exactly the
    // same function as the one that produced serde_json::Value above, but
    // now we are asking it for a Person as output.
    let p: Person = serde_json::from_str(data)?;

    // Do things just like with any other Rust data structure.
    println!("Please call {} at the number {}", p.name, p.phones[0]);

    Ok(())
}

如果我们希望再将 Person 序列化成 json 文本,可以调用 serde_json::to_string

    // Serialize it to a JSON string.
    let j = serde_json::to_string(&p)?;

    // Print, write to a file, or send to an HTTP server.
    println!("{}", j);

Demo For Loose Type: serde_json::Value 是这种松散类型的一般表示。在 serde_json::from_str 的时候指定类型为 Value 即可。

use serde_json::{Result, Value};

fn untyped_example() -> Result<()> {
    // Some JSON input data as a &str. Maybe this comes from the user.
    let data = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

    // Parse the string of data into serde_json::Value.
    let v: Value = serde_json::from_str(data)?;

    // Access parts of the data by indexing with square brackets.
    println!("Please call {} at the number {}", v["name"], v["phones"][0]);

    Ok(())
}

不难想到,序列化就是先构造松散类型的 Value 类型数据,再 to_string。我们可以使用 json! 宏来构造 Value 类型数据。

use serde_json::json;

fn main() {
    // The type of `john` is `serde_json::Value`
    let john = json!({
        "name": "John Doe",
        "age": 43,
        "phones": [
            "+44 1234567",
            "+44 2345678"
        ]
    });

    println!("first phone number: {}", john["phones"][0]);

    // Convert to a string of JSON and print it out
    println!("{}", john.to_string());
}

XML

有人对XML的评价是:一般的语言,要么适合机器阅读,要么适合人类阅读。而XML是少有的对人和机器都不友好的。这里不做过多介绍。

Python

标准库提供的 XML 模块,对于恶意构造的数据是不安全的。 攻击者可能滥用 XML 功能来执行拒绝服务攻击、访问本地文件、生成与其它计算机的网络连接或绕过防火墙。如果来源不信任,推荐使用 GitHub - tiran/defusedxml

TOML

TOML主要用于配置文件,而不是数据传输。

Python - std - tomllib

3.11 引入的解析 TOML 数据的模块,接口和 json 是一致的。

Rust - crates.io - toml

YAML

Rust crates.io - serde-yaml

Base64

base64是相当简单的编码技术。他主要是用于将二进制数据编码成文本格式,从而满足某些限制,本身不对二进制数据作任何限制。base64只使用AZ, az, 09以及+/来编码数据(共64个字符,代表从0到63,6位)。

也就是说,他需要用一个字节(8位)的字符,来编码一个6位的数据,所以编码之后体积会膨胀到原先的约4/3。原来的数据应该是8位的倍数而不是6位的倍数,怎么办?用=作为padding来填充结果。3个原本的数据,编码的到4个字符。所以如果数据最后会补充padding length = 3 - origin length % 3。

例如:编码 hi(0x68, 0x69, 0110 1000 0110 1001),按照6位分就是011010, 000110 1001。也就是26, 6, 36。查表得aGk。还差了一个字符凑到3的倍数,补一个padding。所以最终的编码结果是aGk=

base64有变种base58(去掉0,I,l,O,+,/方便人阅读), base32,base16(即用Hexadecimal表达二进制数据)等。原理都一样。

Python - std - base64

除了 base64,还有 base32,base16 等编码也在这个库中。

>>> base64.b64encode(b"hello world")
b'aGVsbG8gd29ybGQ='
>>> base64.b64decode(b'aGVsbG8gd29ybGQ=')
b'hello world'

Protobuf

golang 和 Protobuf 的集成使用时最常见的。

Programming Language Parser

其实和本章的主题关系微乎其微,只是正好放在这里了。

GitHub - tree-sitter/tree-sitter: An incremental parsing system for programming tools 这个库集成了很多编程语言的 parser。包括但不限于以下语言,具体可以参考 官网给出的列表

  • C
  • JSON
  • HTML
  • Go
  • Python
  • Markdown
  • Makefile
  • gitignore
  • SQL

C, Go, Rust, Java, Python 都可以使用它对这些语言的 parser。你也可以根据这个库自定义语法规则,为自己的语言实现 Parser。它有规定自己的 DSL,使用类似 EBNF 的文法。

// 对下面这段js代码执行命令 tree-sitter generate,会生成parser.c。一般将它编译为静态库使用
module.exports = grammar({
  name: 'YOUR_LANGUAGE_NAME',

  rules: {
    // TODO: add the actual grammar rules
    source_file: $ => 'hello'
  }
});