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, "

1 回复

jaq-syn是一个基于Rust语言开发的高性能JSON查询库,专注于提供快速、灵活的JSON解析和查询功能。它支持类似jq的查询语法,允许用户通过简洁的表达式从JSON数据中提取、筛选和转换内容。jaq-syn的设计注重性能和内存效率,适用于处理大规模JSON数据。

主要功能

  • 高性能解析:利用Rust的零成本抽象和内存安全特性,实现高效的JSON解析。
  • 灵活查询:支持类jq语法,允许复杂查询和数据处理。
  • 类型安全:基于Rust的强类型系统,减少运行时错误。
  • 轻量级:库体积小,依赖少,易于集成到项目中。

安装方法

在项目的Cargo.toml文件中添加jaq-syn依赖:

[dependencies]
jaq-syn = "0.1.0"  # 请根据实际版本调整

基本使用方法

  1. 解析JSON字符串:使用jaq-syn解析JSON数据。
  2. 构建查询表达式:编写查询表达式来筛选或转换数据。
  3. 执行查询:将查询应用于解析后的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的性能优势,它能帮助开发者轻松实现复杂的数据提取和转换任务。

回到顶部