Le moyen le plus rapide d'aplatir / de ne pas aplatir les objets imbriqués JSON

j'ai jeté un peu de code ensemble pour aplatir et non aplati complexe/emboîtés objets JSON. Cela fonctionne, mais il est un peu lent (déclenche la "longue script' avertissement).

pour les noms aplatis que je veux.""comme délimiteur et [INDEX] pour les tableaux.

exemples:

un-flattened | flattened
---------------------------
{foo:{bar:false}} => {"foo.bar":false}
{a:[{b:["c","d"]}]} => {"a[0].b[0]":"c","a[0].b[1]":"d"}
[1,[2,[3,4],5],6] => {"[0]":1,"[1].[0]":2,"[1].[1].[0]":3,"[1].[1].[1]":4,"[1].[2]":5,"[2]":6}

j'ai créé un benchmark qui ~simule mon cas d'utilisation http://jsfiddle.net/WSzec/

  • obtenir un objet imbriqué 1519140920"
  • Aplatir
  • regarder à travers elle et peut-être la modifier en aplatissant
  • Unflatten de retour à l'état initial imbriquée format à être expédié

" je voudrais un code plus rapide: pour la clarification, code qui complète le benchmark JSFiddle ( http://jsfiddle.net/WSzec / ) significativement plus rapide (~20%+ serait bien) en IE 9+, FF 24+, et Chrome 29+.

voici le code JavaScript le plus rapide: http://jsfiddle.net/WSzec/6/

JSON.unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var result = {}, cur, prop, idx, last, temp;
    for(var p in data) {
        cur = result, prop = "", last = 0;
        do {
            idx = p.indexOf(".", last);
            temp = p.substring(last, idx !== -1 ? idx : undefined);
            cur = cur[prop] || (cur[prop] = (!isNaN(parseInt(temp)) ? [] : {}));
            prop = temp;
            last = idx + 1;
        } while(idx >= 0);
        cur[prop] = data[p];
    }
    return result[""];
}
JSON.flatten = function(data) {
    var result = {};
    function recurse (cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
             for(var i=0, l=cur.length; i<l; i++)
                 recurse(cur[i], prop ? prop+"."+i : ""+i);
            if (l == 0)
                result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop+"."+p : p);
            }
            if (isEmpty)
                result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
}

EDIT 1 modifié ci-dessus à l'implémentation de @Bergi qui est actuellement la plus rapide. Comme une mise à part, en utilisant ".indexOf "au lieu de" regex.exec" est environ 20% plus rapide en FF mais 20% plus lent en Chrome; je vais donc m'en tenir au regex car il est plus simple (voici ma tentative à l'aide indexOf pour remplacer le regex http://jsfiddle.net/WSzec/2 / ).

EDIT 2 en me basant sur l'idée de @Bergi, j'ai réussi à créer une version non-regex plus rapide (3X plus rapide en FF et ~10% plus rapide en Chrome). http://jsfiddle.net/WSzec/6 / dans la mise en œuvre de ceci (la mise en œuvre actuelle) les règles pour les noms de clés sont simplement, les clés ne peuvent pas commencer avec un entier ou contenir une période.

Exemple:

  • {"foo": {"bar":[0]}} => {"foo.bar.0": 0}

EDIT 3 ajout de L'approche de parsing en ligne de @AaditMShah (plutôt que String.split) a contribué à améliorer la performance non flatten. Je suis très satisfait de l'amélioration globale du rendement.

La dernière jsfiddle et jsperf:

http://jsfiddle.net/WSzec/14 /

http://jsperf.com/flatten-un-flatten/4

120
demandé sur Louis Ricci 2013-09-30 20:05:46

11 réponses

Voici mon implémentation beaucoup plus courte:

Object.unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var regex = /\.?([^.\[\]]+)|\[(\d+)\]/g,
        resultholder = {};
    for (var p in data) {
        var cur = resultholder,
            prop = "",
            m;
        while (m = regex.exec(p)) {
            cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}));
            prop = m[2] || m[1];
        }
        cur[prop] = data[p];
    }
    return resultholder[""] || resultholder;
};

flatten n'a pas beaucoup changé (et je ne suis pas sûr si vous avez vraiment besoin de ces isEmpty cas):

Object.flatten = function(data) {
    var result = {};
    function recurse (cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
             for(var i=0, l=cur.length; i<l; i++)
                 recurse(cur[i], prop + "[" + i + "]");
            if (l == 0)
                result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop+"."+p : p);
            }
            if (isEmpty && prop)
                result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
}

