序列化
程序通常(至少)使用两种形式的数据:
- 在内存中,数据保存在对象、结构体、列表、数组、散列表、树等中。 这些数据结构针对 CPU 的高效访问和操作进行了优化(通常使用指针)。
- 如果要将数据写入文件,或通过网络发送,则必须将其 编码(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
的参数是字符串,dump
和 load
的参数是文件。
>>> 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 文本的时候,我们可能有两种需求,以及相反的序列化的需求。
- 解析成一个 strongly typed Rust data structure。比如一些 API 已经规定好了返回的数据格式。
- 解析成一个 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只使用A
–Z
, a
–z
, 0
–9
以及+
和/
来编码数据(共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 的文法。