【问题标题】:Merge JSON files with identical structure into JSON file containing lists将具有相同结构的 JSON 文件合并到包含列表的 JSON 文件中
【发布时间】:2017-11-27 23:44:30
【问题描述】:

我有一些 JSON 文件,它们都具有相同的结构(到处都有相同的键,某些键的对应值可能不同)。我想将与某些键关联的值收集到列表中,并将这些列表作为与这些键关联的值存储在新的 JSON 文件中。

例如,考虑这三个文件,我对键 number_items 和相应的值感兴趣。第一个文件——

[
  {
    "box_id": 1,
    "number_items": 4
  },
  {
    "box_id": 3,
    "number_items": 15
  },
  {
    "box_id": 6,
    "number_items": 2
  }
]

第二个文件——

[
  {
    "box_id": 1,
    "number_items": 7
  },
  {
    "box_id": 3,
    "number_items": 15
  },
  {
    "box_id": 6,
    "number_items": 4
  }
]

第三个文件——

[
  {
    "box_id": 1,
    "number_items": 5
  },
  {
    "box_id": 3,
    "number_items": 9
  },
  {
    "box_id": 6,
    "number_items": 0
  }
]

这些应该合并成这样的东西——

[
  {
    "box_id": 1,
    "number_items": [
      4,
      7,
      5
    ]
  },
  {
    "box_id": 3,
    "number_items": [
      15,
      15,
      9
    ]
  },
  {
    "box_id": 6,
    "number_items": [
      2,
      4,
      0
    ]
  }
]

这可以使用jq 完成吗?如果没有,有什么好方法可以做到这一点?请注意,实际场景包含 150 多个具有 3 个键的文件,我希望将它们的值合并到列表中。

