7 个版本 (2 个稳定版本)

1.1.0 2024年7月25日
1.0.0 2024年3月13日
0.5.0 2024年2月25日
0.4.0 2024年2月21日
0.1.0 2023年6月17日

#324 in 解析器实现

Download history 38/week @ 2024-04-30 41/week @ 2024-05-07 90/week @ 2024-05-14 165/week @ 2024-05-21 78/week @ 2024-05-28 82/week @ 2024-06-04 113/week @ 2024-06-11 46/week @ 2024-06-18 80/week @ 2024-06-25 30/week @ 2024-07-02 52/week @ 2024-07-09 25/week @ 2024-07-16 251/week @ 2024-07-23 275/week @ 2024-07-30 271/week @ 2024-08-06 339/week @ 2024-08-13

1,138 每月下载次数
2 个crate中使用(通过 shitpost_markov

MIT 许可证

1.5MB
1K SLoC

Actson Actions Status MIT license 最新版本 文档

Actson 是一个用于响应式应用程序和非阻塞I/O的低级JSON解析器。它是基于事件的,可以在异步代码中使用(例如与 Tokio 结合使用)。



Teaser Image


为什么还需要另一个JSON解析器?

  • 非阻塞。 响应式应用程序应使用非阻塞I/O,以便没有线程需要无限期地等待共享资源变得可用(参见 响应式宣言)。Actson 支持此模式。
  • 大数据。 Actson 可以处理任意大小的JSON文本,而无需将其完全加载到内存中。它非常快,并实现恒定的解析吞吐量(见下文 性能 部分)。
  • 基于事件。 Actson 在解析过程中产生事件,可用于流式传输。例如,如果您编写一个HTTP服务器,您可以同时接收文件并对其进行解析。

Actson 主要是为了 GeoJSONGeoRocket 中的支持而开发的,GeoRocket 是一个用于地理文件的高性能响应式数据存储。对于此应用程序,我们需要一种解析内容不同的非常大的JSON文件的方法。文件通过HTTP服务器接收,在从套接字读取的同时解析成JSON事件,并同时在数据库中进行索引。整个过程是异步运行的。

如果这个用例听起来很熟悉,那么Actson可能就是您的理想选择。下面了解更多关于其性能以及它如何与Serde JSON进行比较

使用方法

基于推送的解析

基于推送的解析是使用Actson最灵活的方式。将新的字节推入一个PushJsonFeeder中,然后让解析器消费这些字节,直到它返回Some(JsonEvent::NeedMoreInput)。重复此过程,直到您收到None,这意味着JSON文本的末尾已到达。如果JSON文本无效或发生其他错误,解析器将返回Err

这种方法非常底层,但让您能够在字节可用时随时向解析器提供新字节,并在需要时生成JSON事件。

use actson::{JsonParser, JsonEvent};
use actson::feeder::{PushJsonFeeder, JsonFeeder};

let json = r#"{"name": "Elvis"}"#.as_bytes();

let feeder = PushJsonFeeder::new();
let mut parser = JsonParser::new(feeder);
let mut i = 0;
while let Some(event) = parser.next_event().unwrap() {
    match event {
        JsonEvent::NeedMoreInput => {
            // feed as many bytes as possible to the parser
            i += parser.feeder.push_bytes(&json[i..]);
            if i == json.len() {
                parser.feeder.done();
            }
        }

        JsonEvent::FieldName => assert!(matches!(parser.current_str(), Ok("name"))),
        JsonEvent::ValueString => assert!(matches!(parser.current_str(), Ok("Elvis"))),

        _ => {} // there are many other event types you may process here
    }
}

使用Tokio进行异步解析

Actson可以与Tokio一起使用,以异步方式解析JSON。

这里的主要思想是在循环中调用JsonParser::next_event()来解析JSON文档并生成事件。每当您得到JsonEvent::NeedMoreInput时,调用AsyncBufReaderJsonFeeder::fill_buf()以异步从输入中读取更多字节,并将它们提供给解析器。

[!NOTE] 必须启用tokio功能才能进行此操作。默认情况下已禁用。

use tokio::fs::File;
use tokio::io::{self, AsyncReadExt, BufReader};

use actson::{JsonParser, JsonEvent};
use actson::tokio::AsyncBufReaderJsonFeeder;

#[tokio::main]
async fn main() {
    let file = File::open("tests/fixtures/pass1.txt").await.unwrap();
    let reader = BufReader::new(file);

    let feeder = AsyncBufReaderJsonFeeder::new(reader);
    let mut parser = JsonParser::new(feeder);
    while let Some(event) = parser.next_event().unwrap() {
        match event {
            JsonEvent::NeedMoreInput => parser.feeder.fill_buf().await.unwrap(),
            _ => {} // do something useful with the event
        }
    }
}

BufReader解析

BufReaderJsonFeeder允许您从std::io::BufReader向解析器提供数据。

[!NOTE] 通过遵循此同步和阻塞的方法,您将错过Actson的响应式特性。我们建议您使用Actson与Tokio一起异步解析JSON(参见上文)。

use actson::{JsonParser, JsonEvent};
use actson::feeder::BufReaderJsonFeeder;

use std::fs::File;
use std::io::BufReader;

let file = File::open("tests/fixtures/pass1.txt").unwrap();
let reader = BufReader::new(file);

let feeder = BufReaderJsonFeeder::new(reader);
let mut parser = JsonParser::new(feeder);
while let Some(event) = parser.next_event().unwrap() {
    match event {
        JsonEvent::NeedMoreInput => parser.feeder.fill_buf().unwrap(),
        _ => {} // do something useful with the event
    }
}

解析字节切片

为了方便起见,SliceJsonFeeder允许您从字节切片向解析器提供数据。

use actson::{JsonParser, JsonEvent};
use actson::feeder::SliceJsonFeeder;

let json = r#"{"name": "Elvis"}"#.as_bytes();

let feeder = SliceJsonFeeder::new(json);
let mut parser = JsonParser::new(feeder);
while let Some(event) = parser.next_event().unwrap() {
    match event {
        JsonEvent::FieldName => assert!(matches!(parser.current_str(), Ok("name"))),
        JsonEvent::ValueString => assert!(matches!(parser.current_str(), Ok("Elvis"))),
        _ => {}
    }
}

解析到Serde JSON值

出于测试和兼容性原因,Actson可以将字节切片解析到Serde JSON值。

[!NOTE] 您需要启用serde_json功能才能进行此操作。

use actson::serde_json::from_slice;

let json = r#"{"name": "Elvis"}"#.as_bytes();
let value = from_slice(json).unwrap();

assert!(value.is_object());
assert_eq!(value["name"], "Elvis");

然而,如果您发现自己这样做,您可能不需要Actson的响应式特性,并且您的数据似乎完全适合内存。在这种情况下,您最有可能直接使用Serde JSON(参见下文比较)。

流模式解析(多个顶层JSON值)

如果您想解析多个顶层JSON值流,您可以启用流模式。值必须是明显可区分的。它们必须是自我界定值(即数组、对象、字符串)或关键字(即truefalsenull),或者它们必须由空格、至少一个自我界定值或至少一个关键字分隔。

示例流

1 2 3 true 4 5

[1,2,3][4,5,6]{"key": "value"} 7 8 9

"a""b"[1, 2, 3] {"key": "value"}

示例

use actson::feeder::SliceJsonFeeder;
use actson::options::JsonParserOptionsBuilder;
use actson::{JsonEvent, JsonParser};

let json = r#"1 2""{"key":"value"}
["a","b"]4true"#.as_bytes();

let feeder = SliceJsonFeeder::new(json);
let mut parser = JsonParser::new_with_options(
    feeder,
    JsonParserOptionsBuilder::default()
        .with_streaming(true)
        .build(),
);

let mut events = Vec::new();
while let Some(e) = parser.next_event().unwrap() {
    events.push(e);
}

assert_eq!(events, vec![
    JsonEvent::ValueInt,
    JsonEvent::ValueInt,
    JsonEvent::ValueString,
    JsonEvent::StartObject,
    JsonEvent::FieldName,
    JsonEvent::ValueString,
    JsonEvent::EndObject,
    JsonEvent::StartArray,
    JsonEvent::ValueString,
    JsonEvent::ValueString,
    JsonEvent::EndArray,
    JsonEvent::ValueInt,
    JsonEvent::ValueTrue,
]);

性能

Actson已针对与大型文件的最佳性能进行了优化。它呈线性扩展,这意味着它表现出恒定的解析速度和内存消耗,无论输入JSON文本的大小如何。

下方的图表展示了不同GeoJSON输入文件以及与Serde JSON相比,解析器的吞吐量运行时间

使用BufReader的Actson在所有测试的文件中表现最佳(actson-bufreader基准)。其吞吐量保持恒定,运行时间仅与输入大小线性增长。

同样,使用Tokio的其他Actson基准(actson-tokioactson-tokio-twotasks)也适用。异步代码有轻微的额外开销,但使用两个并发运行的Tokio任务(actson-tokio-twotasks)可以大部分抵消。

serde-value基准显示,随着文件变大,解析器的吞吐量会下降。这是因为它需要将整个内容加载到内存中(到Serde JSON的Value中)。serde-struct基准将文件反序列化为一个复制GeoJSON格式的结构体。它遭受与serde-value基准相同的问题,即整个文件需要加载到内存中。在这种情况下,由于自定义结构体比Serde JSON的Value小,并且测试系统有36 GB的RAM,因此对吞吐量的影响在图表中不可见。

serde-custom-deser基准是唯一与最慢的异步Actson基准actson-tokio(仅运行一个Tokio任务)性能相当的Serde基准。这是因为serde-custom-deser使用自定义反序列化器,避免了将整个文件加载到内存中的需要(见Serde网站上的示例)。这种非常具体的实现仅因为输入文件的结构已知,并且使用的GeoJSON文件不是深度嵌套的。这种解决方案不是通用的。

关于个别基准和测试文件的更多信息,请参阅此处

吞吐量(越高越好)

Throughput

在配备M3 Pro芯片和36 GB RAM的MacBook Pro 16" 2023上进行了测试。

运行时间(越低越好)

Runtime

在配备M3 Pro芯片和36 GB RAM的MacBook Pro 16" 2023上进行了测试。

我应该使用Actson还是Serde JSON?

如上基准所示,Actson在处理大型文件时表现最佳。然而,如果您的JSON输入文件较小(几个KB或可能1或2 MB),您可能应该坚持使用Serde JSON,它是一个经过实战检验的、稳固的解析器,在这种情况下将表现出极高的速度。

另一方面,如果您需要可扩展性,并且输入文件可以具有任意大小,或者您想要异步解析JSON,请使用Actson。

本节的目的是不使一个解析器看起来比另一个更好。Actson和Serde JSON是两个非常不同的库,每个都有自己的优缺点。以下表格可能有助于您决定是否需要Actson或是否应该优先考虑Serde JSON。

Actson Serde JSON
输入文件可以是任意大小(数GB) 输入文件仅为几个KB或MB
JSON文本是流式传输的,例如通过Web服务器 JSON文本存储在文件系统或内存中
您想要并发读取和解析JSON文本 顺序解析就足够了
解析不应阻塞应用程序中的其他任务(响应式 编程 JSON 文本非常小,解析速度足够快,或者您的应用程序不是反应式的,也不并行运行多个任务
您希望处理单个 JSON 事件 您更喜欢便利性,而不关心事件
JSON 文本的结构可能变化,或者根本不知道 结构非常清楚
您不需要反序列化(将 JSON 文本映射到结构体),或者由于 JSON 文本结构的可变或未知,反序列化是不可能的 您希望并且可以将 JSON 文本反序列化到结构体中

合规性

我们对 Actson 进行了彻底的测试,以确保它与 RFC 8259 兼容,可以解析有效的 JSON 文档,并拒绝无效的文档。

除了自己的单元测试之外,Actson 还通过了来自 JSON_checker.c 的测试,以及来自非常全面的 JSON 解析测试套件 的所有 283 个接受和拒绝测试。

其他语言

除了这里提供的 Rust 实现之外,还有一个 Java 实现

致谢

基于事件的解析代码和用于测试的 JSON 文件主要基于文件 JSON_checker.c 以及来自 JSON.org 的 JSON 测试套件,最初在 此许可 下发布(基本上是 MIT 许可证)。

目录 tests/json_test_suite 是一个指向由 Nicolas Seriot 精心整理的 JSON 解析测试套件 的 Git 子模块,该套件在 MIT 许可证下发布。

许可

Actson 在 MIT 许可证 下发布。有关更多信息,请参阅 LICENSE 文件。

依赖项