【问题标题】:Selectively flattening a nested JSON structure选择性地展平嵌套的 JSON 结构
【发布时间】:2017-02-02 00:42:37
【问题描述】:

所以这是一个我什至不知道从哪里开始的问题,所以即使只是指向正确方向的指针也会很棒。

所以我的数据看起来像这样:

data = {
   "agg": {
      "agg1": [
         {
            "keyWeWant": "*-20.0",
            "asdf": 0,
            "asdf": 20,
            "asdf": 14,
            "some_nested_agg": [
               {
                  "keyWeWant2": 20,
                  "to": 25,
                  "doc_count": 4,
                  "some_nested_agg2": {
                     "count": 7,
                     "min": 2,
                     "max": 5,
                     "keyWeWant3": 2.857142857142857,
                     "sum": 20
                  }
               },
               {
                  "keyWeWant2": 25,
                  "to": 30,
                  "doc_count": 10,
                  "some_nested_agg2": {
                     "count": 16,
                     "min": 2,
                     "max": 10,
                     "keyWeWant3": 6.375,
                     "sum": 102
                  }
               }
            ]
         },
         {
         ...
         },
         {
         ...
         },
         ...
      ]
   }
}

现在从示例中,在“agg”中有 N 个“agg1”结果,在每个“agg1”结果中有一个“keyWeWant”。每个“agg1”结果还有一个“some_nested_agg”结果列表,每个结果都包含一个“keyWeWant2”。每个“keyWeWant2”值都与层次结构中某处的单个“keyWeWant”值相关联。类似地,每个“keyWeWant2”还包含一组“some_nested_agg2”的结果(这次不是列表,而是地图)。每组结果都包含一个“keyWeWant3”。

现在我想扁平化这个结构,同时仍然保留 'keyWeWant'、'keyWeWant2' 和 'keyWeWant3' 之间的关联(我本质上是反规范化)以获得类似这样的结果:

我希望函数看起来像什么:

[
   {
      "keyWeWant" : "*-20",
      "keyWeWant2" : 20,
      "keyWeWant3" : 2.857142857142857
   },
   {
      "keyWeWant" : "*-20",
      "keyWeWant2" : 25,
      "keyWeWant3" : 6.375
   },
   {
   ...
   },
   {
   ...
   }
]

这是一个只有深度 3 的示例,但可能存在任意深度,其中一些嵌套值是列表,一些是数组/列表。

我想做的是编写一个函数来接收我想要的键以及在哪里找到它们,然后去获取键并进行非规范化。

看起来像:

function_name(data_map, {
   "keyWeWant" : ['agg', 'agg1'],
   "keyWeWant2" : ['agg', 'agg1', 'some_nested_agg'],
   "keyWeWant" : ['agg', 'agg1', 'some_nested_agg', 'some_nested_agg2']
})

有什么想法吗?我熟悉 Java、Clojure、Java-script 和 Python,并且正在寻找一种相对简单的方法来解决这个问题。

【问题讨论】:

  • 仅供参考 - 这是一个 javascript 对象,而不是 JSON structure - JSON 是一个 javascript 对象的字符串表示法 - 你没有在这段代码中处理它
  • What I want the function to look like - 这不是一个函数,这是另一个 javascript 对象
  • 我明白你的意思,但这在技术上处理的是从弹性搜索返回的 JSON。现在,在解析数据之后,它变成了 JS 中的 Object,或 Python 中的 dict,Java 中的 Map 或 clojure 中的 hash-map。关于“我希望函数看起来像什么”,我在谈论函数输入。我希望它接收数据(以任何语言表示)和我想要的键的映射以及在哪里可以找到它们。我实际上是用 Python 编写的,只是提供了一个我想如何调用该函数的示例。

标签: javascript python json elasticsearch clojure


【解决方案1】:

这是一个您可以使用的 JavaScript (ES6) 函数:

function flatten(data, keys) {
    var key = keys[0];
    if (key in data)
        keys = keys.slice(1);
    var res = keys.length && Object.keys(data)
        .map( key => data[key] )
        .filter( val => Object(val) === val )
        .reduce( (res, val) => res.concat(flatten(val, keys)), []);
    return !(key in data) ? res
        : (res || [{}]).map ( obj => Object.assign(obj, { [key]: data[key] }) );
}

