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 解析器实现
1,138 每月下载次数
在 2 个crate中使用(通过 shitpost_markov)
1.5MB
1K SLoC
Actson
Actson 是一个用于响应式应用程序和非阻塞I/O的低级JSON解析器。它是基于事件的,可以在异步代码中使用(例如与 Tokio 结合使用)。
为什么还需要另一个JSON解析器?
- 非阻塞。 响应式应用程序应使用非阻塞I/O,以便没有线程需要无限期地等待共享资源变得可用(参见 响应式宣言)。Actson 支持此模式。
- 大数据。 Actson 可以处理任意大小的JSON文本,而无需将其完全加载到内存中。它非常快,并实现恒定的解析吞吐量(见下文 性能 部分)。
- 基于事件。 Actson 在解析过程中产生事件,可用于流式传输。例如,如果您编写一个HTTP服务器,您可以同时接收文件并对其进行解析。
Actson 主要是为了 GeoJSON 在 GeoRocket 中的支持而开发的,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值流,您可以启用流模式。值必须是明显可区分的。它们必须是自我界定值(即数组、对象、字符串)或关键字(即true
、false
、null
),或者它们必须由空格、至少一个自我界定值或至少一个关键字分隔。
示例流
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-tokio
和actson-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文件不是深度嵌套的。这种解决方案不是通用的。
关于个别基准和测试文件的更多信息,请参阅此处。
吞吐量(越高越好)
在配备M3 Pro芯片和36 GB RAM的MacBook Pro 16" 2023上进行了测试。
运行时间(越低越好)
在配备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 文件。