获取jq中的对象数组索引

Posted

技术标签:

【中文标题】获取jq中的对象数组索引【英文标题】:Getting the object array index in jq 【发布时间】:2017-06-16 22:37:10 【问题描述】:

我有一个看起来像这样的 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 是否可行?

【问题讨论】:

【参考方案1】:

所以我为 OP 提供了一个解决方案的策略,OP 很快就接受了。随后@peak 和@Jeff Mercado 提供了更好、更完整的解决方案。所以我把它变成了一个社区维基。如果可以的话,请改进这个答案。

一个简单的解决方案(@peak 指出)是使用内置函数index

map(.name == "something") | index(true)

jq 文档令人困惑地暗示 index 对字符串进行操作,但它也对数组进行操作。因此index(true) 返回映射生成的布尔数组中第一个true 的索引。如果没有满足条件的项,则结果为空。

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 将返回 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 所指出的那样,在最坏的情况下,这种递归实现将使用与数组长度成比例的堆栈空间。在 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 替换它:

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

如果我用“快速”递归版本替换它:

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 编码的函数往往很快!)

【讨论】:

很高兴看到我没有误读文档 :) 这非常有效!谢谢! @Jim D. -- Jeff Mercado 对 indexof 的递归实现是经过深思熟虑的,而您对它的适应则不是。最严重的问题可能是效率——您可能想运行一些基准测试。您的实现表明您对 jq 中数组的实现方式做出了一些错误的假设。至少,承认杰夫的努力是合适的。同时,感谢您对我的认可。 @Jim-D - Jeff 是一位经验丰富且非常称职的 jq 程序员,所以我担心您的程序与他的每一次偏差都会有某种缺点。效率低下的主要原因无疑是使用.[1:],就好像jq 是LISP。顺便说一下,数组是按长度存储的,所以如果输入是一个数组,那么无论其大小如何,调用length 的成本都是微不足道的。 @Jim-D - jq wiki (github.com/stedolan/jq/wiki) 提供了很多重要信息,jq 本身有一个反汇编选项 (jq --debug-dump-disasm ....)。 @peak,谢谢;我知道 wiki,但不知道反汇编函数。【参考方案2】:

@Jim-D 最初提出的使用 foreach 的解决方案只能像预期的那样适用于 JSON 对象数组,而且最初提出的两种解决方案都非常低效。他们在没有满足条件的项目的情况下的行为也可能令人惊讶。

使用index/1的解决方案

如果你只是想要一个快速简单的解决方案,你可以使用内置函数index,如下:

map(.name == "something") | index(true)

如果没有满足条件的项目,则结果为null

顺便说一句,如果您想要条件为真的所有索引,那么只需将index 更改为indices,即可轻松将上述解决方案转换为超快速解决方案:

map(.name == "something") | indices(true)

高效解决方案

这是一个通用且高效的函数,它返回输入数组中第一次出现的项目的索引(即偏移量),其中 (item|f) 为真(既不为空也不为假),否则返回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")

【讨论】:

【参考方案3】:

将数组转换为条目将使您能够访问项数组中的索引和值。您可以使用它来查找您要查找的值并获取它的索引。

def indexof(predicate):
    reduce to_entries[] as $i (null;
        if (. == null) and ($i.value | predicate) then
            $i.key
        else
            .
        end
    );
indexof(.name == "something")

但这不会短路,而是会遍历整个数组来查找索引。一旦找到第一个索引,您就会想要返回。采取更实用的方法可能更合适。

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;

【讨论】:

是的,TCO 仅适用于 0-arity 函数。为了便于阅读,您可以考虑使用 JSON 对象作为 _indexof 的输入。效率应该差不多。 如果使用对象作为参数同样可以正常工作,那就太好了,这些更改看起来更有吸引力。【参考方案4】:

这是另一个版本,它似乎比 @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 秒。这与我在一次运行中看到的情况相匹配,但公平地说,我确实有一次运行该解决方案平均为 3.6 秒,因此每个解决方案之间不太可能存在任何统计显着差异。

【讨论】:

以上是关于获取jq中的对象数组索引的主要内容,如果未能解决你的问题,请参考以下文章

按最近的时间戳对数组中的对象数组进行排序,然后使用 jq 按每个数组的第一个对象的时间戳对外部数组进行排序

使用 jq 为 JSON 对象的嵌套数组中的属性展平数组

使用 jq 和 bash 为数组中的每个对象运行命令

如何使用jq根据内部数组中的值过滤对象数组?

有没有办法获取存储在 firestore 中的数组中对象的索引?

如何使用角度获取数组中对象的索引?