使用 jq 根据特定关联键分割非常大的 JSON 文件

使用 jq 根据特定关联键分割非常大的 JSON 文件

假设我有一个非常大的 JSON 文件,其中包含一个数组,其中包含数百万个具有这些键(以及许多其他键)的条目

 {
        "name": "assets/fUCcxWczWT0",
        "displayName": "The House",
        "authorName": "John Smith",
        "resources": {"house" : "address","car":"bla"},
 }

如何使用jq切片(删除)特定名称键之前的内容?例如,由 唯一标识的元素之前的所有内容"name": "assets/fUCcxWczWT0"

以上是文件中的简化代表性元素。这是顶级数组中的实际元素:

{
  "name": "assets/4vds6twPsb7",
  "displayName": "pim",
  "authorName": "Erwin Braak",
  "createTime": "2017-12-06T11:52:40.557236Z",
  "updateTime": "2020-10-07T09:49:08.752848Z",
  "formats": [
    {
      "root": {
        "relativePath": "Pim.gltf",
        "url": "https://poly.googleapis.com/downloads/fp/1602064148752848/4vds6twPsb7/c8Ksvo0_VjG/Pim.gltf",
        "contentType": "model/gltf+json"
      },
      "resources": [
        {
          "relativePath": "Pim.bin",
          "url": "https://poly.googleapis.com/downloads/fp/1602064148752848/4vds6twPsb7/c8Ksvo0_VjG/Pim.bin",
          "contentType": "application/octet-stream"
        },
        {
          "relativePath": "C:/Users/PC/Desktop/pom/pimSurface_Color.png",
          "url": "https://poly.googleapis.com/downloads/fp/1602064148752848/4vds6twPsb7/c8Ksvo0_VjG/C:/Users/PC/Desktop/pom/pimSurface_Color.png",
          "contentType": "image/png"
        }
      ],
      "formatComplexity": {
        "triangleCount": "586149"
      },
      "formatType": "GLTF2"
    },
    {
      "root": {
        "relativePath": "Pim.obj",
        "url": "https://poly.googleapis.com/downloads/fp/1602064148752848/4vds6twPsb7/5QVGpvfauVK/Pim.obj",
        "contentType": "text/plain"
      },
      "resources": [
        {
          "relativePath": "Pim.mtl",
          "url": "https://poly.googleapis.com/downloads/fp/1602064148752848/4vds6twPsb7/5QVGpvfauVK/Pim.mtl",
          "contentType": "text/plain"
        },
        {
          "relativePath": "C:/Users/PC/Desktop/pom/pimSurface_Color.png",
          "url": "https://poly.googleapis.com/downloads/fp/1602064148752848/4vds6twPsb7/5QVGpvfauVK/C:/Users/PC/Desktop/pom/pimSurface_Color.png",
          "contentType": "image/png"
        }
      ],
      "formatComplexity": {
        "triangleCount": "586149"
      },
      "formatType": "OBJ"
    }
  ],
  "thumbnail": {
    "relativePath": "4vds6twPsb7.png",
    "url": "https://lh3.googleusercontent.com/C_il4QubLWYMAyrnGPMjXWx4E7MVYLZAoX_Hf-qr4WyHfebvf2y3lndh71A350g",
    "contentType": "image/png"
  },
  "license": "CREATIVE_COMMONS_BY",
  "visibility": "PUBLIC",
  "isCurated": true,
  "presentationParams": {
    "orientingRotation": {
      "w": 1
    },
    "colorSpace": "LINEAR",
    "backgroundColor": "#eeeeee"
  }
}

答案1

除了文件大小问题之外,有效解决方案的主要障碍是以下jq方面的原因:所有的对象,这意味着数组也被处理任何一个作为一个整体或者没有什么。因此,我们需要摆脱包含元素的主大数组,以便将后者作为从原始输入一直到的(小得多)对象的流来处理,并且包括,最终输出。

为了实现这一点,我们确实需要一些文本操作,以便jq即使对于最终的输出数组也不使用 的数组处理设施。我可以想到一些变体,每个变体都有一些(可变的)程度的工作需要通过文本操作来完成,但我会选择将这种操作限制在添加数组结构语法的绝对最低限度的变体仅最终输出。

