Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第十一章 数据的序列化、反序列化、持久化、可视化

11.1 概述

什么是序列化与反序列化

序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。

反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。

为什么需要序列化

在计算机系统中,数据存在于内存中时以结构化的对象形式存在,但当我们需要将数据跨进程、跨机器、跨时间传递时,就必须将其转换为一种统一的字节流格式。序列化解决的核心问题是:

场景说明示例
网络传输不同机器间传递数据,需将对象转为字节流RPC调用、REST API
数据持久化将内存中的对象保存到磁盘,以便后续恢复配置文件、数据库记录
进程间通信同一机器上不同进程间交换数据管道、消息队列
深拷贝通过序列化/反序列化实现对象的完整复制缓存快照

序列化的完整流程

一个典型的数据序列化与传输流程可以表示为:

$$ \text{原始对象} \xrightarrow{\text{编码}} \text{字节流} \xrightarrow{\text{传输}} \text{字节流} \xrightarrow{\text{解码}} \text{重建对象} $$

用数学语言描述,设原始对象为 $O$,序列化函数为 $S$,传输通道为 $T$,反序列化函数为 $D$,则完整过程为:

$$ O’ = D(T(S(O))) $$

理想情况下,我们希望 $O’ = O$,即反序列化后的对象与原始对象完全一致。这要求序列化格式能够无损地表达原始数据的所有信息。

序列化和反序列化的底层实现原理是什么?

11.2 常见序列化格式

常见的序列化数据格式有:JSONXML、BSON、YAML、TOML 等。每种格式都有其适用的场景和优缺点。

11.2.1 JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,由 Douglas Crockford 在 2001 年提出。它基于 JavaScript 的对象字面量语法,但独立于任何编程语言,几乎所有的现代编程语言都支持 JSON。

JSON 的核心特点:

  • 轻量级:相比 XML,JSON 没有冗余的标签,数据体积更小
  • 可读性好:人类可以直接阅读和编写
  • 广泛支持:几乎所有语言和平台都内置了 JSON 支持
  • 树形结构:支持嵌套的对象和数组

JSON 与 Rust 数据类型映射:

JSON 类型Rust 类型示例
number (整数)i32, i64, u32, u6442
number (浮点)f32, f643.14
stringString, &str"hello"
booleanbooltrue / false
nullOption<T> (None)null
arrayVec<T>, [T; N][1, 2, 3]
objectstruct, HashMap<K, V>{"key": "value"}
use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Person {
    first_name: String,
    last_name: String,
}

fn main() {
    let person = Person {
        first_name: "Graydon".to_string(),
        last_name: "Hoare".to_string(),
    };

    let json = serde_json::to_string_pretty(&person).unwrap();

    // Prints:
    //
    //    {
    //      "firstName": "Graydon",
    //      "lastName": "Hoare"
    //    }
    println!("{}", json);
}

11.2.2 XML

XML(eXtensible Markup Language,可扩展标记语言)是一种自描述性强的标记语言,由 W3C 于 1998 年标准化。XML 使用标签来描述数据的结构和语义,在 SOAP、SVG、HTML 等领域有广泛应用。

XML 的核心特点:

  • 自描述性强:标签本身携带语义信息
  • 严格的语法规范:必须有根元素、标签必须闭合
  • 支持命名空间:避免标签名冲突
  • 适合文档型数据:如配置文件、文档交换

JSON vs XML 对比:

特性JSONXML
数据体积较小较大(标签冗余)
可读性较好一般(标签较多)
解析速度较慢
数据类型原生支持需要 Schema 定义
注释支持不支持支持 <!-- -->
命名空间不支持支持
适用场景Web API、配置文档交换、SOAP

在 Rust 中,可以使用 quick-xml 库来处理 XML 数据:

use quick_xml::events::Event;
use quick_xml::Reader;
use std::io::BufReader;

fn main() {
    let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
    <config>
        <database>
            <host>localhost</host>
            <port>5432</port>
            <name>mydb</name>
        </database>
        <server>
            <host>0.0.0.0</host>
            <port>8080</port>
        </server>
    </config>"#;

    let mut reader = Reader::from_reader(xml.as_bytes());
    let mut buf = Vec::new();
    let mut depth = 0;

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(e)) => {
                depth += 1;
                println!("{:indent$}<{}>", "", e.name(), indent = depth * 2);
            }
            Ok(Event::Empty(e)) => {
                println!("{:indent$}<{} />", "", e.name(), indent = depth * 2);
            }
            Ok(Event::Text(e)) => {
                let text = e.unescape().unwrap();
                if !text.trim().is_empty() {
                    println!("{:indent$}{}", "", text.trim(), indent = depth * 2 + 2);
                }
            }
            Ok(Event::End(e)) => {
                depth -= 1;
                println!("{:indent$}</{}>", "", e.name(), indent = depth * 2);
            }
            Ok(Event::Eof) => break,
            Err(e) => eprintln!("Error: {:?}", e),
            _ => {}
        }
        buf.clear();
    }
}

