Rust JSON查询库jaq-syn的使用,jaq-syn提供高性能JSON解析与查询功能
jaq(发音类似Jacques)是JSON数据处理工具jq的克隆版本。jaq旨在支持jq语法和操作的大部分子集。
jaq专注于三个目标:
正确性: jaq旨在提供更正确和可预测的jq实现,同时在大多数情况下保持与jq的兼容性。
性能: 我最初创建jaq是因为我对jq的长启动时间感到困扰,这在我的机器上大约需要50毫秒。这在处理大量小文件时尤其明显。jaq的启动速度比jq 1.6快约30倍,并且在许多其他基准测试中也优于jq。
简单性: jaq旨在拥有简单且小的实现,以减少潜在的错误并促进贡献。
我从另一个Rust程序jql中获得了灵感。然而,与jql不同,jaq旨在紧密模仿jq的语法和语义。这应该允许熟练使用jq的用户轻松使用jaq。
安装
二进制文件
您可以在发布页面下载Linux、Mac和Windows的二进制文件。
您也可以使用homebrew在macOS或Linux上安装jaq:
$ brew install jaq
$ brew install --HEAD jaq # 最新开发版本
或使用scoop在Windows上安装:
$ scoop install main/jaq
从源代码安装
要编译jaq,您需要Rust工具链。
以下任何命令都可以安装jaq:
$ cargo install --locked jaq
$ cargo install --locked --git https://github.com/01mf02/jaq # 最新开发版本
在我的系统上,这两个命令都将可执行文件放置在~/.cargo/bin/jaq。
如果您已克隆此存储库,您还可以在克隆的存储库中执行以下命令之一来构建jaq:
$ cargo build --release # 将二进制文件放入target/release/jaq
$ cargo install --locked --path jaq # 安装二进制文件
jaq应该在任何受Rust支持的系统中工作。如果不工作,请提交问题。
示例
以下示例应展示jaq目前可以做什么。您应该通过将jaq替换为jq获得相同的输出。如果没有,您提交问题将不胜感激。 语法在jq手册中有文档记录。
访问字段:
$ echo '{"a": 1, "b": 2}' | jaq '.a'
1
添加值:
$ echo '{"a": 1, "b": 2}' | jaq 'add'
3
以两种方式从对象构造数组并显示它们相等:
$ echo '{"a": 1, "b": 2}' | jaq '[.a, .b] == [.[]]'
true
对数组的所有元素应用过滤器并过滤结果:
$ echo '[0, 1, 2, 3]' | jaq 'map(.*2) | [.[] | select(. < 5)]'
[0, 2, 4]
将(吸入)输入值读入数组并获取其元素的平均值:
$ echo '1 2 3 4' | jaq -s 'add / length'
2.5
重复将过滤器应用于自身并输出中间结果:
$ echo '0' | jaq '[recurse(.+1; . < 3)]'
[0, 1, 2]
懒洋洋地对输入进行折叠并输出中间结果:
$ seq 1000 | jaq -n 'foreach inputs as $x (0; . + $x)'
1 3 6 10 15 [...]
性能
以下评估包括几个基准测试,允许比较jaq、jq和gojq的性能。empty基准运行n次empty过滤器,输入为null,用于测量启动时间。bf-fib基准运行用jq编写的Brainfuck解释器,解释产生n个斐波那契数的Brainfuck脚本。其他基准使用n作为输入评估各种过滤器。
我在带有AMD Ryzen 5 5500U的Linux系统上使用bench.sh target/release/jaq jq-1.7 gojq-0.12.13 jq-1.6 | tee bench.json生成了基准数据。 然后我用一个"单行"(稍微拉伸了这个术语和行)处理了结果:
jq -rs '.[] | "|`\(.name)`|\(.n)|" + ([.time[] | min | (.*1000|round)? // "N/A"] | min as $total_min | map(if . == $total_min then "**\(.)**" else "\(.)" end) | join("|"))' bench.json
(当然,您也可以在这里使用jaq而不是jq。) 最后,我将表头与输出连接起来,并通过pandoc -t gfm管道传输。
表:评估结果,单位为毫秒(如果超过10秒,则为"N/A")。
基准测试 | n | jaq-1.4 | jq-1.7.1 | gojq-0.12.15 | jq-1.6 |
---|---|---|---|---|---|
empty | 512 | 610 | 660 | 740 | 8310 |
bf-fib | 13 | 470 | 1220 | 570 | 1440 |
reverse | 1048576 | 50 | 680 | 270 | 650 |
sort | 1048576 | 140 | 550 | 580 | 680 |
group-by | 1048576 | 400 | 1890 | 1550 | 2860 |
min-max | 1048576 | 210 | 320 | 250 | 350 |
add | 1048576 | 520 | 640 | 1310 | 730 |
kv | 131072 | 170 | 140 | 220 | 190 |
kv-update | 131072 | 190 | 540 | 440 | N/A |
kv-entries | 131072 | 630 | 1150 | 830 | 1120 |
ex-implode | 1048576 | 510 | 1100 | 610 | 1090 |
reduce | 1048576 | 820 | 890 | N/A | 860 |
try-catch | 1048576 | 180 | 320 | 370 | 670 |
tree-flatten | 17 | 730 | 360 | 10 | 480 |
tree-update | 17 | 560 | 970 | 1330 | 1190 |
tree-paths | 17 | 470 | 250 | 880 | 460 |
to-fromjson | 65536 | 30 | 370 | 120 | 390 |
ack | 7 | 530 | 700 | 1230 | 620 |
range-prop | 128 | 280 | 310 | 210 | 590 |
结果显示,jaq-1.4在15个基准测试中最快,而jq-1.7.1在2个基准测试中最快,gojq-0.12.15在2个基准测试中最快。gojq在tree-flatten上快得多,因为它本地实现了flatten过滤器,而不是通过定义。
功能
以下是概述,总结:
- [x] 已实现的功能,和
- [ ] 尚未实现的功能。
扩展jaq的贡献非常欢迎。
基础
- [x] 身份(.)
- [x] 递归(…)
- [x] 基本数据类型(null、boolean、number、string、array、object)
- [x] if-then-else(if .a < .b then .a else .b end)
- [x] 折叠(reduce .[] as $x (0; . + $x), foreach .[] as $x (0; . + $x; . + .))
- [x] 错误处理(try … catch …)(参见与jq的差异)
- [x] 字符串插值(“The successor of (.) is (.+1).”)
- [x] 格式字符串(@json, @text, @csv, @tsv, @html, @sh, @base64, @base64d)
路径
- [x] 数组/对象的索引(.[0], .a, .[“a”])
- [x] 迭代数组/对象(.[])
- [x] 可选索引/迭代(.a?, .[]?)
- [x] 数组切片(.[3:7], .[0:-1])
- [x] 字符串切片
操作符
- [x] 组合(|)
- [x] 绑定(. as $x | $x)
- [x] 连接(,)
- [x] 普通赋值(=)
- [x] 更新赋值(|=, +=, -=)
- [x] 替代(//)
- [x] 逻辑(or, and)
- [x] 相等和比较(.a == .b, .a < .b)
- [x] 算术(+, -, *, /, %)
- [x] 取反(-)
- [x] 错误抑制(?)
定义
- [x] 基本定义(def map(f): [.[] | f];)
- [x] 递归定义(def r: r; r)
核心过滤器
- [x] 空(empty)
- [x] 错误(error)
- [x] 输入(inputs)
- [x] 长度(length, utf8bytelength)
- [x] 舍入(floor, round, ceil)
- [x] 字符串 <-> JSON(fromjson, tojson)
- [x] 字符串 <-> 整数(explode, implode)
- [x] 字符串规范化(ascii_downcase, ascii_upcase)
- [x] 字符串前缀/后缀(startswith, endswith, ltrimstr, rtrimstr)
- [x] 字符串分割(split(“foo”))
- [x] 数组过滤器(reverse, sort, sort_by(-.), group_by, min_by, max_by)
- [x] 流消费者(first, last, range, fold)
- [x] 流生成器(range, recurse)
- [x] 时间(now, fromdateiso8601, todateiso8601)
- [x] 更多数字过滤器(sqrt, sin, log, pow, …)(数字过滤器列表)
- [ ] 更多时间过滤器(strptime, strftime, strflocaltime, mktime, gmtime, 和localtime)
标准过滤器
这些过滤器通过更基本的过滤器定义。它们的定义在std.jq。
- [x] 未定义(null)
- [x] 布尔值(true, false, not)
- [x] 特殊数字(nan, infinite, isnan, isinfinite, isfinite, isnormal)
- [x] 类型(type)
- [x] 过滤(select(. >= 0))
- [x] 选择(values, nulls, booleans, numbers, strings, arrays, objects, iterables, scalars)
- [x] 转换(tostring, tonumber)
- [x] 可迭代过滤器(map(.+1), map_values(.+1), add, join(“a”))
- [x] 数组过滤器(transpose, first, last, nth(10), flatten, min, max)
- [x] 对象-数组转换(to_entries, from_entries, with_entries)
- [x] 全称/存在(all, any)
- [x] 递归(walk)
- [x] I/O(input)
- [x] 正则表达式(test, scan, match, capture, splits, sub, gsub)
- [x] 时间(fromdate, todate)
数字过滤器
jaq从libm导入许多过滤器并遵循它们的类型签名。
零参数过滤器:
- [x] acos
- [x] acosh
- [x] asin
- [x] asinh
- [x] atan
- [x] atanh
- [x] cbrt
- [x] cos
- [x] cosh
- [x] erf
- [x] erfc
- [x] exp
- [x] exp10
- [x] exp2
- [x] expm1
- [x] fabs
- [x] frexp,返回(float, integer)对。
- [x] ilogb,返回整数。
- [x] j0
- [x] j1
- [x] lgamma
- [x] log
- [x] log10
- [x] log1p
- [x] log2
- [x] logb
- [x] modf,返回(float, float)对。
- [x] nearbyint
- [x] pow10
- [x] rint
- [x] significand
- [x] sin
- [x] sinh
- [x] sqrt
- [x] tan
- [x] tanh
- [x] tgamma
- [x] trunc
- [x] y0
- [x] y1
忽略.的双参数过滤器:
- [x] atan2
- [x] copysign
- [x] drem
- [x] fdim
- [x] fmax
- [x] fmin
- [x] fmod
- [x] hypot
- [x] jn,第一个参数为整数。
- [x] ldexp,第二个参数为整数。
- [x] nextafter
- [x] nexttoward
- [x] pow
- [x] remainder
- [x] scalb
- [x] scalbln,第二个参数为整数。
- [x] yn,第一个参数为整数。
忽略.的三参数过滤器:
- [x] fma
高级功能
jaq目前不旨在支持jq的几个功能,例如:
- 模块
- SQL风格的操作符
- 流式处理
jq和jaq之间的差异
数字
jq对所有数字使用64位浮点数(floats)。相比之下,jaq将数字如0或-42解释为机器大小的整数,将数字如0.0或3e8解释为64位浮点数。jaq中的许多操作,如数组索引,检查传递的数字是否确实是整数。这背后的动机是避免可能静默导致错误结果的舍入错误。例如:
$ jq -n '[0, 1, 2] | .[1.0000000000000001]'
1
$ jaq -n '[0, 1, 2] | .[1.0000000000000001]'
Error: cannot use 1.0 as integer
$ jaq -n '[0, 1, 2] | .[1]'
1
jaq的规则是:
- 两个整数的和、差、积和余数是整数。
- 两个数字之间的任何其他操作产生浮点数。
示例:
$ jaq -n '1 + 2'
3
$ jaq -n '10 / 2'
5.0
$ jaq -n '1.0 + 2'
3.0
您可以通过添加0.0、乘以1.0或除以1将整数转换为浮点数。您可以通过round、floor或ceil将浮点数转换为整数:
$ jaq -n '1.2 | [floor, round, ceil]'
[1, 1, 2]
NaN和无穷大
在jq中,除以0有一些令人惊讶的属性;例如,0 / 0产生nan,而0 as $n | $n / 0产生错误。在jaq中,n / 0如果n == 0产生nan,如果n > 0产生infinite,如果n < 0产生-infinite。jaq的行为更接近浮点算术的IEEE标准(IEEE 754)。
jaq在浮点数上实现全序以允许排序值。因此,不幸的是,它必须强制执行nan == nan。(jq通过强制执行nan < nan来绕过这一点,这打破了关于全序的基本法则。)
像jq一样,jaq在JSON中将nan和infinite打印为null,因为JSON不支持将这些值编码为数字。
保留分数数字
jaq完美保留来自JSON数据的分数数字(只要它们不用于某些算术操作),而jq 1.6可能静默转换为64位浮点数:
$ echo '1e500' | jq '.'
1.7976931348623157e+308
$ echo '1e500' | jaq '.'
1e500
因此,与jq 1.6不同,jaq满足jq手册中的以下段落:
身份过滤器的一个重要点是它保证保留值的字面十进制表示。这在处理无法无损转换为IEEE754双精度表示的数字时尤其重要。
请注意,jq的较新版本,例如1.7,似乎也保留了字面十进制表示。
赋值
像jq一样,jaq允许形式为p |= f的赋值。然而,jaq以不同的方式解释这些赋值。幸运的是,在大多数情况下,结果是相同的。
在jq中,赋值p |= f首先构造与p匹配的所有值的路径。然后,它将过滤器f应用于这些值。
在jaq中,赋值p |= f立即将f应用于任何匹配p的值。与jq不同,赋值不显式构造路径。
jaq的赋值实现可能产生更高的性能,因为它不构造路径。此外,这还"通过设计"防止了jq中的几个错误。例如,给定过滤器[0, 1, 2, 3] | .[] |= empty,jq产生[1, 3],而jaq产生[]。这里发生了什么?
jq首先构造对应于.[]的路径,即.0, .1, .2, .3。然后,它移除每个路径处的元素。然而,每个移除都会更改剩余路径引用的值。也就是说,在移除.0(值0)之后
完整示例demo:
# 示例1:基本JSON查询
echo '{"name": "John", "age": 30, "city": "New York"}' | jaq '.name'
# 输出: "John"
# 示例2:数组操作
echo '[{"id": 1, "value": 10}, {"id": 2, "
jaq-syn是一个基于Rust语言开发的高性能JSON查询库,专注于提供快速、灵活的JSON解析和查询功能。它支持类似jq的查询语法,允许用户通过简洁的表达式从JSON数据中提取、筛选和转换内容。jaq-syn的设计注重性能和内存效率,适用于处理大规模JSON数据。
主要功能
- 高性能解析:利用Rust的零成本抽象和内存安全特性,实现高效的JSON解析。
- 灵活查询:支持类jq语法,允许复杂查询和数据处理。
- 类型安全:基于Rust的强类型系统,减少运行时错误。
- 轻量级:库体积小,依赖少,易于集成到项目中。
安装方法
在项目的Cargo.toml
文件中添加jaq-syn依赖:
[dependencies]
jaq-syn = "0.1.0" # 请根据实际版本调整
基本使用方法
- 解析JSON字符串:使用jaq-syn解析JSON数据。
- 构建查询表达式:编写查询表达式来筛选或转换数据。
- 执行查询:将查询应用于解析后的JSON,获取结果。
示例代码
以下是一个简单的示例,展示如何使用jaq-syn查询JSON数据:
use jaq_syn::{parse, Filter};
use serde_json::Value;
fn main() {
// 示例JSON数据
let json_data = r#"
{
"users": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
}
"#;
// 解析JSON
let data: Value = serde_json::from_str(json_data).unwrap();
// 定义查询表达式:提取所有用户的名称
let query = parse(".users[].name", jaq_syn::MainMode::Default).unwrap();
// 执行查询
let mut results = Vec::new();
query.run(&data, &mut results);
// 输出结果
for result in results {
println!("{}", result);
}
}
高级查询示例
jaq-syn支持复杂查询,例如条件筛选和数据处理:
// 查询年龄大于28的用户名称
let query = parse(".users[] | select(.age > 28) | .name", jaq_syn::MainMode::Default).unwrap();
完整示例demo
use jaq_syn::{parse, Filter};
use serde_json::Value;
fn main() {
// 示例JSON数据:包含用户信息的嵌套结构
let json_data = r#"
{
"company": "TechCorp",
"departments": [
{
"name": "Engineering",
"users": [
{"name": "Alice", "age": 30, "role": "Developer"},
{"name": "Bob", "age": 25, "role": "Designer"},
{"name": "Charlie", "age": 35, "role": "Architect"}
]
},
{
"name": "Marketing",
"users": [
{"name": "David", "age": 28, "role": "Manager"},
{"name": "Eve", "age": 32, "role": "Analyst"}
]
}
]
}
"#;
// 解析JSON字符串为Value类型
let data: Value = serde_json::from_str(json_data).unwrap();
// 示例1:提取所有用户的名称(基础查询)
println!("=== 所有用户名称 ===");
let query_names = parse(".departments[].users[].name", jaq_syn::MainMode::Default).unwrap();
let mut results_names = Vec::new();
query_names.run(&data, &mut results_names);
for result in results_names {
println!("{}", result);
}
// 示例2:查询年龄大于28的用户名称(条件筛选)
println!("\n=== 年龄大于28的用户名称 ===");
let query_age_filter = parse(".departments[].users[] | select(.age > 28) | .name", jaq_syn::MainMode::Default).unwrap();
let mut results_age = Vec::new();
query_age_filter.run(&data, &mut results_age);
for result in results_age {
println!("{}", result);
}
// 示例3:提取特定部门的用户角色(多级查询)
println!("\n=== Engineering部门的用户角色 ===");
let query_dept = parse(".departments[] | select(.name == \"Engineering\") | .users[].role", jaq_syn::MainMode::Default).unwrap();
let mut results_dept = Vec::new();
query_dept.run(&data, &mut results_dept);
for result in results_dept {
println!("{}", result);
}
// 示例4:组合查询 - 用户名称和角色
println!("\n=== 用户名称和角色组合 ===");
let query_combined = parse(".departments[].users[] | {name: .name, role: .role}", jaq_syn::MainMode::Default).unwrap();
let mut results_combined = Vec::new();
query_combined.run(&data, &mut results_combined);
for result in results_combined {
println!("{}", result);
}
}
注意事项
- 确保JSON数据格式正确,否则解析可能失败。
- 查询表达式需遵循jaq-syn的语法规则,参考官方文档学习更多表达式写法。
- 对于大规模数据,建议优化查询表达式以提高性能。
jaq-syn是一个强大且高效的JSON查询工具,适合需要处理JSON数据的Rust项目。通过结合Rust的性能优势,它能帮助开发者轻松实现复杂的数据提取和转换任务。