使用JQ对大数据集中的键值进行统计聚合

Using JQ for statistical aggregation of key values in a large data set

我有一个复杂的用例,目前我主要可以使用 JQ 来解决,但我想知道 JQ 是否不再是完成这项工作的正确工具,或者是否有一种方法可以概括我的过滤器。用例是在换行分隔 JSON 的大数据集中创建排序键值计数的统计聚合。我 运行 遇到的问题与我的数据集中的键数变得太大以至于 jq 无法实际编译的过滤器有关。

下面是一个示例输入,请注意它包括数组。

输入:(input.json)

{
  "content": {
    "user": {
      "name": "Bill",
      "items": [
        {
          "name": "shovel",
          "details": {
            "heavy": false
          }
        },
        {
          "name": "hammer",
        }
      ],
      "details": {
        "age": 60
      }
    }
  }
}
{
  "content": {
    "user": {
      "name": "Bill",
      "items": [
        {
          "name": "shovel",
          "details": {
            "heavy": false
          }
        },
        {
          "name": "hammer",
        }
      ],
      "details": {
        "age": 21
      }
    }
  }
}
{
  "content": {
    "user": {
      "name": "Alice",
      "items": [
        {
          "name": "hammer",
          "details": {
            "heavy": true
          }
        },
        {
          "name": "shovel",
        }
      ],
      "details": {
        "age": 30
      }
    }
  }
}

我想要的输出如下,基本上我得到了结构中所有键的列表,包括数组索引,并按键值排序。

输出:

{
  "stats": {
    "user.name": {
      "Bill": 2,
      "Alice": 1
    },
    "user.items.0.name": {
      "shovel": 2,
      "hammer": 1
    },
    "user.items.1.name": {
      "hammer": 2,
      "shovel": 1
    },
    "user.items.0.details.heavy": {
      "true": 1,
      "": 2,
    },
    "user.items.1.details.heavy": {
      "true": 1,
      "": 2
    },
    "user.details.age": {
      "30": 1,
      "62": 1,
      "21": 1
    }
  }
}

当前有问题的解决方案:

目前,我最初得到 json 输入 [content.user.name, content.user.items.1.name, etc.] 中所有键的列表,并使用它来构建 jq 过滤器。

对于上下文,这是我用来获取 keys 的 jq 过滤器 select(objects)|=[.] | map( .content | paths(scalars)) | map(join(".")) | unique

当前聚合过滤器看起来像这样(仅针对单个 content.user.name 聚合计算):

cat input.json | jq -c -s '{"stats": {"user.name": (map(.content."user"?."name"?) 
| del(..|nulls) | map(. | tostring) 
| reduce .[] as $i ( {}; setpath([$i]; getpath([$i]) + 1)) 
| to_entries | sort_by(.value) | reverse | from_entries)}}'

所以要添加更多聚合计算,我使用这个模板:

(newlines added for legibility)

"{KEY}": (map(.content.{KEY})
| del(..|nulls) | map(. | tostring)
| reduce .[] as $i ( {}; setpath([$i]; getpath([$i]) + 1))
| to_entries | sort_by(.value) | reverse | from_entries)

过滤器包括 content.user.details..age

cat input.json | jq -c -s '{"stats": {"user.name": (map(.content."user"?."name"?) 
| del(..|nulls) | map(. | tostring) 
| reduce .[] as $i ( {}; setpath([$i]; getpath([$i]) + 1)) 
| to_entries | sort_by(.value) | reverse | from_entries),
"user.details.age": (map(.content."user"?."details"?."age"?) 
| del(..|nulls) | map(. | tostring) 
| reduce .[] as $i ( {}; setpath([$i]; getpath([$i]) + 1)) 
| to_entries | sort_by(.value) | reverse | from_entries)}}'

所以我的过滤器的大小随着数据集中键的数量线性增长。这意味着对于大型数据集,我的过滤器实际上变得太大以至于 jq 无法编译。我不确定我是否已经盯着这个太久了,但我不确定这是否是 jq 最好解决的问题。如果我减小键聚合模板的大小,我仍然会受到某些键数的最大过滤器大小的限制,而且我似乎无法找到一种方法来映射原始键以便在迭代时重用模板键。这将意味着为每个换行符重新计算密钥 JSON 这不是最佳的但也不确定

TLDR;

我想通过一些换行符分隔的键来聚合键值 json。

下面是一些理想解决方案的伪代码,但我无法让它工作。

get keys:
select(objects)|=[.] | map( .content | paths(scalars)) | map(join(".")) | unique

iterate through all keys and run:
"{KEY}": (map(.content.{KEY})
| del(..|nulls) | map(. | tostring)
| reduce .[] as $i ( {}; setpath([$i]; getpath([$i]) + 1))
| to_entries | sort_by(.value) | reverse | from_entries)

有人有什么想法吗?

您可以使用 --stream 选项以较小的部分读取大输入

jq --stream -n '
  {stats: (reduce (1 | truncate_stream(inputs)) as $i ({};
    if ($i | has(1)) then ."\($i[0] | join("."))"."\($i[1])" += 1 else . end
  ))}
' input.json
{
  "stats": {
    "user.name": {
      "Bill": 2,
      "Alice": 1
    },
    "user.items.0.name": {
      "shovel": 2,
      "hammer": 1
    },
    "user.items.0.details.heavy": {
      "false": 2,
      "true": 1
    },
    "user.items.1.name": {
      "hammer": 2,
      "shovel": 1
    },
    "user.details.age": {
      "60": 1,
      "21": 1,
      "30": 1
    }
  }
}

由于输入是 JSON 流,您应该能够使用 inputs 和 -n command-line 选项获得一个简单但高效的解决方案。根据我对问题的理解,这是我得到的:

def summary(stream):
   reduce stream as [$p, $v] ({};
      ($p|join(".")) as $q
      | if .[$q] then .[$q][$v|tostring] += 1
      else .[$q] = {($v|tostring): 1}
      end);

{stats: summary(inputs.content
        | . as $in
        | paths(scalars) as $p
        | [$p, ($in|getpath($p))]) }