11.2.3 YAML

YAML(YAML Ain’t Markup Language)是一种人类友好的数据序列化格式,广泛用于配置文件(如 Kubernetes 的资源清单、CI/CD 配置文件等)。

YAML 的核心特点:

  • 缩进表示层级:使用空格缩进而非花括号
  • 人类友好:语法简洁,接近自然语言
  • 支持注释:使用 # 添加注释
  • 支持复杂数据类型:锚点(&)和别名(*)实现引用
# 应用配置
server:
  host: 0.0.0.0
  port: 8080
  workers: 4

database:
  url: "postgres://user:pass@localhost/mydb"
  pool_size: 10
  timeout: 30s

logging:
  level: info
  format: json

在 Rust 中,可以使用 serde_yaml 来处理 YAML:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Config {
    server: Server,
    database: Database,
    logging: Logging,
}

#[derive(Debug, Deserialize)]
struct Server {
    host: String,
    port: u16,
    workers: usize,
}

#[derive(Debug, Deserialize)]
struct Database {
    url: String,
    pool_size: u32,
    timeout: String,
}

#[derive(Debug, Deserialize)]
struct Logging {
    level: String,
    format: String,
}

fn main() {
    let yaml = r#"
server:
  host: 0.0.0.0
  port: 8080
  workers: 4

database:
  url: "postgres://user:pass@localhost/mydb"
  pool_size: 10
  timeout: 30s

logging:
  level: info
  format: json
"#;

    let config: Config = serde_yaml::from_str(yaml).unwrap();
    println!("Server: {}:{}", config.server.host, config.server.port);
    println!("Database pool: {}", config.database.pool_size);
    println!("Log level: {}", config.logging.level);
}

11.2.4 TOML

TOML(Tom’s Obvious, Minimal Language)由 GitHub 联合创始人 Tom Preston-Werner 创建,是一种语义明确、易于阅读的配置文件格式。Rust 社区对 TOML 有着天然的支持——Cargo.toml 就是 TOML 格式的配置文件。

TOML 的核心特点:

  • 语义明确:类型清晰,不会产生歧义
  • INI 风格:使用 [section]key = value 的形式
  • Rust 生态首选:Cargo.toml 就是 TOML
  • 支持多种数据类型:字符串、整数、浮点数、布尔值、日期时间、数组、内联表
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[profile.release]
opt-level = 3
lto = true

在 Rust 中,可以使用 toml crate 来解析 TOML:

use serde::Deserialize;
use std::fs;

#[derive(Debug, Deserialize)]
struct AppConfig {
    package: Package,
    dependencies: std::collections::HashMap<String, toml::Value>,
}

#[derive(Debug, Deserialize)]
struct Package {
    name: String,
    version: String,
    edition: String,
}

fn main() {
    let content = fs::read_to_string("Cargo.toml").unwrap();
    let config: AppConfig = toml::from_str(&content).unwrap();
    println!("项目名称: {}", config.package.name);
    println!("版本: {}", config.package.version);

    for (name, value) in &config.dependencies {
        println!("依赖 {}: {:?}", name, value);
    }
}

11.2.5 BSON

BSON(Binary JSON)是一种二进制形式的 JSON 序列化格式,由 MongoDB 开发。BSON 扩展了 JSON 的数据类型,支持日期时间、二进制数据、正则表达式等。

BSON 的主要优势在于:

  • 二进制格式:解析速度比文本格式的 JSON 更快
  • 扩展数据类型:支持 DateBinaryObjectIdRegex
  • MongoDB 原生格式:MongoDB 内部使用 BSON 存储和传输数据

11.2.6 Protocol Buffers(Protobuf)

Protocol Buffers(简称 Protobuf)是 Google 开发的高效二进制序列化协议。与 JSON/XML 不同,Protobuf 需要先定义 .proto 模式文件,然后通过编译器生成各语言的代码。

// person.proto
syntax = "proto3";

message Person {
    string name = 1;
    int32 age = 2;
    repeated string emails = 3;
}

Protobuf 的核心优势:

  • 极致性能:二进制编码,体积小、解析快
  • 强类型约束:通过 .proto 文件定义数据结构
  • 向前/向后兼容:字段编号机制保证兼容性
  • 跨语言支持:支持 C++、Java、Python、Go、Rust 等多种语言

11.2.7 各格式对比总结