这种文本操作是如此之少,以至于我们可以使用jq自己的文本构造来实现此目的,这不仅使我们能够拥有纯粹的jq解决方案而无需诉诸其他工具,还允许我们在遵循以下内容的同时添加所需的文本位流本身的流动,因此不依赖于预期的缩进/换行或任何其他格式假设,所有这些都有利于“手动”构建最终输出数组的更稳健的方式。这些优点的代价是脚本比通过专门的工具(例如sed.

示例输入的“一行”可能如下所示:

jq -rn --stream --arg f name --arg q 'assets/fUCcxWczWT0' '"[", foreach fromstream(1|truncate_stream(inputs)) as $o ([null,null]; if .[1] or $o[$f]? == $q then [.[1], ","] else . end; .[0]//empty, if .[1] then $o else empty end), "]"'

使用的选项jq,其中前三个对于正确结果是必需的:

  • -r用于输出任意文本时的原始(即未转换为 JSON)输出
  • -n用于通过inputs内置消耗输入数据仅有的jq,否则输入中的第一个整个对象将被的常规循环吃掉
  • --stream用于使用jq自己的 Streaming 模式来消耗输入
  • --arg f name包含我们要查找的值的字段的名称
  • --arg q 'assets/fUCcxWczWT0'我们在每个对象的字段中寻找的值

单独深入研究jq脚本,进行扩展和评论:

# Firstly, the initial bit of arbitrary text: the opening bracket for the final output,
# as required for a valid JSON array syntax

"[",

# Then a foreach statement, looping over the objects provided singularly, one by one,
# by the streamlined input
# NOTE 1: jq's streaming mode is used for this solution primarily so that we
#         can use `1 | truncate_stream()` here, which courteously (and natively)
#         strips the first (1 | ...) structure of the original input along the course of
#         the streamlined operation.
#         The first structure is obviously the main huge array containing the
#         objects, hence we receive these latter singularly in a truly streamlined
#         fashion, freed by the containment of the array
# NOTE 2: using `inputs | tostream` here in place of `--stream`, although functioning,
#         would not obtain the streaming goal, because it would first take the entire
#         input as a whole rather than streaming it from the start
    foreach fromstream( 1|truncate_stream(inputs) ) as $o (
        # the initial state of the loop: we use 2 values as a "shifting 2-value state-machine"
        # for the comma to be output (as text) along with all the elements except the
        # first. We use the second value as an overall state too
        [null, null];
        # here we look for the wanted value in the wanted field unless already found
        # previously according to the overall state, and we update the loop state "shifting"
        # the 2-state for the comma as soon as the wanted value is found
        if .[1] or $o[$f]? == $q then [.[1], ","] else . end;
        # here we print the comma text (if need be according to its 2-state)
        # as required for separating elements in a JSON array syntax, then we print
        # the object element itself if the overall state says so
        .[0]//empty, if .[1] then $o else empty end
    ),

# Lastly, the final bit of arbitrary text: the closing bracket for the output,
# as required for a valid JSON array syntax

"]"

最后几点说明:

  • 显然,这种方法假设(并且确实利用了这一事实)原始输入本质上是平坦且简单的,尽管巨大:如果它是一个更复杂的整体结构,也许内部和外部值之间有交叉引用,那么脚本很容易需要同样更加复杂和令人费解
  • 如果我们想输出元素取决于给通缉犯,而不是从...开始想要的,流逻辑可以更简单,可能根本不需要状态,因为原则上我们可以只是halt遇到想要的对象时的脚本,因此作为副作用也会导致更快的速度(完全流式操作)

答案2

问题的简化是这样的:在不阅读整个文档的情况下,如何删除数组的前几个元素,例如

[7,10,8,6,5,4,0,9,3,2,1]

...这样我们就可以得到例如

[4,0,9,3,2,1]

...4解决方案的输入查询在哪里?

答案:在不关心是否将数据读入内存的情况下,我们可以通过首先将输入数组“分解”为一组单独的数字,然后找到与我们的查询相对应的第一个数字来解决这个更简单的问题。一旦找到,我们将找到的元素和所有后续数字放入一个数组中。我们通过jq在管道中调用两次来实现此目的,其中第一次jq调用将创建一个数字流,每行一个:

$ jq '.[]' file | jq -c --argjson query 4 'select(. == $query) as $elem | [$elem, inputs]'
[4,0,9,3,2,1]

问题是输入和输出数据必须存储在内存中;输入完全由第一个jq进程解析,而输出由第二个进程收集在内存中。

如果我们能找到一种方法来避免将输入数据存储在内存中,那就太好了。我们可以尝试使用 的流媒体功能来做到这一点jq。使用 时--streamjq表达式将接收表示输入流当前状态的数组流,而无需解析完整的输入(请参阅“流媒体”手册部分jq)。

$ jq -c --stream --argjson query 4 'fromstream(select(.[1] == $query) as $elem | $elem, inputs | .[0][0] -= $elem[0][0])' file
[4,0,9,3,2,1]

上面的jq命令读取数组的流表示并找到与我们的查询相对应的第一个元素。它无需将完整输入存储在内存中即可完成此操作。

.[1]表达式求值为数组元素的值,而.[0][0]对应于数组中元素的索引(一般来说,中的数据.[0]对应于小路原始非流数据集中的值)。对于我们接受到输出中的每个元素,我们必须通过使用找到的元素的索引进行偏移来重新计算该元素的索引。这避免了存储输入在内存中,但我们可能仍然不得不承认将输出存储在内存中以转换为非流形式。

将上述命令调整为我们可以与原始数据一起使用的命令非常简单。在比较键值之前,我们还必须测试对象中的正确键。元素的键存储在.[0][1].但请注意,由于我们收到了不完整对象流,找到完全的基于name键的正确元素假设name键是第一的每个元素的关键。如果它是第二个或第三个键,我们就会丢弃该对象的初始部分。

$ cat file
[
   {
      "name": "assets/1",
      "authorName": "John Smith",
      "displayName": "The House",
      "resources": { "car": "bla", "house": "address" }
   },
   {
      "name": "assets/6",
      "authorName": "John Smith",
      "displayName": "The House",
      "resources": { "car": "bla", "house": "address" }
   },
   {
      "name": "assets/0",
      "authorName": "John Smith",
      "displayName": "The House",
      "resources": { "car": "bla", "house": "address" }
   },
   {
      "name": "assets/2",
      "authorName": "John Smith",
      "displayName": "The House",
      "resources": { "car": "bla", "house": "address" }
   }
]

以下内容删除该元素之前assets/0作为其name值的所有元素:

$ jq --stream --arg query 'assets/0' 'fromstream(select(.[0][1] == "name" and .[1] == $query) as $elem | $elem, inputs | .[0][0] -= $elem[0][0])' file
[
  {
    "name": "assets/0",
    "authorName": "John Smith",
    "displayName": "The House",
    "resources": {
      "car": "bla",
      "house": "address"
    }
  },
  {
    "name": "assets/2",
    "authorName": "John Smith",
    "displayName": "The House",
    "resources": {
      "car": "bla",
      "house": "address"
    }
  }
]

为了解决查询键必须是每个对象中的第一个键的限制,我们可以执行 2 遍操作,首先根据键确定数组元素的第一个索引name,然后使用该键来提取我们的数组元素。数据:

jq --stream --arg query 'assets/0' \
   'fromstream(select(.[0][1] == "name" and .[1] == $query) | [0,.[0][0]])' file |
   head -n 1 |
   jq --stream '.[1] as $index | fromstream(inputs | select(.[0][0] >= $index) | .[0][0] -= $index)' - file

上述管道中的第一个将输出具有我们正在查找的特定值的字段jq的所有元素的索引。name选择head -n 1这些索引中的第一个。第二个从标准输入jq将整数读入内部$index变量,然后从输入文件中提取索引等于或大于的任何元素(第二次读取)$index

相关内容