【问题标题】:Trim white spaces in both Object key and value recursively递归修剪对象键和值中的空格
【发布时间】:2015-11-03 22:52:36
【问题描述】:

如何递归地修剪 JavaScript 对象中键和值中的空格?

我遇到了一个问题,我试图“清理”用户提供的 JSON 字符串并将其发送到我的其他代码中以进行进一步处理。

假设我们有一个用户提供的 JSON 字符串,其属性键和值都是“字符串”类型。但是,在这种情况下,问题在于键和值不像期望的那样干净。说一个 { " key_with_leading_n_trailing_spaces ": " my_value_with_leading_spaces" }。

在这种情况下,它很容易导致您编写出色的 JavaScript 程序尝试使用此类数据(或者我们应该称之为脏数据吗?),因为当您的代码试图从这个 JSON 对象中获取值时,不仅key不匹配,value也不匹配。我浏览了谷歌并找到了一些提示,但没有一种方法可以治愈这一切。

鉴于此 JSON 在键和值中有大量空格。

var badJson = {
  "  some-key   ": "    let it go    ",
  "  mypuppy     ": "    donrio   ",
  "   age  ": "   12.3",
  "  children      ": [
    { 
      "   color": " yellow",
      "name    ": "    alice"
    },    { 
      "   color": " silver        ",
      "name    ": "    bruce"
    },    { 
      "   color": " brown       ",
      "     name    ": "    francis"
    },    { 
      "   color": " red",
      "      name    ": "    york"
    },

  ],
  "     house": [
    {
      "   name": "    mylovelyhouse     ",
      " address      " : { "number" : 2343, "road    "  : "   boardway", "city      " : "   Lexiton   "}
    }
  ]

};

这就是我想出的(在使用 lodash.js 的帮助下):

//I made this function to "recursively" hunt down keys that may 
//contain leading and trailing white spaces
function trimKeys(targetObj) {

  _.forEach(targetObj, function(value, key) {

      if(_.isString(key)){
        var newKey = key.trim();
        if (newKey !== key) {
            targetObj[newKey] = value;
            delete targetObj[key];
        }

        if(_.isArray(targetObj[newKey]) || _.isObject(targetObj[newKey])){
            trimKeys(targetObj[newKey]);
        }
      }else{

        if(_.isArray(targetObj[key]) || _.isObject(targetObj[key])){
            trimKeys(targetObj[key]);
        }
      }
   });

}

//I stringify this is just to show it in a bad state
var badJson = JSON.stringify(badJson);

console.log(badJson);

//now it is partially fixed with value of string type trimed
badJson = JSON.parse(badJson,function(key,value){
    if(typeof value === 'string'){
        return value.trim();
    }
    return value;
});

trimKeys(badJson);

console.log(JSON.stringify(badJson));

请注意:我分 1、2 步完成此操作,因为我找不到更好的一次性解决方案。如果我的代码有问题或有更好的地方,请与我们分享。

谢谢!

【问题讨论】:

  • 技术上它不是 JSON。
  • 删除了 json 标记,因为您谈论的是 javascript 对象文字,而不是 JSON。
  • 谢谢,epascarello,我可能没有准确地使用这个词,但这是一个微不足道的 JavaScript 对象。如果您不介意,请告诉我它在哪里不符合 JSON 对象的条件。
  • 现在我看到了区别,我应该说它是一个 javscript 对象文字。谢谢,迈克!我尝试了 RobG 的建议,但我得到 obj.reduce 不是函数。你指的是node js npm包中的object.reduce吗?
  • @vichsu—哎呀,reduce 是一种数组方法,我的意思是遍历Object.keys(obj).reduce(...),但是该函数也需要递归。目前没有足够的时间来回答。

标签: javascript object recursion trim


【解决方案1】:

你可以对它进行字符串化、字符串替换和重新解析

JSON.parse(JSON.stringify(badJson).replace(/"\s+|\s+"/g,'"'))