格式可读性解析速度数据体积数据类型适用场景
JSON基本类型Web API、前后端交互
XML基本类型文档交换、SOAP、SVG
YAML基本类型配置文件、K8s 清单
TOML基本类型+日期Rust/Cargo 配置
BSON扩展类型MongoDB 存储
Protobuf极快极小强类型高性能 RPC、微服务

11.3 Rust 中的 Serde 生态

11.3.1 Serde 框架概述

Serde 是 Rust 生态中最核心的序列化/反序列化框架,其名字来源于 SErialize / DEserialize 的缩写。Serde 采用了一种优雅的设计:将数据结构的定义与序列化格式解耦,通过 trait 抽象实现“一次定义,多种格式输出“。

Serde 的架构可以用以下公式表达:

$$ \text{Data} \xleftrightarrow{\text{Serialize/Deserialize}} \text{Serializer/Deserializer} \xleftrightarrow{\text{Format}} \text{JSON/XML/YAML/…} $$

11.3.2 核心 Trait

Serde 定义了两个核心 trait:

  • Serialize:将 Rust 数据结构转换为通用的中间表示
  • Deserialize:从通用的中间表示重建 Rust 数据结构

这两个 trait 是 Serde 生态的基石,所有数据格式(JSON、XML、YAML 等)都基于这两个 trait 实现。

11.3.3 常用派生宏

在大多数场景下,我们不需要手动实现 SerializeDeserialize,只需使用 #[derive] 派生宏即可:

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: u64,
    name: String,
    email: Option<String>,
    roles: Vec<String>,
}
}

11.3.4 常用属性

Serde 提供了丰富的属性来控制序列化行为:

属性说明示例
#[serde(rename_all = "camelCase")]批量重命名所有字段first_name -> firstName
#[serde(rename = "name")]重命名单个字段字段名映射
#[serde(skip)]跳过序列化/反序列化敏感字段
#[serde(skip_serializing)]仅跳过序列化密码字段
#[serde(skip_deserializing)]仅跳过反序列化计算字段
#[serde(default)]反序列化时使用默认值可选字段
#[serde(flatten)]扁平化嵌套结构合并层级
#[serde(with = "module")]自定义序列化模块特殊格式
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    status_code: u16,
    message: String,
    #[serde(skip_serializing)]
    debug_info: String,
    #[serde(default)]
    retry_after: Option<u32>,
    #[serde(flatten)]
    metadata: Metadata,
}

#[derive(Serialize, Deserialize, Debug)]
struct Metadata {
    request_id: String,
    timestamp: i64,
}

fn main() {
    let resp = ApiResponse {
        status_code: 200,
        message: "OK".to_string(),
        debug_info: "internal trace".to_string(),
        retry_after: None,
        metadata: Metadata {
            request_id: "abc-123".to_string(),
            timestamp: 1700000000,
        },
    };

    let json = serde_json::to_string_pretty(&resp).unwrap();
    println!("{}", json);
}

11.3.5 Person 序列化示例

以下是一个完整的 Person 结构体序列化示例:

use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Person {
    first_name: String,
    last_name: String,
}

fn main() {
    let person = Person {
        first_name: "Graydon".to_string(),
        last_name: "Hoare".to_string(),
    };

    let json = serde_json::to_string_pretty(&person).unwrap();

    // Prints:
    //
    //    {
    //      "firstName": "Graydon",
    //      "lastName": "Hoare"
    //    }
    println!("{}", json);
}

11.3.6 自定义 Deserialize 实现

在某些场景下,派生宏无法满足需求(如自定义数据格式、条件解析等),此时需要手动实现 Deserialize trait。以下是一个完整的 Duration 结构体自定义反序列化实现:

use std::fmt;
use serde::Serialize;
use serde::de::{self, Deserialize, Deserializer, Visitor, SeqAccess, MapAccess};

#[allow(dead_code)]
#[derive(Serialize)]
struct Duration {
    secs: u64,
    nanos: u32,
}

impl Duration {
    fn new(secs: u64, nanos: u32) -> Self {
        Duration{
            secs,
            nanos
        }
    }
}

impl<'de> Deserialize<'de> for Duration {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        enum Field { Secs, Nanos }

        // This part could also be generated independently by:
        //
        //    #[derive(Deserialize)]
        //    #[serde(field_identifier, rename_all = "lowercase")]
        //    enum Field { Secs, Nanos }
        impl<'de> Deserialize<'de> for Field {
            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
            where
                D: Deserializer<'de>,
            {
                struct FieldVisitor;

                impl<'de> Visitor<'de> for FieldVisitor {
                    type Value = Field;

                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                        formatter.write_str("`secs` or `nanos`")
                    }

                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
                    where
                        E: de::Error,
                    {
                        match value {
                            "secs" => Ok(Field::Secs),
                            "nanos" => Ok(Field::Nanos),
                            _ => Err(de::Error::unknown_field(value, FIELDS)),
                        }
                    }
                }