ensemble, ils exécuter votre benchmark dans environ la moitié du temps (Opera 12.16: ~900ms au lieu de ~ 1900ms, Chrome 29: ~800ms au lieu de ~1600ms).

164
répondu Bergi 2017-03-18 15:37:52

j'ai écrit deux fonctions à flatten et unflatten un objet JSON.


aplatir un objet JSON :

var flatten = (function (isArray, wrapped) {
    return function (table) {
        return reduce("", {}, table);
    };

    function reduce(path, accumulator, table) {
        if (isArray(table)) {
            var length = table.length;

            if (length) {
                var index = 0;

                while (index < length) {
                    var property = path + "[" + index + "]", item = table[index++];
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            } else accumulator[path] = table;
        } else {
            var empty = true;

            if (path) {
                for (var property in table) {
                    var item = table[property], property = path + "." + property, empty = false;
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            } else {
                for (var property in table) {
                    var item = table[property], empty = false;
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            }

            if (empty) accumulator[path] = table;
        }

        return accumulator;
    }
}(Array.isArray, Object));

Performance :

  1. C'est plus rapide que la solution actuelle à Opera. La solution actuelle est 26% plus lente dans Opera.
  2. c'est plus rapide que le solution actuelle dans Firefox. La solution actuelle est 9% plus lente dans Firefox.
  3. c'est plus rapide que la solution actuelle dans Chrome. La solution actuelle est 29% plus lente dans Chrome.

Unflatten a JSON object :

function unflatten(table) {
    var result = {};

    for (var path in table) {
        var cursor = result, length = path.length, property = "", index = 0;

        while (index < length) {
            var char = path.charAt(index);

            if (char === "[") {
                var start = index + 1,
                    end = path.indexOf("]", start),
                    cursor = cursor[property] = cursor[property] || [],
                    property = path.slice(start, end),
                    index = end + 1;
            } else {
                var cursor = cursor[property] = cursor[property] || {},
                    start = char === "." ? index + 1 : index,
                    bracket = path.indexOf("[", start),
                    dot = path.indexOf(".", start);

                if (bracket < 0 && dot < 0) var end = index = length;
                else if (bracket < 0) var end = index = dot;
                else if (dot < 0) var end = index = bracket;
                else var end = index = bracket < dot ? bracket : dot;

                var property = path.slice(start, end);
            }
        }

        cursor[property] = table[path];
    }

    return result[""];
}

Performance :

  1. C'est plus rapide que le solution actuelle à Opera. La solution actuelle est 5% plus lente dans Opera.
  2. c'est plus lent que la solution actuelle dans Firefox. Ma solution est 26% plus lente dans Firefox.
  3. il est plus lent que la solution actuelle dans Chrome. Ma solution est 6% plus lente dans le Chrome.

les Aplatir et les unflatten un objet JSON :

Dans l'ensemble, ma solution fonctionne aussi bien ou même mieux que la solution actuelle.

Performance :

  1. C'est plus rapide que la solution actuelle à Opera. La solution actuelle est 21% plus lente dans Opera.
  2. c'est aussi rapide que la solution actuelle dans Firefox.
  3. c'est plus rapide que la solution actuelle dans Firefox. La solution actuelle est 20% plus lente dans Chrome.

format de sortie :

un objet aplati utilise la notation par points pour les propriétés de l'objet et la notation par crochets pour les indices de tableaux:

  1. {foo:{bar:false}} => {"foo.bar":false}
  2. {a:[{b:["c","d"]}]} => {"a[0].b[0]":"c","a[0].b[1]":"d"}
  3. [1,[2,[3,4],5],6] => {"[0]":1,"[1][0]":2,"[1][1][0]":3,"[1][1][1]":4,"[1][2]":5,"[2]":6}

à mon avis, ce format est mieux qu'utiliser seulement le point note:

  1. {foo:{bar:false}} => {"foo.bar":false}
  2. {a:[{b:["c","d"]}]} => {"a.0.b.0":"c","a.0.b.1":"d"}
  3. [1,[2,[3,4],5],6] => {"0":1,"1.0":2,"1.1.0":3,"1.1.1":4,"1.2":5,"2":6}

avantages :

  1. aplatir un objet est plus rapide que la solution actuelle.
  2. aplatir et dégonfler un objet est aussi rapide ou plus rapide que la solution actuelle.
  3. objets aplatis utiliser à la fois la notation par points et la notation par crochets pour la lisibilité.

désavantages :

  1. Unflattening un objet est plus lent que la solution actuelle dans la plupart (mais pas tous) des cas.

l'actuel Démo jsfiddle a donné les valeurs suivantes en sortie:

Nested : 132175 : 63
Flattened : 132175 : 564
Nested : 132175 : 54
Flattened : 132175 : 508

Ma mise à jour JSFiddle démo a donné les valeurs suivantes en sortie:

Nested : 132175 : 59
Flattened : 132175 : 514
Nested : 132175 : 60
Flattened : 132175 : 451

Je ne suis pas vraiment sûr de ce que cela signifie, donc je vais m'en tenir aux résultats de jsPerf. Après tout, jsPerf est un outil d'analyse comparative des performances. JSFiddle ne l'est pas.

16
répondu Aadit M Shah 2013-10-06 03:02:19

basé sur le code de @Bergi j'ai fait une page Web simple pour aplatir // unflatten.

http://fiddle.jshell.net/blowsie/S2hsS/show/light /

enter image description here

JSON.flatten = function (data) {
    var result = {};

    function recurse(cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
            for (var i = 0, l = cur.length; i < l; i++)
            recurse(cur[i], prop + "[" + i + "]");
            if (l == 0) result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop + "." + p : p);
            }
            if (isEmpty && prop) result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
};
JSON.unflatten = function (data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data)) return data;
    var regex = /\.?([^.\[\]]+)|\[(\d+)\]/g,
        resultholder = {};
    for (var p in data) {
        var cur = resultholder,
            prop = "",
            m;
        while (m = regex.exec(p)) {
            cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}));
            prop = m[2] || m[1];
        }
        cur[prop] = data[p];
    }
    return resultholder[""] || resultholder;
};