【讨论】:

  • 这看起来不错!谢谢,埃帕斯卡雷罗!这看起来非常简洁。
  • 如果对象中有引号,例如:{" key ": ' 和 "let" it '},Stringify 将转义它们,但正则表达式也会去除它们周围的空格,产生 '和“让它”。由于 JS 没有负面的lookbehinds,你可以用一个函数来解决这个问题:JSON.parse(JSON.stringify(badJson).replace(/(\\)?"\s*|\s+"/g, ($0, $1) => $1 ? $0 : '"'))
  • 您还可以使用带有JSON.stringify 的替换函数来检查值是否为字符串,如果是则修剪它。见this CodePen。我确信这可以针对性能进行优化,但它很容易阅读,有点简洁,并且对于合理大小的对象应该执行得很好。
  • @AlexMueller 有正确的解决方案。请不要使用正则表达式操作字符串化的 JSON —— 正是这样的东西给 javascript 程序员带来了坏名声。即使使用带有负面后视功能的改进正则表达式,请考虑:'" ' 该字符串有 5 个尾随空格,并且没有得到正确处理。
【解决方案2】:

您可以使用 Object.keys 清理属性名称和属性以获取键数组,然后 Array.prototype.reduce 遍历键并使用修剪的键和值创建一个新对象。该函数需要是递归的,以便它还修剪嵌套的对象和数组。

注意它只处理普通数组和对象,如果你想处理其他类型的对象,对 reduce 的调用需要更复杂来确定对象的类型(例如new obj.constructor()) 的适当聪明版本。

function trimObj(obj) {
  if (!Array.isArray(obj) && typeof obj != 'object') return obj;
  return Object.keys(obj).reduce(function(acc, key) {
    acc[key.trim()] = typeof obj[key] == 'string'? obj[key].trim() : trimObj(obj[key]);
    return acc;
  }, Array.isArray(obj)? []:{});
}

【讨论】:

  • 谢谢,@RobG。这很好用!我只是想知道为什么我们必须编写代码来处理这样的问题。为什么原生 JS 默认不能有这样的方法?
  • 有一个TC39 mailing list。去吧。 ;-)
  • 将第一行更改为if (obj === null || !Array.isArray(obj) && typeof obj != 'object') return obj; 帮助我避免了空对象的错误
【解决方案3】:

我使用的最佳解决方案是这个。查看replacer function 上的文档。

function trimObject(obj){
  var trimmed = JSON.stringify(obj, (key, value) => {
    if (typeof value === 'string') {
      return value.trim();
    }
    return value;
  });
  return JSON.parse(trimmed);
}

var obj = {"data": {"address": {"city": "\n \r     New York", "country": "      USA     \n\n\r"}}};
console.log(trimObject(obj));