                deserializer.deserialize_identifier(FieldVisitor)
            }
        }

        struct DurationVisitor;

        impl<'de> Visitor<'de> for DurationVisitor {
            type Value = Duration;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct Duration")
            }

            fn visit_seq<V>(self, mut seq: V) -> Result<Duration, V::Error>
            where
                V: SeqAccess<'de>,
            {
                let secs = seq.next_element()?
                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
                let nanos = seq.next_element()?
                    .ok_or_else(|| de::Error::invalid_length(1, &self))?;
                Ok(Duration::new(secs, nanos))
            }

            fn visit_map<V>(self, mut map: V) -> Result<Duration, V::Error>
            where
                V: MapAccess<'de>,
            {
                let mut secs = None;
                let mut nanos = None;
                while let Some(key) = map.next_key()? {
                    match key {
                        Field::Secs => {
                            if secs.is_some() {
                                return Err(de::Error::duplicate_field("secs"));
                            }
                            secs = Some(map.next_value()?);
                        }
                        Field::Nanos => {
                            if nanos.is_some() {
                                return Err(de::Error::duplicate_field("nanos"));
                            }
                            nanos = Some(map.next_value()?);
                        }
                    }
                }
                let secs = secs.ok_or_else(|| de::Error::missing_field("secs"))?;
                let nanos = nanos.ok_or_else(|| de::Error::missing_field("nanos"))?;
                Ok(Duration::new(secs, nanos))
            }
        }

        const FIELDS: &'static [&'static str] = &["secs", "nanos"];
        deserializer.deserialize_struct("Duration", FIELDS, DurationVisitor)
    }
}

fn main(){
    let duration = Duration::new(10u64,140_000_000u32);
    let json = serde_json::to_string_pretty(&duration).unwrap();
    println!("{}",json);
    
    if let Ok(duration2) = serde_json::from_str::<Duration>(&json){
        println!("secs:{}, nanos:{}",duration2.secs, duration2.nanos); 
    }
    
}

11.3.7 serde_json 常用 API

API说明返回类型
serde_json::to_string(&val)序列化为紧凑 JSON 字符串Result<String>
serde_json::to_string_pretty(&val)序列化为格式化 JSON 字符串Result<String>
serde_json::to_vec(&val)序列化为字节向量Result<Vec<u8>>
serde_json::to_writer(writer, &val)序列化并写入 WriterResult<()>
serde_json::from_str::<T>(&str)从字符串反序列化Result<T>
serde_json::from_slice::<T>(&[u8])从字节切片反序列化Result<T>
serde_json::from_reader::<T, R>(reader)从 Reader 反序列化Result<T>
serde_json::Value动态 JSON 值类型枚举类型

11.3.8 错误处理

Serde 的错误处理通过 serde_json::Error 类型实现,它提供了丰富的错误信息:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
    name: String,
    port: u16,
}

fn main() {
    let json = r#"{"name": "test", "port": "abc"}"#;
    
    match serde_json::from_str::<Config>(json) {
        Ok(config) => println!("配置加载成功: {:?}", config),
        Err(e) => {
            eprintln!("配置解析失败: {}", e);
            // 输出详细错误分类
            if e.is_syntax() {
                eprintln!("  -> JSON 语法错误");
            } else if e.is_data() {
                eprintln!("  -> 数据类型不匹配");
            } else if e.is_eof() {
                eprintln!("  -> JSON 数据不完整");
            }
        }
    }
}

11.3.9 相关库参考

Rust 库:

Java 库(对比参考):

  • FasterXML — Java 高性能 XML/JSON 处理
  • jackson — Java 多格式数据处理
  • hessian — 二进制 RPC 协议

FastJson小技巧——@JSONField的史上最全最详细讲解——一看就会

11.4 数据持久化

11.4.1 什么是持久化

持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML 数据文件中等等。

从信息论的角度看,持久化是将易失性存储(内存,$T_{volatile} \approx 10^{-9}$ 秒访问延迟)中的数据转移到非易失性存储(磁盘/SSD,$T_{nonvolatile} \approx 10^{-6}$ 秒访问延迟)的过程:

$$ \text{Memory Data} \xrightarrow{\text{Serialize}} \text{Byte Stream} \xrightarrow{\text{Write}} \text{Persistent Storage} $$

11.4.2 持久化方式