$("#process").click(function () {
    var flatten = $("#flatten").is(":checked");

    var result = flatten ? JSON.stringify(JSON.flatten(JSON.parse($("#input").val())), null, "\t") : JSON.stringify(JSON.unflatten(JSON.parse($("#input").val())), null, "\t")

    $("#output").val(result);
    $("#formatted").text(result);
});
body {
    padding:20px;
}
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet"/>
<h1>JSON Flattener</h1>

<div class="form-group">
    <label>Mode:</label>
    <label class="radio-inline">
        <input id="flatten" name="mode" type="radio" value="flatten" checked="">Flatten</label>
    <label class="radio-inline">
        <input name="mode" type="radio" value="unflatten">Unflatten</label>
</div>
<div class="form-group">
    <label>Input:</label>
    <input class="form-control" type="text" name="" id="input">
</div>
<div class="form-group">
    <label>Output:</label>
    <textarea class="form-control" name="" id="output" cols="30" rows="5"></textarea>
</div>
<button id="process" class="btn btn-primary">Process</button>
<br/>
<br/>
<label>Formatted:</label>
<pre><code id="formatted"></code></pre>
10
répondu Blowsie 2015-10-05 12:58:20

Voici une autre approche qui court plus lentement (environ 1000ms) que la réponse ci-dessus, mais a une idée intéressante: -)

au lieu d'itérer à travers chaque chaîne de propriétés, il choisit simplement la dernière propriété et utilise une table de recherche pour le reste pour stocker les résultats intermédiaires. Cette table de recherche sera itérée jusqu'à ce qu'il n'y ait plus de chaînes de propriétés et que toutes les valeurs résident sur des propriétés non localisées.

JSON.unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var regex = /\.?([^.\[\]]+)$|\[(\d+)\]$/,
        props = Object.keys(data),
        result, p;
    while(p = props.shift()) {
        var m = regex.exec(p),
            target;
        if (m.index) {
            var rest = p.slice(0, m.index);
            if (!(rest in data)) {
                data[rest] = m[2] ? [] : {};
                props.push(rest);
            }
            target = data[rest];
        } else {
            target = result || (result = (m[2] ? [] : {}));
        }
        target[m[2] || m[1]] = data[p];
    }
    return result;
};

il utilise actuellement le data paramètre d'entrée pour la table, et met beaucoup de propriétés sur elle - une version non destructive devrait être aussi possible. Peut-être qu'une utilisation intelligente du lastIndexOf donne de meilleurs résultats que le regex (cela dépend du moteur regex).

Voir en action ici .

6
répondu Bergi 2013-10-06 18:28:35

3 ½ ans plus tard...

pour mon propre projet j'ai voulu aplatir des objets JSON dans mongoDB point notation et est venu avec une solution simple:

/**
 * Recursively flattens a JSON object using dot notation.
 *
 * NOTE: input must be an object as described by JSON spec. Arbitrary
 * JS objects (e.g. {a: () => 42}) may result in unexpected output.
 * MOREOVER, it removes keys with empty objects/arrays as value (see
 * examples bellow).
 *
 * @example
 * // returns {a:1, 'b.0.c': 2, 'b.0.d.e': 3, 'b.1': 4}
 * flatten({a: 1, b: [{c: 2, d: {e: 3}}, 4]})
 * // returns {a:1, 'b.0.c': 2, 'b.0.d.e.0': true, 'b.0.d.e.1': false, 'b.0.d.e.2.f': 1}
 * flatten({a: 1, b: [{c: 2, d: {e: [true, false, {f: 1}]}}]})
 * // return {a: 1}
 * flatten({a: 1, b: [], c: {}})
 *
 * @param obj item to be flattened
 * @param {Array.string} [prefix=[]] chain of prefix joined with a dot and prepended to key
 * @param {Object} [current={}] result of flatten during the recursion
 *
 * @see https://docs.mongodb.com/manual/core/document/#dot-notation
 */
function flatten (obj, prefix, current) {
  prefix = prefix || []
  current = current || {}

  // Remember kids, null is also an object!
  if (typeof (obj) === 'object' && obj !== null) {
    Object.keys(obj).forEach(key => {
      this.flatten(obj[key], prefix.concat(key), current)
    })
  } else {
    current[prefix.join('.')] = obj
  }

  return current
}

caractéristiques et/ou avertissements

  • il n'accepte que les objets JSON. Donc si vous passez quelque chose comme {a: () => {}} vous pourriez ne pas obtenir ce que vous vouliez!
  • il supprime les tableaux et objets vides. Donc ce {a: {}, b: []} est aplati en {} .
4
répondu Yan Foto 2017-02-10 10:23:43

version ES6:

const flatten = (obj, path = '') => {        
    if (!(obj instanceof Object)) return {[path.replace(/\.$/g, '')]:obj};

    return Object.keys(obj).reduce((output, key) => {
        return obj instanceof Array ? 
             {...output, ...flatten(obj[key], path +  '[' + key + '].')}:
             {...output, ...flatten(obj[key], path + key + '.')};
    }, {});
}

exemple:

console.log(flatten({a:[{b:["c","d"]}]}));
console.log(flatten([1,[2,[3,4],5],6]));
3
répondu Guy 2018-03-02 03:39:39

ce code aplatit de façon récursive les objets JSON.

j'ai inclus mon mécanisme de synchronisation dans le code et il me donne 1ms mais je ne suis pas sûr que ce soit le plus précis.

            var new_json = [{
              "name": "fatima",
              "age": 25,
              "neighbour": {
                "name": "taqi",
                "location": "end of the street",
                "property": {
                  "built in": 1990,
                  "owned": false,
                  "years on market": [1990, 1998, 2002, 2013],
                  "year short listed": [], //means never
                }
              },
              "town": "Mountain View",
              "state": "CA"
            },
            {
              "name": "qianru",
              "age": 20,
              "neighbour": {
                "name": "joe",
                "location": "opposite to the park",
                "property": {
                  "built in": 2011,
                  "owned": true,
                  "years on market": [1996, 2011],
                  "year short listed": [], //means never
                }
              },
              "town": "Pittsburgh",
              "state": "PA"
            }]

            function flatten(json, flattened, str_key) {
                for (var key in json) {
                  if (json.hasOwnProperty(key)) {
                    if (json[key] instanceof Object && json[key] != "") {
                      flatten(json[key], flattened, str_key + "." + key);
                    } else {
                      flattened[str_key + "." + key] = json[key];
                    }
                  }
                }
            }

        var flattened = {};
        console.time('flatten'); 
        flatten(new_json, flattened, "");
        console.timeEnd('flatten');

        for (var key in flattened){
          console.log(key + ": " + flattened[key]);
        }

sortie:

flatten: 1ms
.0.name: fatima
.0.age: 25
.0.neighbour.name: taqi
.0.neighbour.location: end of the street
.0.neighbour.property.built in: 1990
.0.neighbour.property.owned: false
.0.neighbour.property.years on market.0: 1990
.0.neighbour.property.years on market.1: 1998
.0.neighbour.property.years on market.2: 2002
.0.neighbour.property.years on market.3: 2013
.0.neighbour.property.year short listed: 
.0.town: Mountain View
.0.state: CA
.1.name: qianru
.1.age: 20
.1.neighbour.name: joe
.1.neighbour.location: opposite to the park
.1.neighbour.property.built in: 2011
.1.neighbour.property.owned: true
.1.neighbour.property.years on market.0: 1996
.1.neighbour.property.years on market.1: 2011
.1.neighbour.property.year short listed: 
.1.town: Pittsburgh
.1.state: PA
2
répondu sfrizvi6 2015-07-26 16:34:08