// Sample data
var data = {
   "agg": {
      "agg1": [
         {
            "keyWeWant": "*-20.0",
            "asdf": 0,
            "asdf": 20,
            "asdf": 14,
            "some_nested_agg": [
               {
                  "keyWeWant2": 20,
                  "to": 25,
                  "doc_count": 4,
                  "some_nested_agg2": {
                     "count": 7,
                     "min": 2,
                     "max": 5,
                     "keyWeWant3": 2.857142857142857,
                     "sum": 20
                  }
               },
               {
                  "keyWeWant2": 25,
                  "to": 30,
                  "doc_count": 10,
                  "some_nested_agg2": {
                     "count": 16,
                     "min": 2,
                     "max": 10,
                     "keyWeWant3": 6.375,
                     "sum": 102
                  }
               }
            ]
         },
      ]
   }
};

// Flatten it by array of keys
var res = flatten(data, ['keyWeWant', 'keyWeWant2', 'keyWeWant3']);

// Output result
console.log(res);

使用路径的替代方法

如 cmets 中所述,上述代码没有使用路径信息;它只是在所有数组中查找。如果要查找的键也出现在应该被忽略的路径中,这可能是一个问题。

以下替代方案将使用路径信息,该信息应作为子数组的数组传递,其中每个子数组首先列出路径键,最后一个元素是要保留的值键:

function flatten(data, [path, ...paths]) {
    return path && (
        Array.isArray(data)
            ? data.reduce( (res, item) => res.concat(flatten(item, arguments[1])), [] )
            : path[0] in data && (
                path.length > 1 
                    ? flatten(data[path[0]], [path.slice(1), ...paths])
                    : (flatten(data, paths) || [{}]).map ( 
                        item => Object.assign(item, { [path[0]]: data[path[0]] }) 
                    )
            )
    );
}

// Sample data
var data = {
   "agg": {
      "agg1": [
         {
            "keyWeWant": "*-20.0",
            "asdf": 0,
            "asdf": 20,
            "asdf": 14,
            "some_nested_agg": [
               {
                  "keyWeWant2": 20,
                  "to": 25,
                  "doc_count": 4,
                  "some_nested_agg2": {
                     "count": 7,
                     "min": 2,
                     "max": 5,
                     "keyWeWant3": 2.857142857142857,
                     "sum": 20
                  }
               },
               {
                  "keyWeWant2": 25,
                  "to": 30,
                  "doc_count": 10,
                  "some_nested_agg2": {
                     "count": 16,
                     "min": 2,
                     "max": 10,
                     "keyWeWant3": 6.375,
                     "sum": 102
                  }
               }
            ]
         },
      ]
   }
};

// Flatten it by array of keys
var res = flatten(data, [
    ['agg', 'agg1', 'keyWeWant'], 
    ['some_nested_agg', 'keyWeWant2'], 
    ['some_nested_agg2', 'keyWeWant3']]);

// Output result
console.log(res);

【讨论】:

  • 虽然这是一个非常酷的简洁实现(我喜欢 ECMAScript 6 提供的语法糖),但它完全忽略了路径。问题在于不同路径下可能有多个“keysWeWant”。我们只想要一条道路上的那些人,而不是其他道路上的那些人。
  • 是的,如果键也在数据的其他部分,但应该被忽略,那么这个解决方案太简单了。我在我的答案中添加了一个替代函数,它确实将路径作为参数,并且只从那里收集信息。
【解决方案2】:

可能有更好的方法来解决这个特定问题(使用一些 ElasticSearch 库或其他东西),但这里有一个 Clojure 中的解决方案,使用您请求的输入和输出数据格式。

我把这个测试数据放在一个名为data.json的文件中:

{
    "agg": {
        "agg1": [
            {
                "keyWeWant": "*-20.0",
                "asdf": 0,
                "asdf": 20,
                "asdf": 14,
                "some_nested_agg": [
                    {
                        "keyWeWant2": 20,
                        "to": 25,
                        "doc_count": 4,
                        "some_nested_agg2": {
                            "count": 7,
                            "min": 2,
                            "max": 5,
                            "keyWeWant3": 2.857142857142857,
                            "sum": 20
                        }
                    },
                    {
                        "keyWeWant2": 25,
                        "to": 30,
                        "doc_count": 10,
                        "some_nested_agg2": {
                            "count": 16,
                            "min": 2,
                            "max": 10,
                            "keyWeWant3": 6.375,
                            "sum": 102
                        }
                    }]
            }]}
}