方式说明适用场景Rust 代表库
文件存储JSON/TOML/YAML 等配置文件应用配置、小型数据serde_json, toml
关系型数据库SQL 数据库,结构化存储业务数据、事务处理sea-orm, sqlx
NoSQL 数据库键值存储、文档存储高并发、灵活 Schemaredis, mongodb
对象存储S3 兼容的分布式存储大文件、媒体资源minio, rustfs

11.4.3 文件存储示例

文件存储是最简单的持久化方式,适合配置文件和小型数据集:

use serde::{Serialize, Deserialize};
use std::fs;

#[derive(Serialize, Deserialize, Debug)]
struct AppSettings {
    theme: String,
    language: String,
    max_connections: u32,
    auto_save: bool,
}

fn save_settings(settings: &AppSettings, path: &str) -> std::io::Result<()> {
    let json = serde_json::to_string_pretty(settings).unwrap();
    fs::write(path, json)
}

fn load_settings(path: &str) -> std::io::Result<AppSettings> {
    let json = fs::read_to_string(path)?;
    let settings: AppSettings = serde_json::from_str(&json).unwrap();
    Ok(settings)
}

fn main() {
    let settings = AppSettings {
        theme: "dark".to_string(),
        language: "zh-CN".to_string(),
        max_connections: 100,
        auto_save: true,
    };

    save_settings(&settings, "settings.json").unwrap();
    println!("配置已保存");

    let loaded = load_settings("settings.json").unwrap();
    println!("加载的配置: {:?}", loaded);
}

11.4.4 数据库存储

sea-orm 是 Rust 生态中优秀的异步 ORM 框架,基于 SQLx 构建,提供了类型安全的数据库操作接口。

以下是一个使用 SQLite 进行数据持久化的示例:

use rusqlite::{Connection, Result};
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: Option<i64>,
    name: String,
    email: String,
    age: u32,
}

fn init_db(conn: &Connection) -> Result<()> {
    conn.execute(
        "CREATE TABLE IF NOT EXISTS users (
            id    INTEGER PRIMARY KEY AUTOINCREMENT,
            name  TEXT NOT NULL,
            email TEXT NOT NULL UNIQUE,
            age   INTEGER NOT NULL
        )",
        [],
    )?;
    Ok(())
}

fn insert_user(conn: &Connection, user: &User) -> Result<i64> {
    conn.execute(
        "INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)",
        [&user.name, &user.email, &user.age.to_string()],
    )?;
    Ok(conn.last_insert_rowid())
}

fn query_users(conn: &Connection) -> Result<Vec<User>> {
    let mut stmt = conn.prepare("SELECT id, name, email, age FROM users")?;
    let user_iter = stmt.query_map([], |row| {
        Ok(User {
            id: Some(row.get(0)?),
            name: row.get(1)?,
            email: row.get(2)?,
            age: row.get(3)?,
        })
    })?;

    let mut users = Vec::new();
    for user in user_iter {
        users.push(user?);
    }
    Ok(users)
}

fn main() -> Result<()> {
    let conn = Connection::open("users.db")?;
    init_db(&conn)?;

    let user = User {
        id: None,
        name: "张三".to_string(),
        email: "zhangsan@example.com".to_string(),
        age: 28,
    };

    let id = insert_user(&conn, &user)?;
    println!("插入用户成功,ID: {}", id);

    let users = query_users(&conn)?;
    let json = serde_json::to_string_pretty(&users).unwrap();
    println!("所有用户:\n{}", json);

    Ok(())
}

11.4.5 对象存储

对象存储是一种适合存储海量非结构化数据(图片、视频、日志文件等)的存储方案,兼容 Amazon S3 API。

  • MINIO — 全球增长最快的对象存储系统
  • RustFS — 世界上增长最快的分布式对象存储
#![allow(unused)]
fn main() {
// 使用 minio-rs 上传文件的简要示例
// Cargo.toml: minio = "0.2"

use minio::s3::args::*;
use minio::s3::Client;

async fn upload_file(
    client: &Client,
    bucket: &str,
    object_name: &str,
    file_path: &str,
) {
    let args = PutObjectArgs::new(bucket, object_name, file_path);
    client.put_object(args).await.unwrap();
    println!("文件 {} 上传成功", object_name);
}
}

11.5 数据可视化

11.5.1 可视化的意义

数据可视化(Data Visualization)是将数据转化为图形或图像的过程,目的是让数据“说话“,帮助人们更直观地理解数据中隐藏的模式、趋势和关联。

在数据分析中,可视化扮演着不可替代的角色:

  • 趋势分析:股价走势、气温变化、降雨量统计
  • 对比分析:不同产品销量对比、部门绩效对比
  • 分布分析:用户年龄分布、收入分布
  • 关联分析:变量之间的相关性
  • 实时监控:服务器负载、系统健康度、网络流量

