在 jq 中从另一个文件中减去一个 json 文件

subtracting one json file from another in jq

有没有办法在 jq 中比较两个 json 文件?具体来说,如果对象出现在另一个 json 文件中,我希望能够从一个 json 文件中删除这些对象。基本上,从一个文件中减去另一个文件。如果我可以概括这一点以便我可以定义对象的相等标准,那将是一个奖励,但这不是绝对必要的,它可以严格基于相同的对象。

所以更一般的情况是这样的。假设我有一个如下所示的文件:

[
  {
    "name": "Cynthia",
    "surname": "Craig",
    "isActive": true,
    "balance": ",426.88"
  },
  {
    "name": "Elise",
    "surname": "Long",
    "isActive": false,
    "balance": ",892.72"
  },
  {
    "name": "Hyde",
    "surname": "Adkins",
    "isActive": true,
    "balance": ",769.34"
  },
  {
    "name": "Matthews",
    "surname": "Jefferson",
    "isActive": true,
    "balance": ",991.42"
  },
  {
    "name": "Kris",
    "surname": "Norris",
    "isActive": false,
    "balance": ",137.11"
  }
]

我还有第二个文件,如下所示:

[
  {
    "name": "Cynthia",
    "surname": "Craig"
  },
  {
    "name": "Kris",
    "surname": "Norris"
  }
] 

我想从第一个文件中删除名称和姓氏字段与第二个文件的对象匹配的所有对象,以便结果应如下所示:

[
  {
    "name": "Elise",
    "surname": "Long",
    "isActive": false,
    "balance": ",892.72"
  },
  {
    "name": "Hyde",
    "surname": "Adkins",
    "isActive": true,
    "balance": ",769.34"
  },
  {
    "name": "Matthews",
    "surname": "Jefferson",
    "isActive": true,
    "balance": ",991.42"
  }
] 

这是一个使用 pull/1062

中的 --argfileproject/1 的解决方案
def project(q):
    . as $in
  | reduce (q | if type == "object" then keys[] else .[] end) as $k (
      {}
      ; . + { ($k) : ($in[$k]) }
    )
;

  map(
    reduce $arg[] as $a (
        .
      ; select(project($a) != $a)
    )
    | values
  )

如果将 "second" 文件放在 second.json 中,将数据放在 data.json 中,将上面的过滤器放在 filter.jq 中,您可以 运行 使用

jq -M --argfile arg second.json -f filter.jq data.json

生产

[
  {
    "name": "Elise",
    "surname": "Long",
    "isActive": false,
    "balance": ",892.72"
  },
  {
    "name": "Hyde",
    "surname": "Adkins",
    "isActive": true,
    "balance": ",769.34"
  },
  {
    "name": "Matthews",
    "surname": "Jefferson",
    "isActive": true,
    "balance": ",991.42"
  }
]

如果您想修改对象的相等性标准,可以将表达式 select(project($a) != $a) 替换为其他内容。

再仔细考虑一下,我们可以通过使用 contains 来消除对 project/1 的需要。这应该更有效,因为它消除了临时对象的构造。

  map(
    reduce $arg[] as $a (
        .
      ; select(.!=null and contains($a)==false)
    )
    | values
  )

这可以使用 any 进一步简化:

map(select(any(.; contains($arg[]))==false))

它足够短,可以直接在命令行上使用:

jq -M --argfile arg second.json 'map(select(any(.; contains($arg[]))==false))' data.json

jq解法:

jq --slurpfile s f2.json '[ .[] | . as $o | if (reduce $s[0][] as $i
     ([]; . + [($o | contains($i))]) | any) then empty else $o end ]' f1.json

输出:

[
  {
    "name": "Elise",
    "surname": "Long",
    "isActive": false,
    "balance": ",892.72"
  },
  {
    "name": "Hyde",
    "surname": "Adkins",
    "isActive": true,
    "balance": ",769.34"
  },
  {
    "name": "Matthews",
    "surname": "Jefferson",
    "isActive": true,
    "balance": ",991.42"
  }
]

根据前两个目标,以下解决方案旨在通用、高效且尽可能简单。

通用性

为了通用性,让我们假设 $one 和 $two 是两个数组 JSON 个实体,我们希望在 $one 中找到这些项目 $x 这样 ($x|filter) 就不会出现在 map($two | filter) 中,其中 filter 是任意过滤器。 (在当前实例中,它是 {surname, name}。)

解决方案使用了INDEX/1,它是在1.5正式发布后添加到jq中的,所以我们从复制它的定义开始:

def INDEX(stream; idx_expr):
  reduce stream as $row ({};
    .[$row|idx_expr|
      if type != "string" then tojson
      else .
      end] |= $row);
def INDEX(idx_expr): INDEX(.[]; idx_expr);

效率

为了效率,我们需要使用一个JSON对象作为字典; 由于键必须是字符串,因此我们需要确保在转换对象时 到一个字符串,对象被规范化。为此,我们定义normalize如下:

# Normalize the input with respect to the order of keys in objects
def normalize:
  . as $in
  | if type == "object" then reduce keys[] as $key
         ( {}; . + { ($key):  ($in[$key] | normalize) } ) 
    elif type == "array" then map( normalize )
    else .
    end;

要构造字典,我​​们只需应用 (normalize|tojson):

def todict(filter):
  INDEX(filter| normalize | tojson);

解决方案

现在解决方法很简单:

# select those items from the input stream for which 
# (normalize|tojson) is NOT in dict:
def MINUS(filter; $dict):
 select( $dict[filter | normalize | tojson] | not);

def difference($one; $two; filter):
  ($two | todict(filter)) as $dict
  | $one[] | MINUS( filter; $dict );

difference( $one; $two; {surname, name} )

调用

$ jq -n --argfile one one.json --argfile two two.json -f difference.jq