【讨论】:

    【解决方案4】:

    epascarello 上面的回答加上一些单元测试(只是为了我确定):

    function trimAllFieldsInObjectAndChildren(o: any) {
      return JSON.parse(JSON.stringify(o).replace(/"\s+|\s+"/g, '"'));
    }
    
    import * as _ from 'lodash';
    assert.true(_.isEqual(trimAllFieldsInObjectAndChildren(' bob '), 'bob'));
    assert.true(_.isEqual(trimAllFieldsInObjectAndChildren('2 '), '2'));
    assert.true(_.isEqual(trimAllFieldsInObjectAndChildren(['2 ', ' bob ']), ['2', 'bob']));
    assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'b ': ' bob '}), {'b': 'bob'}));
    assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'b ': ' bob ', 'c': 5, d: true }), {'b': 'bob', 'c': 5, d: true}));
    assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'b ': ' bob ', 'c': {' d': 'alica c c '}}), {'b': 'bob', 'c': {'d': 'alica c c'}}));
    assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'a ': ' bob ', 'b': {'c ': {'d': 'e '}}}), {'a': 'bob', 'b': {'c': {'d': 'e'}}}));
    assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'a ': ' bob ', 'b': [{'c ': {'d': 'e '}}, {' f ': ' g ' }]}), {'a': 'bob', 'b': [{'c': {'d': 'e'}}, {'f': 'g' }]}));
    

    【讨论】:

      【解决方案5】:

      我认为一个通用的map 函数可以很好地处理这个问题。它将深度对象遍历和转换与我们希望执行的特定操作分开 -

      const identity = x =>
        x
      
      const map = (f = identity, x = null) =>
        Array.isArray(x)
          ? x.map(v => map(f, v))
      : Object(x) === x
          ? Object.fromEntries(Object.entries(x).map(([ k, v ]) => [ map(f, k), map(f, v) ]))
          : f(x)
      
      const dirty = 
      ` { "  a  ": "  one "
        , " b": [ null,  { "c ": 2, " d ": { "e": "  three" }}, 4 ]
        , "  f": { "  g" : [ "  five", 6] }
        , "h " : [[ [" seven  ", 8 ], null, { " i": " nine " } ]]
        , " keep  space  ": [ " betweeen   words.  only  trim  ends   " ]
        }
      `
        
      const result =
        map
         ( x => String(x) === x ? x.trim() : x // x.trim() only if x is a String
         , JSON.parse(dirty)
         )
         
      console.log(JSON.stringify(result))
      // {"a":"one","b":[null,{"c":2,"d":{"e":"three"}},4],"f":{"g":["five",6]},"h":[[["seven",8],null,{"i":"nine"}]],"keep  space":["betweeen   words.  only  trim  ends"]}

      map 可以重复使用以轻松应用不同的转换 -

      const result =
        map
         ( x => String(x) === x ? x.trim().toUpperCase() : x
         , JSON.parse(dirty)
         )
      
      console.log(JSON.stringify(result))
      // {"A":"ONE","B":[null,{"C":2,"D":{"E":"THREE"}},4],"F":{"G":["FIVE",6]},"H":[[["SEVEN",8],null,{"I":"NINE"}]],"KEEP  SPACE":["BETWEEEN   WORDS.  ONLY  TRIM  ENDS"]}
      

      使map实用

      感谢 Scott 的评论,我们为 map 添加了一些人体工程学。在这个例子中,我们把trim写成一个函数-

      const trim = (dirty = "") =>
        map
         ( k => k.trim().toUpperCase()          // transform keys
         , v => String(v) === v ? v.trim() : v  // transform values
         , JSON.parse(dirty)                    // init
         )
      

      这意味着 map 现在必须接受 两个 函数参数 -

      const map = (fk = identity, fv = identity, x = null) =>
        Array.isArray(x)
          ? x.map(v => map(fk, fv, v)) // recur into arrays
      : Object(x) === x
          ? Object.fromEntries(
              Object.entries(x).map(([ k, v ]) =>
                [ fk(k)           // call fk on keys
                , map(fk, fv, v)  // recur into objects
                ] 
              )
            )
      : fv(x) // call fv on values
      

      现在我们可以看到键转换与值转换分开工作。字符串值得到一个简单的.trim,而键得到.trim().toUpperCase() -

      console.log(JSON.stringify(trim(dirty)))
      // {"A":"one","B":[null,{"C":2,"D":{"E":"three"}},4],"F":{"G":["five",6]},"H":[[["seven",8],null,{"I":"nine"}]],"KEEP  SPACES":["betweeen   words.  only  trim  ends"]}
      

      展开下面的sn-p,在自己的浏览器中验证结果-

      const identity = x =>
        x
      
      const map = (fk = identity, fv = identity, x = null) =>
        Array.isArray(x)
          ? x.map(v => map(fk, fv, v))
      : Object(x) === x
          ? Object.fromEntries(
              Object.entries(x).map(([ k, v ]) =>
                [ fk(k), map(fk, fv, v) ]
              )
            )
      : fv(x)
      
      const dirty = 
      ` { "  a  ": "  one "
        , " b": [ null,  { "c ": 2, " d ": { "e": "  three" }}, 4 ]
        , "  f": { "  g" : [ "  five", 6] }
        , "h " : [[ [" seven  ", 8 ], null, { " i": " nine " } ]]
        , " keep  spaces  ": [ " betweeen   words.  only  trim  ends   " ]
        }
      `
      
      const trim = (dirty = "") =>
        map
         ( k => k.trim().toUpperCase()
         , v => String(v) === v ? v.trim() : v
         , JSON.parse(dirty)
         )
         
      console.log(JSON.stringify(trim(dirty)))
      // {"A":"one","B":[null,{"C":2,"D":{"E":"three"}},4],"F":{"G":["five",6]},"H":[[["seven",8],null,{"I":"nine"}]],"KEEP  SPACES":["betweeen   words.  only  trim  ends"]}

      【讨论】:

      • 或者,也许,将具有相似实现的 mapmapKeys 函数分开,然后简单地将一个管道连接到另一个。类似pipe (mapKeys (toUpper), map (trim)).
      • 能够分别作用于键和值将是值得改进的。也许像map((k, v) => [ k.trim(), String(v) === v ? v.trim() : v ], dirtyObj) 这样的东西?它比原来的调用稍微冗长一些,但能够区分键和值要有用得多。
      • 嗯,比这更棘手。您的 mapKeys->map 是两次通过输入,但这是我现在可以看到的唯一解决方案。
      • 我根据您的评论添加了更新。从来没有像这样写过map,但我认为它可能有一些东西......
      • 不幸的是,它不是真正的双函子,或者可能有更多关于此的文献。密钥必须映射回密钥类型 (String/Symbol),因此没有真正的参数化。但它似乎仍然密切相关并且真正有用。我不喜欢它的是参数的位置性质意味着虽然我们可以使用它轻松实现mapKeysmapValues,但直接使用它来做其中之一并不容易;这是可能的,但很难看。 (更新:我想这还不错,如果你只是通过 identity 来获取缺少的功能。)
      【解决方案6】:

      类似于 epascarello 的回答。这就是我所做的:

      import java.util.regex.Matcher;
      import java.util.regex.Pattern;
      
      ........
      
      public String trimWhiteSpaceAroundBoundary(String inputJson) {
          String result;
          final String regex = "\"\\s+|\\s+\"";
          final Pattern pattern = Pattern.compile(regex);
          final Matcher matcher = pattern.matcher(inputJson.trim());
          // replacing the pattern twice to cover the edge case of extra white space around ','
          result = pattern.matcher(matcher.replaceAll("\"")).replaceAll("\"");
          return result;
      }
      

      测试用例

      assertEquals("\"2\"", trimWhiteSpace("\" 2 \""));
      assertEquals("2", trimWhiteSpace(" 2 "));
      assertEquals("{   }", trimWhiteSpace("   {   }   "));
      assertEquals("\"bob\"", trimWhiteSpace("\" bob \""));
      assertEquals("[\"2\",\"bob\"]", trimWhiteSpace("[\"  2  \",  \"  bob  \"]"));
      assertEquals("{\"b\":\"bob\",\"c c\": 5,\"d\": true }",
                    trimWhiteSpace("{\"b \": \" bob \", \"c c\": 5, \"d\": true }"));
      

      【讨论】:

        【解决方案7】:

        我尝试了上面的 JSON.stringify 解决方案,但它不适用于像 '"this is \'my\' test"' 这样的字符串。您可以使用 stringify 的替换函数绕过它,只需修剪输入的值。

        JSON.parse(JSON.stringify(obj, (key, value) => (typeof value === 'string' ? value.trim() : value)))

        【讨论】:

          【解决方案8】:

          @RobG 感谢您的解决方案。添加一个条件不会创建更多的嵌套对象

          function trimObj(obj) {
                if (obj === null && !Array.isArray(obj) && typeof obj != 'object') return obj;
                return Object.keys(obj).reduce(function(acc, key) { 
                  acc[key.trim()] = typeof obj[key] === 'string' ? 
                    obj[key].trim() : typeof obj[key] === 'object' ?  trimObj(obj[key]) : obj[key];
                  return acc;
                }, Array.isArray(obj)? []:{});
              }
          

          【讨论】:

            猜你喜欢
            • 2022-01-05
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-06-08
            • 1970-01-01
            • 2020-09-09
            相关资源
            最近更新 更多