11.5.2 常见可视化类型

类型用途示例场景
折线图展示趋势变化股价走势、气温变化
柱状图展示分类对比各部门销售额、月度收入
饼图展示占比分布市场份额、预算分配
散点图展示相关性身高与体重关系
热力图展示密度分布用户活跃时段、地理热力
地图展示地理分布销售区域分布、疫情分布

11.5.3 Rust 可视化库

Rust 生态中有多个可视化库可供选择:

特点适用场景
plotters纯 Rust 绑图库,支持 SVG/PNG/BMP服务端绑图、报告生成
plotly交互式图表,基于 Plotly.jsWeb 可视化、仪表盘
egui即时模式 GUI 框架桌面应用内嵌图表
icedElm 架构 GUI 框架跨平台桌面应用

11.5.4 plotters 折线图示例

plotters 是 Rust 中最流行的纯绑图库,无需外部依赖,支持多种输出格式:

use plotters::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let root = BitMapBackend::new("line_chart.png", (800, 600))
        .into_drawing_area();
    root.fill(&WHITE)?;
    
    // 创建绑图区域
    let mut chart = ChartBuilder::on(&root)
        .caption("月度销售额趋势", ("Microsoft YaHei", 30))
        .x_label_area_size(40)
        .y_label_area_size(60)
        .margin(10)
        .build_cartesian_2d(1u32..12u32, 0f64..100f64)?;

    chart.configure_mesh()
        .x_desc("月份")
        .y_desc("销售额(万元)")
        .draw()?;

    // 绘制折线
    let data: Vec<(u32, f64)> = vec![
        (1, 25.0), (2, 32.0), (3, 28.0), (4, 45.0),
        (5, 52.0), (6, 48.0), (7, 61.0), (8, 58.0),
        (9, 72.0), (10, 68.0), (11, 80.0), (12, 95.0),
    ];

    chart.draw_series(LineSeries::new(
        data.iter().map(|&(x, y)| (x, y)),
        &RED,
    ))?
    .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));

    root.present()?;
    println!("折线图已保存为 line_chart.png");
    Ok(())
}

11.5.5 plotters 柱状图示例

use plotters::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let root = BitMapBackend::new("bar_chart.png", (800, 600))
        .into_drawing_area();
    root.fill(&WHITE)?;

    let mut chart = ChartBuilder::on(&root)
        .caption("各编程语言使用率", ("Microsoft YaHei", 30))
        .x_label_area_size(50)
        .y_label_area_size(60)
        .margin(10)
        .build_cartesian_2d(0usize..5usize, 0f64..40f64)?;

    chart.configure_mesh()
        .x_desc("编程语言")
        .y_desc("使用率 (%)")
        .x_labels(&["Rust", "Python", "Java", "Go", "C++"])
        .draw()?;

    let data: Vec<(usize, f64)> = vec![
        (0, 13.5), (1, 28.1), (2, 16.3), (3, 8.7), (4, 10.2),
    ];

    let colors = [RED, BLUE, GREEN, ORANGE, PURPLE];

    chart.draw_series(
        data.iter().enumerate().map(|(i, &(x, y))| {
            Rectangle::new(
                [(x, 0), (x + 1, y)],
                colors[i].filled(),
            )
        }),
    )?;

    root.present()?;
    println!("柱状图已保存为 bar_chart.png");
    Ok(())
}

11.6 数据融合

11.6.1 什么是数据融合

数据融合(Data Fusion)是一种将来自不同来源、不同格式或不同结构的数据集成到一个统一的数据模型或数据集中的过程。它不仅仅是简单的数据拼接,而是一个复杂的系统工程,涉及到数据的采集、清洗、转换、整合和存储等多个环节。

数据融合是什么?

11.6.2 ETL 流程

数据融合的核心流程是 ETL(Extract-Transform-Load):

$$ \text{数据源} \xrightarrow{\text{Extract(抽取)}} \text{原始数据} \xrightarrow{\text{Transform(转换)}} \text{清洗后数据} \xrightarrow{\text{Load(加载)}} \text{目标系统} $$

阶段说明关键操作
Extract(抽取)从多个数据源提取数据API 调用、数据库查询、文件读取
Transform(转换)清洗和转换数据去重、格式转换、类型映射、聚合计算
Load(加载)将处理后的数据写入目标批量插入、增量更新、实时流写入

11.6.3 数据清洗与转换

数据融合中最关键的环节是数据清洗和转换,常见操作包括:

  • 去重:删除重复记录,保证数据唯一性
  • 格式统一:日期格式、编码格式、数值精度的统一
  • 缺失值处理:填充默认值、插值或标记为空
  • 类型转换:字符串转数值、时间戳转日期等
  • 数据校验:范围检查、正则匹配、业务规则验证
  • 聚合计算:分组统计、滚动窗口、连接关联