然后Cheshire JSON library将数据解析为Clojure数据结构:

(use '[cheshire.core :as cheshire])

(def my-data (-> "data.json" slurp cheshire/parse-string))

接下来要获取的路径定义如下:

(def my-data-map
  {"keyWeWant"  ["agg", "agg1"],
   "keyWeWant2" ["agg", "agg1", "some_nested_agg"],
   "keyWeWant3" ["agg", "agg1", "some_nested_agg", "some_nested_agg2"]})

上面是你的data_map,没有“:”,单引号改成双引号,最后一个“keyWeWant”改成“keyWeWant3”。

find-nested 下面具有 Clojure 的 get-in 的语义,只有这样它才能在带有向量的映射上工作,并返回所有值而不是一个。 当find-nested 被赋予一个搜索向量时,它会在嵌套映射中查找所有值,其中一些值可以由带有映射列表的向量组成。检查向量中的每个地图。

(defn find-nested
  "Finds all values in a coll consisting of maps and vectors.

  All values are returned in a tree structure:
  i.e, in your problem it returns (20 25) if you call it with
  (find-nested ['agg', 'agg1', 'some_nested_agg', 'keyWeWant2'] 
  my-data).

  Returns nil if not found."
  [ks c]
  (let [k (first ks)]
    (cond (nil? k)    c
          (map? c)    (find-nested (rest ks) (get c k))
          (vector? c) (if-let [e (-> c first (get k))]
                        (if (string? e) e ; do not map over chars in str
                            (map (partial find-nested (rest ks)) e))
                        (find-nested ks (into [] (rest c)))) ; create vec again
          :else       nil)))

find-nested 查找搜索路径的值:

(find-nested ["agg", "agg1", "some_nested_agg", "keyWeWant2"] my-data) 
; => (20 25)

如果通向“keyWeWant”的所有路径都映射到my-data,则这些是tree 的切片:

(*-20.0
(20 25)
(2.857142857142857 6.375))

可以从tree 中的function-name 中获得您要求的结构(所有最终结果以及到达那里的路径),如下所示:

(defn function-name
  "Transforms data d by finding (nested keys) via data-map m in d and 
  flattening the structure."
  [d m]
  (let [tree               (map #(find-nested (conj (second %) (first %)) d) m)
        leaves             (last tree)
        leaf-indices       (range (count leaves))
        results            (for [index leaf-indices]
                             (map (fn [slice]
                                    (if (string? slice)
                                      slice
                                      (loop [node (nth slice index)]
                                        (if node
                                          node
                                          (recur (nth slice (dec index)))))))
                                  tree))
        results-with-paths (mapv #(zipmap (keys m) %) results)
        json               (cheshire/encode results-with-paths)]
    json))

results 使用 loop 如果 leaf-index 大于该特定切片,则后退。我认为它也适用于更深的嵌套结构 - 如果下一个切片始终是前一个切片的两倍或它应该工作的相同大小 - 但我还没有测试过。

调用 (function-name my-data my-data-map) 会生成您请求格式的 JSON 字符串:

[{
"keyWeWant": "-20.0",
“keyWeWant2”:20,
"keyWeWant3": 2.857142857142857 }
{
"keyWeWant": "
-20.0",
"keyWeWant2" 25,
"keyWeWant3" 6.375 }]

/编辑 我看到您正在寻找一个相对简单的解决方案,但事实并非如此。 :-) 也许有一个图书馆没有它。我很高兴知道如何简化它。

【讨论】:

  • 你的 clojure 技能太疯狂了!这实际上很酷,我一定要尝试一下。你的代码实际上给了我一个利用多方法和传递上下文的实现的想法。谢谢:)
  • @AbhinavRamakrishnan 另一种在嵌套结构中查找多个嵌套值的方法是使用specter。方法方法specter/select可以与指令specter/ALL一起使用导航到向量,specter/VAL捕获路径,selected?通过谓词查找节点。
猜你喜欢
  • 2019-01-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-24
  • 1970-01-01
  • 2019-10-31
  • 2020-05-03
  • 2016-10-06
相关资源
最近更新 更多