vous pouvez utiliser https://github.com/hughsk/flat

prendre un objet Javascript imbriqué et l'aplatir, ou unflatten un objet avec des clés délimitées.

exemple tiré du doc

var flatten = require('flat')

flatten({
    key1: {
        keyA: 'valueI'
    },
    key2: {
        keyB: 'valueII'
    },
    key3: { a: { b: { c: 2 } } }
})

// {
//   'key1.keyA': 'valueI',
//   'key2.keyB': 'valueII',
//   'key3.a.b.c': 2
// }


var unflatten = require('flat').unflatten

unflatten({
    'three.levels.deep': 42,
    'three.levels': {
        nested: true
    }
})

// {
//     three: {
//         levels: {
//             deep: 42,
//             nested: true
//         }
//     }
// }
2
répondu Tom Esterez 2016-04-06 16:36:31

j'ai ajouté une efficacité de +/- 10-15% à la réponse sélectionnée par un remaniement mineur du code et en déplaçant la fonction récursive en dehors de l'Espace-nom de la fonction.

Voir ma question: Sont des espaces de fonctions réévaluée à chaque appel? pour expliquer pourquoi cela ralentit les fonctions imbriquées.

function _flatten (target, obj, path) {
  var i, empty;
  if (obj.constructor === Object) {
    empty = true;
    for (i in obj) {
      empty = false;
      _flatten(target, obj[i], path ? path + '.' + i : i);
    }
    if (empty && path) {
      target[path] = {};
    }
  } 
  else if (obj.constructor === Array) {
    i = obj.length;
    if (i > 0) {
      while (i--) {
        _flatten(target, obj[i], path + '[' + i + ']');
      }
    } else {
      target[path] = [];
    }
  }
  else {
    target[path] = obj;
  }
}

function flatten (data) {
  var result = {};
  _flatten(result, data, null);
  return result;
}

voir benchmark .

1
répondu jtrumbull 2017-05-23 12:02:50

voici le mien. Il s'exécute en <2ms dans le Script Googleapps sur un objet de taille appréciable. Il utilise des tirets au lieu de points pour les séparateurs, et il ne gère pas les tableaux spécialement comme dans la question de l'asker, mais c'est ce que je voulais pour mon usage.

function flatten (obj) {
  var newObj = {};
  for (var key in obj) {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      var temp = flatten(obj[key])
      for (var key2 in temp) {
        newObj[key+"-"+key2] = temp[key2];
      }
    } else {
      newObj[key] = obj[key];
    }
  }
  return newObj;
}

exemple:

var test = {
  a: 1,
  b: 2,
  c: {
    c1: 3.1,
    c2: 3.2
  },
  d: 4,
  e: {
    e1: 5.1,
    e2: 5.2,
    e3: {
      e3a: 5.31,
      e3b: 5.32
    },
    e4: 5.4
  },
  f: 6
}

Logger.log("start");
Logger.log(JSON.stringify(flatten(test),null,2));
Logger.log("done");

exemple de sortie:

[17-02-08 13:21:05:245 CST] start
[17-02-08 13:21:05:246 CST] {
  "a": 1,
  "b": 2,
  "c-c1": 3.1,
  "c-c2": 3.2,
  "d": 4,
  "e-e1": 5.1,
  "e-e2": 5.2,
  "e-e3-e3a": 5.31,
  "e-e3-e3b": 5.32,
  "e-e4": 5.4,
  "f": 6
}
[17-02-08 13:21:05:247 CST] done
1
répondu paulwal222 2017-02-08 19:25:20

je voudrais ajouter une nouvelle version de l'aplatir cas (c'est ce dont j'avais besoin :)) qui, selon mes sondes ci-dessus jsFiddler, est légèrement plus rapide que celui sélectionné. En outre, je vois personnellement cet extrait un peu plus lisible, ce qui est bien sûr important pour les projets multi-développeurs.

function flattenObject(graph) {
    let result = {},
        item,
        key;

    function recurr(graph, path) {
        if (Array.isArray(graph)) {
            graph.forEach(function (itm, idx) {
                key = path + '[' + idx + ']';
                if (itm && typeof itm === 'object') {
                    recurr(itm, key);
                } else {
                    result[key] = itm;
                }
            });
        } else {
            Reflect.ownKeys(graph).forEach(function (p) {
                key = path + '.' + p;
                item = graph[p];
                if (item && typeof item === 'object') {
                    recurr(item, key);
                } else {
                    result[key] = item;
                }
            });
        }
    }
    recurr(graph, '');

    return result;
}
0
répondu GullerYA 2016-04-01 11:23:10