use serde::{Serialize, Deserialize};
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Record {
    id: String,
    name: String,
    value: f64,
    source: String,
}

/// 数据清洗:去重和格式标准化
fn deduplicate(records: Vec<Record>) -> Vec<Record> {
    let mut seen = HashMap::new();
    for record in records {
        seen.entry(record.id.clone())
            .or_insert(record);
    }
    seen.into_values().collect()
}

/// 数据转换:值域映射
fn normalize_values(records: &mut [Record], min: f64, max: f64) {
    let range = max - min;
    for record in records.iter_mut() {
        record.value = (record.value - min) / range;
    }
}

/// 数据融合:合并多个来源的数据
fn merge_sources(sources: Vec<Vec<Record>>) -> Vec<Record> {
    let all: Vec<Record> = sources.into_iter().flatten().collect();
    let mut deduped = deduplicate(all);
    if let (Some(&min_val), Some(&max_val)) = (
        deduped.iter().map(|r| r.value).reduce(f64::min),
        deduped.iter().map(|r| r.value).reduce(f64::max),
    ) {
        normalize_values(&mut deduped, min_val, max_val);
    }
    deduped
}

fn main() {
    let source_a = vec![
        Record { id: "1".into(), name: "Alpha".into(), value: 100.0, source: "A".into() },
        Record { id: "2".into(), name: "Beta".into(), value: 200.0, source: "A".into() },
    ];
    let source_b = vec![
        Record { id: "1".into(), name: "Alpha".into(), value: 100.0, source: "B".into() },
        Record { id: "3".into(), name: "Gamma".into(), value: 300.0, source: "B".into() },
    ];

    let merged = merge_sources(vec![source_a, source_b]);
    println!("融合后共 {} 条记录:", merged.len());
    for r in &merged {
        println!("  {} - {} (normalized: {:.2})", r.id, r.name, r.value);
    }
}

11.7 列式存储与 Apache Arrow

11.7.1 行式存储 vs 列式存储

传统数据库大多采用行式存储(Row-oriented Storage),即一行数据连续存储。而列式存储(Column-oriented Storage)则将每一列的数据连续存储。两种存储方式各有优劣:

特性行式存储列式存储
写入方式逐行写入,适合 OLTP逐列写入,适合 OLAP
读取效率读取整行快读取特定列快
压缩比较低(不同类型数据混合)较高(同类型数据连续)
适用场景事务处理、点查询分析查询、聚合统计
代表系统MySQL、PostgreSQLClickHouse、Arrow

11.7.2 Apache Arrow 概述

Apache Arrow 是一种基于内存的列式数据结构,它的出现就是为了解决系统到系统之间的数据传输问题。在分布式系统内部,每个系统都有自己的内存格式,大量的 CPU 资源被消耗在序列化和反序列化过程中,并且由于每个项目都有自己的实现,没有一个明确的标准,造成各个系统都在重复着复制、转换工作,这种问题在微服务系统架构出现之后更加明显,Arrow 的出现就是为了解决这一问题。

Arrow 的核心优势在于:

  • 零拷贝读取:不同系统之间共享内存,无需序列化/反序列化
  • 列式内存格式:天然适合向量化操作和分析查询
  • 语言无关:支持 C/C++、Java、Python、Rust 等多种语言
  • 标准化:统一的内存格式规范,消除格式转换开销

11.7.3 SIMD 加速

Apache Arrow 的列式内存布局天然适合 SIMD(Single Instruction, Multiple Data)指令集加速。由于同一列的数据类型相同且连续存储,CPU 可以使用一条指令同时处理多个数据元素:

设列中有 $n$ 个元素,标量处理的时间复杂度为 $O(n)$,而 SIMD 处理(假设向量宽度为 $w$)的时间复杂度为 $O(n/w)$,理论上可获得 $w$ 倍的加速比。

11.7.4 DataFusion 查询引擎

DataFusion 是基于 Apache Arrow 构建的查询引擎,用 Rust 编写,提供了 SQL 查询能力和 DataFrame API,适合构建高性能的数据分析系统。

11.7.5 Rust arrow crate 示例

以下示例展示了如何使用 Rust 的 arrow crate 创建和操作列式数据:

use arrow::array::{Float64Array, Int64Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use std::sync::Arc;

fn main() {
    // 定义 Schema
    let schema = Schema::new(vec![
        Field::new("city", DataType::Utf8, false),
        Field::new("population", DataType::Int64, false),
        Field::new("avg_temperature", DataType::Float64, false),
    ]);

    // 创建列数据
    let cities = StringArray::from(vec![
        "北京", "上海", "广州", "深圳", "杭州",
    ]);
    let populations = Int64Array::from(vec![
        21_540_000, 24_870_000, 18_680_000, 17_560_000, 12_200_000,
    ]);
    let temperatures = Float64Array::from(vec![
        12.6, 16.1, 22.0, 22.4, 16.9,
    ]);

    // 创建 RecordBatch
    let batch = RecordBatch::try_new(
        Arc::new(schema),
        vec![
            Arc::new(cities),
            Arc::new(populations),
            Arc::new(temperatures),
        ],
    ).unwrap();

    println!("RecordBatch 包含 {} 行, {} 列", batch.num_rows(), batch.num_columns());
    println!("Schema: {:?}", batch.schema());

    // 访问列数据
    let temp_col = batch
        .column(2)
        .as_any()
        .downcast_ref::<Float64Array>()
        .unwrap();
    println!("平均温度: {:?}", temp_col.values());
}

11.7.6 相关链接

11.8 总结

序列化格式对比

格式可读性解析速度数据体积扩展性Rust 支持适用场景
JSONserde_jsonWeb API、配置
XMLquick-xml文档交换、SOAP
YAMLserde_yaml配置文件
TOMLtomlCargo 配置
BSONbsonMongoDB
Protobuf极快极小prost高性能 RPC

持久化方案对比

方案类型性能事务支持Rust 代表库适用场景
JSON 文件文件存储serde_json小型配置
SQLite嵌入式数据库ACIDrusqlite本地应用
MySQL关系型数据库ACIDsea-orm, sqlx服务端应用
PostgreSQL关系型数据库ACIDsea-orm, sqlx服务端应用
Oracle关系型数据库ACIDsea-orm, sqlx服务端应用
Redis内存数据库极高有限redis缓存、会话
MinIO/RustFS对象存储最终一致minio大文件存储

Rust 数据生态全景

Rust 在数据处理领域拥有完善的生态体系:

数据获取 ──→ 数据序列化 ──→ 数据存储 ──→ 数据分析 ──→ 数据可视化
   │              │              │              │              │
   ├─ HTTP       ├─ serde       ├─ sea-orm     ├─ arrow       ├─ plotters
   ├─ gRPC       ├─ serde_json  ├─ sqlx        ├─ datafusion  ├─ plotly
   ├─ CSV        ├─ serde_yaml  ├─ redis       ├─ polars      ├─ egui
   ├─ 数据库     ├─ quick-xml   ├─ rusqlite    └─ ndarray     └─ iced
   └─ 文件       └─ toml        └─ minio

11.9 练习题

练习 1:使用 serde_json 将以下 Rust 结构体序列化为 JSON,并要求字段名使用 snake_case,同时跳过 password 字段的序列化。

#![allow(unused)]
fn main() {
struct Account {
    user_id: u64,
    username: String,
    email: String,
    password: String,
    created_at: String,
}
}

练习 2:编写一个程序,读取一个 TOML 配置文件,解析为 Rust 结构体,并打印其中的数据库连接信息。

练习 3:使用 quick-xml 解析以下 XML 数据,提取所有 <book> 标签中的 titleauthor 属性:

<library>
    <book title="Rust编程之道" author="张三"/>
    <book title="Rust实战" author="李四"/>
    <book title="Rust系统编程" author="王五"/>
</library>

练习 4:实现一个简单的 ETL 流程:从 CSV 文件中读取数据(Extract),将价格字段从字符串转换为浮点数(Transform),然后将结果保存为 JSON 文件(Load)。

练习 5:使用 rusqlite 创建一个学生信息表,包含学号、姓名、成绩三个字段,实现插入、查询、更新和删除操作。

练习 6:使用 plotters 库绘制一个散点图,展示以下数据点的分布:

#![allow(unused)]
fn main() {
let data: Vec<(f64, f64)> = vec![
    (1.0, 2.3), (2.0, 4.1), (3.0, 5.8), (4.0, 8.2),
    (5.0, 9.7), (6.0, 12.1), (7.0, 14.5), (8.0, 16.0),
];
}

练习 7:使用 arrow crate 创建一个包含 1000 个随机浮点数的列式数据集,计算其均值和标准差,并与使用普通 Vec<f64> 的计算进行性能对比。

练习 8:设计一个数据融合方案:假设你有两个数据源,一个是 JSON 格式的用户信息(包含 user_id、name),另一个是 CSV 格式的订单信息(包含 user_id、amount、date),请编写程序将两个数据源按 user_id 进行关联,生成一个包含用户名称和订单总额的汇总报告。