【问题讨论】:

    标签: json list merge jq


    【解决方案1】:

    根据您尝试保存此新文件的位置(本地与服务器),有几种不同的方法。据我所知,如果不使用可用插件之一 (How to write data to a JSON file using Javascript),就无法在本地保存文件。如果你想把它保存到服务器上,用 JavaScript 是不可能的,最好用后台语言来完成。

    这是一种将多个 JSON 文件的内容组合成所需格式的方法。

    // send json files you want combined, and a new file path and name (path/to/filename.json)
      function combineJsonFiles(files, newFileName) {
        var combinedJson = [];
        // iterate through each file 
        $.each(files, function(key, fileName) {
          // load json file
          // wait to combine until loaded. without this 'when().done()', boxes would return 'undefined'
          $.when(loadJsonFile(fileName)).done(function(boxes) {
            // combine json from file with combinedJson array
            combinedJson = combineJson(boxes, combinedJson);
            // check if this is the last file
            if (key == files.length-1) {
              // puts into json format
              combinedJson = JSON.stringify(combinedJson);
              // your json is now ready to be saved to a file
            }
          });
        });
      }
    
      function loadJsonFile(fileName) {
        return $.getJSON(fileName);
      }
    
    
    
    function combineJson(boxes, combinedJson) {
      // iterate through each box 
      $.each(boxes, function(key, box) {
        // use grep to search if this box's id is already included
        var matches = $.grep(combinedJson, function(e) { return e.box_id == box.box_id; });
    
        // if there are no matches, add box to the combined file
        if (matches.length == 0) {
    
          var newBox = { box_id: box.box_id };
    
          // iterate through properties of box
          for (var property in box) {
            // check to ensure that properties are not inherited from base class
            if (box.hasOwnProperty(property)) {
              // will ignore if property is box_id
              if (property !== 'box_id') {
                // box is reformatted to make the property type into array
                newBox[property] = [box[property]];
              }
            }
          }
          combinedJson.push(newBox);
        } else {
          // select first match (there should never be more than one)
          var match = matches[0];
    
          // iterate through properties of box
          for (var property in box) {
            // check to ensure that properties are not inherited from base class
            if (box.hasOwnProperty(property)) {
              // will ignore if property is box_id
              if (property !== 'box_id') {
                // add property to the already existing box in the combined file
                match[property].push(box[property]);
              }
            }
          }
        }
      });
      return combinedJson;
    }
    
      var jsonFiles = ['path/to/data.json', 'path/to/data2.json', 'path/to/data3.json'];
    
      combineJsonFiles(jsonFiles, 'combined_json.json');
    

    此 JSON 输出将如下所示:

    [{"box_id":1,"number_items":[4,7,5]},{"box_id":3,"number_items":[15,15,9]},{"box_id":6,"number_items":[2,4,0]}]
    

    希望这会有所帮助!

    【讨论】:

      【解决方案2】:

      您可以合并具有相似结构的文件,只需将它们全部作为输入传入即可。它们的内容将按它们所在的顺序流式传输。

      然后您可以将它们读入单个数组,按box_id 对对象进行分组,然后绘制结果。

      $ jq -n '
          [inputs[]] | group_by(.box_id)
              | map({box_id:.[0].box_id, number_items:map(.number_items)})
      ' input{1,2,3}.json
      

      产生:

      [
        {
          "box_id": 1,
          "number_items": [
            4,
            7,
            5
          ]
        },
        {
          "box_id": 3,
          "number_items": [
            15,
            15,
            9
          ]
        },
        {
          "box_id": 6,
          "number_items": [
            4,
            2,
            0
          ]
        }
      ]
      

      在某些平台上对项目进行分组时,似乎没有保留顺序。就我而言,在 Windows 64 位版本上运行会产生这种情况。因此,如果您想使用group_by,请注意这一点。如果您想避免使用此过滤器,当然可以采取其他方法,但使用起来更方便。

      【讨论】:

      • Jeff - 当我运行你的程序时,我得到了最后一个 number_items 数组的 [2,4,0],正如预期的那样。你真的得到 [4,2,0] 了吗?
      • 嗯,我按原样复制了数据和收到的输出。我也期待[2,4,0]。看起来它是从分组中重新排序的,而不是我所期望的。
      • @JeffMercado 太好了。快速跟进问题,如何显示每个文件的 number_items 总和?使用jq -s 'map(.[].number_items) | add' input{1,2,3}.json 返回61,即所有文件中所有项目的总和。当然,我可以为每个文件手动运行命令,然后收集所有内容,但我想知道jq 是否可以为我执行此操作?
      • (在 Linux 上,我会使用 for k in *.json; do jq -s 'map(.[].number_items) | add' $k; done 来执行此操作)
      • @Ailurus:对于每个单独文件的总和,只是不要在开始时将它们组合起来。您甚至可以获取当前正在处理的文件的名称。我愿意:jq '{file:input_filename, sum:map(.number_items)|add}' *.json.
      【解决方案3】:

      我想收集与某些键关联的值

      这是一种以相同方式处理除分组键之外的所有键的解决方案。它还可以优雅地处理丢失的键,并且不依赖于 jq 的sort 的稳定性。该解决方案基于通用过滤器merge/0,定义如下:

      # Combine an array of objects into a single object, ans, with array-valued keys,
      # such that for every key, k, in the i-th object of the input array, a,
      # ans[k][i] = a[i][k]
      # null is used as padding if a value is missing.
      # Example:
      # [{a:1, b:2}, {b:3, c:4}] | merge
      # produces:
      # {"a":[1,null],"b":[2,3],"c":[null,4]}
      def merge:
        def allkeys: map(keys) | add | unique;
        allkeys as $allkeys
        | reduce .[] as $in ({};
           reduce $allkeys[] as $k (.;
            . + {($k): (.[$k] + [$in[$k]]) } ));
      

      给定问题的解决方案可以表述为:

      transpose | map(merge) | map( .box_id |= .[0] )
      

      调用:

        jq -s -f merge.jq input{1,2,3}.json
      

      输出:如题所示。

      更强大的解决方案

      上述解决方案假设每个文件中box_id 的排序是一致的。这个假设似乎符合 OP 要求,但为了安全性和稳健性,首先对对象进行排序:

      map(sort_by(.box_id)) | transpose | map( merge | (.box_id |= .[0]) )
      

      请注意,这仍然假设在任何输入文件中都没有 box_id 的缺失值。

      更强大的解决方案

      如果任何输入文件中可能缺少某些box_id 值,则添加缺失值是合适的。这可以在以下过滤器的帮助下完成:

      # Input: a matrix of objects (that is, an array of rows of objects),
      #   each of which is assumed to have a distinguished field, f,
      #   with distinct values on each row;
      # Output: a rectangular matrix such that every row, r, of the output
      #   matrix includes the elements of the corresponding row of the input
      #   matrix, with additional elements as necessary so that (r |
      #   map(.id) | sort) is the same for all rows r.
      #
      def rectanglize(f):
        def ids: [.[][] | f] | unique;
        def it: . as $in | {} | (f = $in);
        ids as $ids
        | map( . + ( $ids - [.[]|f] | map(it) ) )
      ;  
      

      把所有东西放在一起,主管道就变成了:

      rectanglize(.id)
      | map(sort_by(.box_id))
      | transpose 
      | map( merge | .box_id |= .[0] )
      

      【讨论】:

      • 不错的方法,在此之上普遍适用且稳定。谢谢!尽管如此,Jeff 的回答更容易理解,运行起来也很简单,对于我的简单数据文件来说已经足够了。因此,我会将他的答案标记为已接受。
      猜你喜欢
      • 1970-01-01
      • 2019-12-15
      • 1970-01-01
      • 1970-01-01
      • 2019-06-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多