获取jq中的对象数组索引
Getting the object array index in jq
我有一个看起来像这样的 json 对象(由 i3-msg -t get_workspaces
制作。
[
{
"name": "1",
"urgent": false
},
{
"name": "2",
"urgent": false
},
{
"name": "something",
"urgent": false
}
]
我正在尝试使用 jq
根据 select
查询找出列表中的哪个索引号。 jq
有一个叫做 index()
的东西,但它似乎只支持字符串?
使用 i3-msg -t get_workspaces | jq '.[] | select(.name=="something")'
之类的东西可以得到我想要的对象。但我想要它的索引。在这种情况下 2
(从 0 开始计数)
是否可以单独使用 jq
?
所以我给OP提供了一个解决策略,OP很快接受了。随后@peak 和@Jeff Mercado 提供了更好更完整的解决方案。所以我把它变成了一个社区维基。如果可以,请改进此答案。
一个简单的解决方案(@peak 指出)是使用内置函数,index
:
map(.name == "something") | index(true)
jq
文档令人困惑地表明 index
对字符串进行操作,但它也对数组进行操作。因此 index(true)
returns 是映射生成的布尔数组中第一个 true
的索引。如果没有满足条件的item,结果为null。
jq 表达式在 "lazy" manner 中计算,但 map
将遍历整个输入数组。我们可以通过重写上面的代码并引入一些调试语句来验证这一点:
[ .[] | debug | .name == "something" ] | index(true)
正如@peak所建议的那样,做得更好的关键是使用jq 1.5中引入的break
语句:
label $out |
foreach .[] as $item (
-1;
.+1;
if $item.name == "something" then
.,
break $out
else
empty
end
) // null
注意//
是无注释;它是替代运算符。如果找不到名称,foreach
将 return empty
将由替代运算符转换为 null。
另一种方法是递归处理数组:
def get_index(name):
name as $name |
if (. == []) then
null
elif (.[0].name == $name) then
0
else
(.[1:] | get_index($name)) as $result |
if ($result == null) then null else $result+1 end
end;
get_index("something")
然而,正如@Jeff Mercado 指出的那样,在最坏的情况下,此递归实现将使用与数组长度成比例的堆栈 space。在 1.5 版中 jq
引入了 Tail Call Optimization (TCO) 这将允许我们使用本地辅助函数对其进行优化(请注意,这是对@Jeff Mercado 提供的解决方案的微小调整,以便与上述内容保持一致示例):
def get_index(name):
name as $name |
def _get_index:
if (.i >= .len) then
null
elif (.array[.i].name == $name) then
.i
else
.i += 1 | _get_index
end;
{ array: ., i: 0, len: length } | _get_index;
get_index("something")
根据@peak 的说法,在 jq
中获取数组的长度是一个常数时间操作,显然索引数组也很便宜。我会尝试为此找到引文。
现在让我们尝试实际测量。这是测量简单解决方案的示例:
#!/bin/bash
jq -n '
def get_index(name):
name as $name |
map(.name == $name) | index(true)
;
def gen_input(n):
n as $n |
if ($n == 0) then
[]
else
gen_input($n-1) + [ { "name": $n, "urgent":false } ]
end
;
2000 as $n |
gen_input($n) as $i |
[(0 | while (.<$n; [ ($i | get_index(.)), .+1 ][1]))][$n-1]
'
当我在我的机器上 运行 时,我得到以下信息:
$ time ./simple
1999
real 0m10.024s
user 0m10.023s
sys 0m0.008s
如果我用 get_index 的 "fast" 版本替换它:
def get_index(name):
name as $name |
label $out |
foreach .[] as $item (
-1;
.+1;
if $item.name == $name then
.,
break $out
else
empty
end
) // null;
然后我得到:
$ time ./fast
1999
real 0m13.165s
user 0m13.173s
sys 0m0.000s
如果我用 "fast" 递归版本替换它:
def get_index(name):
name as $name |
def _get_index:
if (.i >= .len) then
null
elif (.array[.i].name == $name) then
.i
else
.i += 1 | _get_index
end;
{ array: ., i: 0, len: length } | _get_index;
我得到:
$ time ./fast-recursive
1999
real 0m52.628s
user 0m52.657s
sys 0m0.005s
哎呀!但我们可以做得更好。 @peak 提到了一个未记录的开关 --debug-dump-disasm
它可以让你看到 jq
是如何编译你的代码的。有了这个你可以看到修改对象并将其传递给 _indexof
然后提取数组、长度和索引是很昂贵的。重构以仅通过索引是一个 巨大的 改进,并且进一步改进以避免根据长度测试索引使其与迭代版本具有竞争力:
def indexof($name):
(.+[{name: $name}]) as $a | # add a "sentinel"
length as $l | # note length sees original array
def _indexof:
if ($a[.].name == $name) then
if (. != $l) then . else null end
else
.+1 | _indexof
end
;
0 | _indexof
;
我得到:
$ time ./fast-recursive2
null
real 0m13.238s
user 0m13.243s
sys 0m0.005s
因此看来,如果每个元素的可能性均等,并且您想要平均情况下的表现,则应该坚持使用简单的实现方式。 (C-coded 函数往往很快!)
@Jim-D 最初提出的使用 foreach 的解决方案只能按预期用于 JSON 对象的数组,并且最初提出的两种解决方案都非常低效。他们在没有满足条件的项目时的行为也可能令人惊讶。
解决方案使用index/1
如果你只想要一个 quick-and-easy 的解决方案,你可以使用内置函数 index
,如下所示:
map(.name == "something") | index(true)
如果没有满足条件的项目,则结果为null
。
顺便说一下,如果您想要所有条件为真的索引,那么只需将 index
更改为 indices
即可将上述内容轻松转换为 super-fast 解决方案:
map(.name == "something") | indices(true)
高效解决方案
这是一个通用且高效的函数,它 returns 输入数组中 (item|f) 为真值(非 null 或 false)的项目第一次出现的索引(即偏移量) , 和 null
否则。 (在 jq、javascript 和许多其他语言中,数组的索引始终从 0 开始。)
# 0-based index of item in input array such that f is truthy, else null
def which(f):
label $out
| foreach .[] as $x (-1; .+1; if ($x|f) then ., break $out else empty end)
// null ;
用法示例:
which(.name == "something")
将数组转换为条目将使您能够访问项目数组中的索引和值。您可以使用它来找到您要查找的值并获取其索引。
def indexof(predicate):
reduce to_entries[] as $i (null;
if (. == null) and ($i.value | predicate) then
$i.key
else
.
end
);
indexof(.name == "something")
但这并没有短路,而是会遍历整个数组来找到索引。一旦找到第一个索引,您就会想要return。采用更实用的方法可能更合适。
def indexof(predicate):
def _indexof:
if .i >= .len then
null
elif (.arr[.i] | predicate) then
.i
else
.i += 1 | _indexof
end;
{ arr: ., i: 0, len: length } | _indexof;
indexof(.name == "something")
注意,参数是通过这种方式传递给内部函数的,以利用some optimizations。即要利用 TCO,函数不得接受任何附加参数。
通过识别数组及其长度不变可以获得更快的版本:
def indexof(predicate):
. as $in
| length as $len
| def _indexof:
if . >= $len then null
elif ($in[.] | predicate) then .
else . + 1 | _indexof
end;
0 | _indexof;
这是另一个版本,它似乎比@peak 和@jeff-mercado 的优化版本要快一些:
label $out | . as $elements | range(length) |
select($elements[.].name == "something") | . , break $out
IMO 它更容易阅读,尽管它仍然依赖于 break
(仅获得第一个匹配项)。
我在一个约 1,000,000 个元素的数组上进行了 100 次迭代(最后一个元素是要匹配的元素)。我只计算了用户和内核时间,而不是挂钟时间。这个解决方案平均用了 3.4 秒,@peak 的解决方案用了 3.5 秒,@jeff-mercado 的用了 3.6 秒。这与我在一次性 运行s 中看到的相符,尽管公平地说我确实有一个 运行,其中这个解决方案平均为 3.6s,因此每个解决方案之间不太可能存在任何统计上的显着差异。
我有一个看起来像这样的 json 对象(由 i3-msg -t get_workspaces
制作。
[
{
"name": "1",
"urgent": false
},
{
"name": "2",
"urgent": false
},
{
"name": "something",
"urgent": false
}
]
我正在尝试使用 jq
根据 select
查询找出列表中的哪个索引号。 jq
有一个叫做 index()
的东西,但它似乎只支持字符串?
使用 i3-msg -t get_workspaces | jq '.[] | select(.name=="something")'
之类的东西可以得到我想要的对象。但我想要它的索引。在这种情况下 2
(从 0 开始计数)
是否可以单独使用 jq
?
所以我给OP提供了一个解决策略,OP很快接受了。随后@peak 和@Jeff Mercado 提供了更好更完整的解决方案。所以我把它变成了一个社区维基。如果可以,请改进此答案。
一个简单的解决方案(@peak 指出)是使用内置函数,index
:
map(.name == "something") | index(true)
jq
文档令人困惑地表明 index
对字符串进行操作,但它也对数组进行操作。因此 index(true)
returns 是映射生成的布尔数组中第一个 true
的索引。如果没有满足条件的item,结果为null。
jq 表达式在 "lazy" manner 中计算,但 map
将遍历整个输入数组。我们可以通过重写上面的代码并引入一些调试语句来验证这一点:
[ .[] | debug | .name == "something" ] | index(true)
正如@peak所建议的那样,做得更好的关键是使用jq 1.5中引入的break
语句:
label $out |
foreach .[] as $item (
-1;
.+1;
if $item.name == "something" then
.,
break $out
else
empty
end
) // null
注意//
是无注释;它是替代运算符。如果找不到名称,foreach
将 return empty
将由替代运算符转换为 null。
另一种方法是递归处理数组:
def get_index(name):
name as $name |
if (. == []) then
null
elif (.[0].name == $name) then
0
else
(.[1:] | get_index($name)) as $result |
if ($result == null) then null else $result+1 end
end;
get_index("something")
然而,正如@Jeff Mercado 指出的那样,在最坏的情况下,此递归实现将使用与数组长度成比例的堆栈 space。在 1.5 版中 jq
引入了 Tail Call Optimization (TCO) 这将允许我们使用本地辅助函数对其进行优化(请注意,这是对@Jeff Mercado 提供的解决方案的微小调整,以便与上述内容保持一致示例):
def get_index(name):
name as $name |
def _get_index:
if (.i >= .len) then
null
elif (.array[.i].name == $name) then
.i
else
.i += 1 | _get_index
end;
{ array: ., i: 0, len: length } | _get_index;
get_index("something")
根据@peak 的说法,在 jq
中获取数组的长度是一个常数时间操作,显然索引数组也很便宜。我会尝试为此找到引文。
现在让我们尝试实际测量。这是测量简单解决方案的示例:
#!/bin/bash
jq -n '
def get_index(name):
name as $name |
map(.name == $name) | index(true)
;
def gen_input(n):
n as $n |
if ($n == 0) then
[]
else
gen_input($n-1) + [ { "name": $n, "urgent":false } ]
end
;
2000 as $n |
gen_input($n) as $i |
[(0 | while (.<$n; [ ($i | get_index(.)), .+1 ][1]))][$n-1]
'
当我在我的机器上 运行 时,我得到以下信息:
$ time ./simple
1999
real 0m10.024s
user 0m10.023s
sys 0m0.008s
如果我用 get_index 的 "fast" 版本替换它:
def get_index(name):
name as $name |
label $out |
foreach .[] as $item (
-1;
.+1;
if $item.name == $name then
.,
break $out
else
empty
end
) // null;
然后我得到:
$ time ./fast
1999
real 0m13.165s
user 0m13.173s
sys 0m0.000s
如果我用 "fast" 递归版本替换它:
def get_index(name):
name as $name |
def _get_index:
if (.i >= .len) then
null
elif (.array[.i].name == $name) then
.i
else
.i += 1 | _get_index
end;
{ array: ., i: 0, len: length } | _get_index;
我得到:
$ time ./fast-recursive
1999
real 0m52.628s
user 0m52.657s
sys 0m0.005s
哎呀!但我们可以做得更好。 @peak 提到了一个未记录的开关 --debug-dump-disasm
它可以让你看到 jq
是如何编译你的代码的。有了这个你可以看到修改对象并将其传递给 _indexof
然后提取数组、长度和索引是很昂贵的。重构以仅通过索引是一个 巨大的 改进,并且进一步改进以避免根据长度测试索引使其与迭代版本具有竞争力:
def indexof($name):
(.+[{name: $name}]) as $a | # add a "sentinel"
length as $l | # note length sees original array
def _indexof:
if ($a[.].name == $name) then
if (. != $l) then . else null end
else
.+1 | _indexof
end
;
0 | _indexof
;
我得到:
$ time ./fast-recursive2
null
real 0m13.238s
user 0m13.243s
sys 0m0.005s
因此看来,如果每个元素的可能性均等,并且您想要平均情况下的表现,则应该坚持使用简单的实现方式。 (C-coded 函数往往很快!)
@Jim-D 最初提出的使用 foreach 的解决方案只能按预期用于 JSON 对象的数组,并且最初提出的两种解决方案都非常低效。他们在没有满足条件的项目时的行为也可能令人惊讶。
解决方案使用index/1
如果你只想要一个 quick-and-easy 的解决方案,你可以使用内置函数 index
,如下所示:
map(.name == "something") | index(true)
如果没有满足条件的项目,则结果为null
。
顺便说一下,如果您想要所有条件为真的索引,那么只需将 index
更改为 indices
即可将上述内容轻松转换为 super-fast 解决方案:
map(.name == "something") | indices(true)
高效解决方案
这是一个通用且高效的函数,它 returns 输入数组中 (item|f) 为真值(非 null 或 false)的项目第一次出现的索引(即偏移量) , 和 null
否则。 (在 jq、javascript 和许多其他语言中,数组的索引始终从 0 开始。)
# 0-based index of item in input array such that f is truthy, else null
def which(f):
label $out
| foreach .[] as $x (-1; .+1; if ($x|f) then ., break $out else empty end)
// null ;
用法示例:
which(.name == "something")
将数组转换为条目将使您能够访问项目数组中的索引和值。您可以使用它来找到您要查找的值并获取其索引。
def indexof(predicate):
reduce to_entries[] as $i (null;
if (. == null) and ($i.value | predicate) then
$i.key
else
.
end
);
indexof(.name == "something")
但这并没有短路,而是会遍历整个数组来找到索引。一旦找到第一个索引,您就会想要return。采用更实用的方法可能更合适。
def indexof(predicate):
def _indexof:
if .i >= .len then
null
elif (.arr[.i] | predicate) then
.i
else
.i += 1 | _indexof
end;
{ arr: ., i: 0, len: length } | _indexof;
indexof(.name == "something")
注意,参数是通过这种方式传递给内部函数的,以利用some optimizations。即要利用 TCO,函数不得接受任何附加参数。
通过识别数组及其长度不变可以获得更快的版本:
def indexof(predicate):
. as $in
| length as $len
| def _indexof:
if . >= $len then null
elif ($in[.] | predicate) then .
else . + 1 | _indexof
end;
0 | _indexof;
这是另一个版本,它似乎比@peak 和@jeff-mercado 的优化版本要快一些:
label $out | . as $elements | range(length) |
select($elements[.].name == "something") | . , break $out
IMO 它更容易阅读,尽管它仍然依赖于 break
(仅获得第一个匹配项)。
我在一个约 1,000,000 个元素的数组上进行了 100 次迭代(最后一个元素是要匹配的元素)。我只计算了用户和内核时间,而不是挂钟时间。这个解决方案平均用了 3.4 秒,@peak 的解决方案用了 3.5 秒,@jeff-mercado 的用了 3.6 秒。这与我在一次性 运行s 中看到的相符,尽管公平地说我确实有一个 运行,其中这个解决方案平均为 3.6s,因此每个解决方案之间不太可能存在任何统计上的显着差异。