【问题标题】:Create nested json from parsed textinput从解析的文本输入创建嵌套 json
【发布时间】:2020-08-06 07:07:14
【问题描述】:

我正在尝试创建一个 javascript 函数来将文本解析为嵌套的 JSON,但我坚持以递归方式管理它。

所以基本上转换文本框中的内容:

todo list
  learn js
     hello world
shopping list
  costco
procrastination list

到这里:

[
{'val':'todo list','children':[{'val':'learn js','children':['val':'hello world']}]},
{'val':'shopping list','children':[{'val':'costco'}]},
{'val':'procrastination list'}
]

我想出了这个:

const TxtParser = txtBoxVal => {
  let txtArr = [];
  let nbrSpacesPrev = 0;
  if (txtBoxVal) {
    if (txtBoxVal.split("\n").length) {
      let lines = txtBoxVal.split("\n");
      let numNewLines = txtBoxVal.split("\n").length;
      let i;
      for (i = 0; i < numNewLines; i++) {
        if (lines[i].search(/\S/) !== -1) {
          let txtObj = {};
          txtObj["line"] = lines[i].trim();
          // check for space diff
          txtObj["nbrSpaces"] = lines[i].search(/\S/);
          txtArr.push(txtObj);
        }
      }
    }
  }
  return txtArr;
}; 

我只得到线性结果: https://codesandbox.io/s/text-to-json-parser-kuc28

我不知道如何创建嵌套子级。

【问题讨论】:

  • 空格数是否总是相同的格式?
  • 只要比以前嵌套更多,就像简化版的yaml一样
  • 请检查我的答案,如果您已经可以使用它,请告诉我。否则我也可以将插入函数写到你的对象中。

标签: javascript json reactjs parsing recursion


【解决方案1】:

编辑:添加.filter行来清理输入,并切换到Thankyou的输入数据进行演示。)


这是一种方法。我们转换为如下所示的中间格式:

[
    {indent: 0, val: "todo list"},
    {indent: 2, val: "learn js"},
    {indent: 5, val: "hello world"},
    {indent: 0, val: "shopping list"},
    {indent: 2, val: "costco"},
    {indent: 0, val: "procrastination list"}
]

indents 计算每行文本前的空格。然后,通过保留最近节点的父节点堆栈,搜索第一个缩进值低于当前节点的节点,并将当前节点添加为其子节点之一,我们将该列表折叠成这样的数据结构:

{
    indent: -1,
    children: [
        {
            indent: 0,
            val: "todo list",
            children: [
                {indent: 2, val: "learn js", children: [{indent: 5, val: "hello world"}]}
            ],
        },
        {indent: 0, val: "shopping list" children: [{indent: 2, val: "costco"]},
        {indent: 0, val: "procrastination list"}
    ],
}

最后,我们通过该结构的子级递归删除所有嵌套的 indent 属性,我们得到您的输出。

这段代码就是建立在这个想法之上的:

// Helper function
const deepMap = (fn) => ({children, ...rest}) => ({
  ... fn ({...rest}),
  ... (children ? {children: children .map (deepMap (fn))} : {})
})

// Main function
const extractTree = (text) => 
  text
    .split ('\n')
    .filter ((line) => /\S/ .test (line))
    .map (s => s .match (/^(\s*)(.*)$/) .slice (1))
    .map (([prefix, val]) => ({indent: prefix .length, val}))
    .reduce ((path, node) => {
      const {indent, val} = node
      const parentIdx = path .findIndex (node => node .indent < indent)
      const parent = path [parentIdx]
      parent .children = [... (parent .children || []), node]
      return [node, ... path .slice (parentIdx)]
    }, [{indent: -1, children: []}]) 
    .slice (-1) [0] 
    .children
    .map (deepMap (({indent, ...rest}) => ({...rest})))

// Test data
const text = `  
todo list
  learn js
    hello world
    functions
  
shopping list
  costco
  berries


  mushrooms
procrastination list
`

// Demo
console .log (
  extractTree (text)
)
.as-console-wrapper {min-height: 100% !important; top: 0}

.filter 调用删除所有空行,以及只有空格字符的行。 .split 调用和两个.map 调用将这个经过处理的输入转换为第一个中间格式。我希望它们应该相当清楚。 (如有必要,您可以在此处添加一些tab -&gt; space 转换。)

.reduce 调用更复杂,维护一堆祖先节点,从一个缩进为-1 的默认节点开始,所以它总是低于任何实际值,找到我们当前的直接父节点堆栈上的节点作为第一个缩进低于其自身缩进的节点,然后将当前节点附加为该父节点的子节点,并将其推入堆栈。

之后,我们使用.slice (-1) [0] .children获取栈底元素的孩子,应该是我们关心的节点。

最后,我们.map 处理结果,传递辅助函数deepMap 以递归遍历这些对象中的每一个,并应用我们传递的函数来删除现在不需要的indent 节点。 deepMap 是一个有用的、相当通用的函数,将函数应用于节点,并递归地应用于其每个子节点。


这做了一些我在我的代码中不常做的事情:它会沿途改变数据。我还没有想出任何干净的方法来避免突变。我们不会改变原始输入数据——我们不是野蛮人! -- 但内部节点在reduce 调用期间发生了变异。

如果有人发现没有这种突变的干净方法,我很想听听!

【讨论】:

  • 很好的线性方法,斯科特。我将它放在我的 vscode 中以逐步完成它并了解转换。这是对堆栈的非常巧妙的管理。 path.findIndex 是否有效,因为我们的父节点被预先添加到结果中?我主要关心的是path.findIndex,随着输入的增长,它会变得低效,但我仍然在思考它......我还想知道这种方法是否有更好的方法来处理空行。
  • > "...without mutation" - 我在帖子中展示了一种可能的方法
  • @Thankyou:是的,通过添加节点,我们将数组用作 LIFO 堆栈,而不是弹出或推送值,findIndex 让我们砍掉不相关的头部。我认为findIndex 可能效率低下,因为我们倾向于数十万行,但如果它在较小时出现此类问题,我会感到惊讶。性能方面的更大问题可能是我做了太多 little 突变。每行都有一个新的堆栈数组。这可能会导致大量垃圾收集。如果空行是个问题,我会尽早过滤掉它们。
  • @Thankyou:我认为您可能会提出一个有趣的解决方案。今晚我会试着看看它。
【解决方案2】:

递归方法

你有一个超级有趣的问题!我将在您的输入中添加更多元素,以便我们可以看到兄弟姐妹和后代正确嵌套。我还分散了一些空行以使我们的程序更健壮-

const data = `
todo list
  learn js
    hello world
    functions

shopping list
  costco
  berries


  mushrooms
procrastination list
`

首先,我们将通过删除所有空行以及所有前导和尾随空格来sanitize 数据 -

const sanitize = (str = "") =>
  str.trim().replace(/\n\s*\n/g, "\n")

console.log(sanitize(data))
todo list
  learn js
    hello world
    functions
shopping list
  costco
  berries
  mushrooms
procrastination list

有了一个清晰的起点,我们就可以开始分解问题了...


设计

让我们用空格代替,用行尾代替¬,这样我们就可以看到发生了什么。我们首先在干净的字符串上调用makeChildren -

makeChildren(
  todo•list¬
  ••learn•js¬
  ••••hello•world¬
  ••••functions¬
  shopping•list¬
  ••costco¬
  ••berries¬
  ••mushrooms¬
  procrastination•list
)

makeChildren 创建一个数组并在每个元素上调用make1 -

[ make1(
    todo•list¬
    ••learn•js¬
    ••••hello•world¬
    ••••functions¬
  )
, make1(
    shopping•list¬
    ••costco¬
    ••berries¬
    ••mushrooms¬
  )
, make1(
    procrastination•list
  )
]

make1 创建一个节点并随后在其后代上调用makeChildren -

[ { value: todo•list
  , children: makeChildren(outdent(
      ••learn•js¬
      ••••hello•world¬
      ••••functions¬
    ))
  }
, { value: shopping•list
  , children: makeChildren(outdent(
      ••costco¬
      ••berries¬
      ••mushrooms¬
    ))
  }
, { value: procrastination•list
  , children: makeChildren(outdent(

    ))
  }
]

正如我们已经看到的,makeChildren 创建一个数组并在每个孩子上调用 make1 -

[ { value: todo•list
  , children:
      [ make1(
          learn•js¬
          ••hello•world¬
          ••functions¬
        )
      ]
  }
, { value: shopping•list
  , children:
      [ make1(costco¬)
      , make1(berries¬)
      , make1(mushrooms¬)
      ]
  }
, { value: procrastination•list
  , children:
      []
  }
]

mutually recursive 进程不断地继续...makeChildren 调用 make1,后者调用 makeChildren,后者调用 make1 等等,直到每个分支都满足基本情况。


实施

根据我们的设计,我们将从makeChildren 开始-

const makeChildren = (str = "") =>
  str === ""
    ? []
    : str.split(/\n(?!\s)/).map(make1)

这要求我们实现make1 -

const make1 = (str = "") =>
{ const [ value, children ] = cut(str, "\n")
  return { value, children: makeChildren(outdent(children)) }
}

这要求我们实现 cutoutdent -

  • cutString.prototype.split 类似,但仅在char第一次 出现时拆分str
  • outdent 删除一级缩进
const cut = (str = "", char = "") =>
{ const pos = str.search(char)
  return pos === -1
    ? [ str, "" ]
    : [ str.substr(0, pos), str.substr(pos + 1) ]
}

const outdent = (str = "") =>
{ const spaces = Math.max(0, str.search(/\S/))
  const re = new RegExp(`(^|\n)\\s{${spaces}}`, "g")
  return str.replace(re, "$1")
}

就是这样!最后的result 是-

const result =
  makeChildren(sanitize(data))

console.log(result)
[ { value: "todo list"
  , children:
      [ { value: "learn js"
        , children:
            [ { value: "hello world", children: [] }
            , { value: "functions", children: [] }
            ]
        }
      ]
  }
, { value: "shopping list"
  , children:
      [ { value: "costco", children: [] }
      , { value: "berries", children: [] }
      , { value: "mushrooms", children: [] }
      ]
  }
, { value: "procrastination list", children: [] }
]

在你自己的浏览器中运行下面的sn-p来验证结果-

const sanitize = (str = "") =>
  str.trim().replace(/\n\s*\n/g, "\n")

const cut = (str = "", char = "") =>
{ const pos = str.search(char)
  return pos === -1
    ? [ str, "" ]
    : [ str.substr(0, pos), str.substr(pos + 1) ]
}

const outdent = (str = "") =>
{ const spaces = Math.max(0, str.search(/\S/))
  const re = new RegExp(`(^|\n)\\s{${spaces}}`, "g")
  return str.replace(re, "$1")
}

const makeChildren = (str) =>
  str === ""
    ? []
    : str.split(/\n(?!\s)/).map(make1)

const make1 = (str = "") =>
{ const [ value, children ] = cut(str, "\n")
  return { value, children: makeChildren(outdent(children)) }
}

const data = `
todo list
  learn js
    hello world
    functions

shopping list
  costco
  berries


  mushrooms
procrastination list
`

const result =
  makeChildren(sanitize(data))

console.log(JSON.stringify(result, null, 2))
// [ { value: "todo list"
//   , children:
//       [ { value: "learn js"
//         , children:
//             [ { value: "hello world", children: [] }
//             , { value: "functions", children: [] }
//             ]
//         }
//       ]
//   }
// , { value: "shopping list"
//   , children:
//       [ { value: "costco", children: [] }
//       , { value: "berries", children: [] }
//       , { value: "mushrooms", children: [] }
//       ]
//   }
// , { value: "procrastination list", children: [] }
// ]

对我来说很有意义!

一个程序“简单直接”是因为它对我有意义吗?如果我们可以使用 objective 品质来做出这样的断言会怎样? @tokafew420 对他们的计划很有信心,所以我提供了这个客观的分析。

我在 each 程序中将变量名称更改为 _n,以便我们可以轻松识别和计算各个移动部件 -

const TxtParser = _1 => { // 10 total variables; 4 mutations; 5 variable reassignments
  let _2 = []; // <-- mutates below but never reassigned; should be const
  let _3 = []; // <-- mutates below but never reassigned; should be const
  let _4 = {   // <-- reassigned below
    nbrSpaces: -1,
    children: _2 // <-- mutates below
  };
  let _5; // <-- reassigned below
  if (_1) {
    let _6 = _1.split("\n"); // <-- reassignment #1
    let _7 = _6.length;      // <-- reassignment #2
    if (_7) {
      let i;                 // <-- mutates; leaks variable out of `for` scope
      for (i = 0; i < _7; i++) {     // <-- mutation #1
        let _8 = _6[i].trim();       // <-- never reassigned, does not mutate; should be const
        let _9 = _6[i].search(/\S/); // <-- never reassigned, does not mutate; should be const
        if (_8) {
          let _10 = {                // <-- never reassigned, does not mutate; should be const
            line: _8,
            nbrSpaces: _9,
            children: []
          };
          if (_5 && _9 > _5.nbrSpaces) {
            _3.push(_4);         // <-- mutation #2
            _4 = _5;             // <-- reassignment #3
          } else {
            while (_9 <= _4.nbrSpaces) {
              _4 = _3.pop();     // <-- reassignment #4 AND mutation #3
            }
          }
          _4.children.push(_10); // <-- mutation #4
          _5 = _10;              // <-- reassignment #5
        }
      }
    }
  }
  return _2;
};
  • 单个范围内的最高变量计数:10
  • 突变:4
  • 变量重新分配:5
  • 实施行数:36
  • 可重用函数:0
  • 需要额外的转换才能产生预期的结果:是的

将其与声明式函数方法进行比较 -

const sanitize = (_1 = "") => // 1 total variable; never mutates; never reassigned
  _1.trim().replace(/\n\s*\n/g, "\n")

const cut = (_1 = "", _2 = "") => // 3 total variables; never mutates; never reassigned
{ const _3 = _1.search(_2)
  return _3 === -1
    ? [ _1, "" ]
    : [ _1.substr(0, _3), _1.substr(_3 + 1) ]
}

const outdent = (_1 = "") => // 3 total variables; never mutates; never reassigned
{ const _2 = Math.max(0, _1.search(/\S/))
  const _3 = new RegExp(`(^|\n)\\s{${_2}}`, "g")
  return _1.replace(_3, "$1")
}

const makeChildren = (_1) => // 1 total variable; never mutates; never reassigned
  _1 === ""
    ? []
    : _1.split(/\n(?!\s)/).map(make1)

const make1 = (_1 = "") =>  // 3 total variables; never mutates; never reassigned
{ const [ _2, _3 ] = cut(_1, "\n")
  return { value: _2, children: makeChildren(outdent(_3)) }
}
  • 单个范围内的最高变量计数:3
  • 突变:0
  • 变量重新分配:0
  • 实施行数:13
  • 可重用函数:3 个(sanitizecutoutdent
  • 需要额外的转换才能产生所需的结果:否

为什么这些事情很重要?

当单个作用域中有 10 个变量,并且它们都可以随时更改和重新分配时,我们的大脑非常很难跟踪所有移动的部分。这个程序很大,很难写。即使我们对一个输入得到了正确的结果,我们怎么知道我们的程序对其他输入是正确的呢?需要编写更多测试以确保正确的行为,并且由于它是 36 行的特定行为,它不能在程序的其他部分中重用。

当您将其与函数式程序的低复杂性进行比较时,我们的函数更小,目的明确,易于编写、测试和维护,并可在程序的其他部分重用。如您所见,将变量重命名为 _1_2_3 几乎不会影响可读性,因为我们的大脑很容易同时跟踪 3 件事,当我们知道这 3 件事不是时更容易变异或重新分配。

命令式程序的Y线上x的值是多少?由于 for-while 嵌套循环中的所有突变和重新分配,这是任何人的猜测。除非您的大脑被计算机取代,否则对于该程序中几乎所有变量和所有行,这个问题很难回答,所以我认为它绝非简单或直截了当。

另一方面,很容易回答有关函数式程序的这些问题。我们可以立即知道任何行上任何变量的值,而无需引用无数其他变量或产生难以管理的概念开销。如果这不简单或直接,我不知道是什么......

/2美分

【讨论】:

  • 也感谢您的解释,删除空子结果将是完美的
  • 虽然我最初是从这条线开始的,但我没有想出关键的outdent。这使它成为另一个优雅的解决方案!
  • @Ardhi,我建议你保持节点统一,这意味着每个节点都有一个 children 数组属性。这样做的下游好处很大,因为所有节点消费者都可以依赖已知可靠的数据集。也就是说,这是你的程序,所以你可以改变你认为合适的方式。 return { value, children: makeChildren(outdent(children)) } 可以更新为 const newChildren = makeChildren(outdent(children)); return newChildren.length ? { value, children: newChildren } : { value };
【解决方案3】:

IMO,最简单的方法是直接执行 for 循环,就像 OP 最初拥有的那样。我们只需要使用堆栈并维护一些引用即可实现目标。

优点:O(n) 性能和代码可读性。

这是我的尝试(详情在 cmets 中):

let input = `item.1
    item.1.1
        item.1.1.1
item.2
    item.2.1
item.3
item.4
    item.4.1
        item.4.1.1
        item.4.1.2
        item.4.1.3
        
    item.4.2
        item.4.2.1
            item.4.2.1.1
                item.4.2.1.1.1

                item.4.2.1.1.2
                item.4.2.1.1.3
            item.4.2.1.2
                item.4.2.1.2.1
            item.4.2.1.3
        item.4.2.2`;

// Updated to use 2 stacks (for spaces and parent items) so that we don't include
// the spaces count in the final result.
const parseList = list => {
    const final = []; // The final result
    const parents = []; // A stack to maintain parent references.
    const spaces = []; // A stack to track parent spaces.
    let parentItem = {
        children: final // Use final reference so initial parent is a proxy to the final result
    };
    let parentSpaces = -1; // Initial space starting at -1 (which can never occur)
    let prevItem;   // The previous list item
    let prevSpaces; // The previous item's spaces

    const lines = String(list).split("\n");
    const lineCount = lines.length;

    for (let i = 0; i < lineCount; i++) {
        const line = lines[i].trim();
        const currSpaces = lines[i].search(/\S/);

        // Ignore empty lines
        if (line) {
            // Here's the magic!!
            // If the current spaces are more than the previous spaces, then this item should be a child
            // of the previous item. Also account for prevSpaces === -1 for initial iteration
            if (prevSpaces !== -1 && currSpaces > prevSpaces) {
                // Set new parent
                parents.push(parentItem);
                spaces.push(parentSpaces);
                parentItem = prevItem;
                parentSpaces = prevSpaces;
            } else {
                // If item is not a child then pop() the parents until the parent's spaces are less
                // than the current item's spaces
                while (currSpaces <= parentSpaces) {
                    parentItem = parents.pop();
                    parentSpaces = spaces.pop();
                }
            }

            // Create new list item
            const item = {
                val: line
            };

            // Add child
            parentItem.children = parentItem.children || []; // This is so the children property isn't created it no child
            parentItem.children.push(item);

            prevSpaces = currSpaces;
            prevItem = item;
        }
    }

    return final;
};

console.log(JSON.stringify(parseList(input), null, 2));

回应@Thankyou的挑战

让我们比较一下这两个函数:

const max0 = (x, y, z) => x > y ? x > z ? x : z : y > z ? y : z;
// variable: 3
// mutations: 0
// variable reassignments: 0
// lines of implementation: 1

const max1 = (x, y, z) => {
  let max = x;
  if (y > max) max = y;
  if (z > max) max = z;
  return max;
}
// variable: 4
// mutations: 0
// variable reassignments: 2
// lines of implementation: 5

根据这些指标,max0 会被认为更简单,但如果你随机询问 100 位开发人员,情况会是这样吗?这些指标虽然是客观的,但并不能说明全部情况(即:函数调用、语言特性、依赖关系等……),也不能定义一个人对简单性的主观概念。

我可以内联一些东西并使用母语功能来减少这些指标吗?当然!但我没有。我承认,经验丰富的开发人员可能会更喜欢你的答案,因为它的优雅、狡猾和“简单”。但是我的粗略实现还有其他好处,并展示了另一种在使用基本构造时可以完成的方式。即使是像我这样的菜鸟(希望其他人比你年长)也能轻松上手。大学在递归之前教授 for 循环是有原因的。但这只是我的看法。

【讨论】:

  • 很好,但请注意这与请求的输出格式之间的差异。对于 OP 来说这可能不是问题,但是您将如何解决这些问题?最简单的是linevalue 的属性名称。那是一条线的变化。但还有两个。每个对象都有一个children 属性,即使它只是一个空数组。删除这些容易吗?而且每个人都有一个无关的nbrSpaces 属性。如果它们是不需要的,您将如何删除它们?
  • 我要补充一点,这不是O(n),你有一个for 包裹着一个while
  • 我在对my answer 的编辑中质疑了您的“最简单、直接” 声明
  • 哇,我的第一次争吵。我终于成功了!!这是一场有趣的辩论,你的挑战会被注意到。但是,我不认为我们刚刚遇到过你的愤怒:(我编辑了我的答案,以回应你的挑战,希望我们可以把它留在那个地方。最后我觉得你的回答,我的回答,而你的挑战将使这个社区和那些寻求不仅仅是复制/粘贴的人受益,所以@Thankyou
  • @tokafew420 我从没想过争吵。感谢您深思熟虑的评论,我同意我认为整个社区都将从讨论中受益。
【解决方案4】:

这是一个部分答案,但它可能会帮助您构建您的对象:

var text = `todo list
  learn js
    hello world
  other level
   deeper
shopping list
  costco
procrastination list`;

var paths = []; // this will store our path tree

parseText = () => {
    var lines = text.split("\n");
    var outputObject = {};

    var pathsStack = [];
    var previousSpaces = 0;

    lines.forEach( line => {
      //var line = lines[key];
      var spaces = line.match(/^\s*/)[0].length; // search 

      if (spaces === 0){ // reset path stack
        pathsStack = [line.trim()];
      } else if (spaces > previousSpaces) {
        pathsStack.push(line.trim());
      } else if (spaces === previousSpaces) {
        pathsStack.pop(); // remove last item
        pathsStack.push(line.trim());
      } else if (spaces < previousSpaces) {
        pathsStack.pop(); // remove last two items
        pathsStack.pop(); 
        pathsStack.push(line.trim());
      } 

      previousSpaces = spaces;

      paths.push(pathsStack.join("."));
    });
    console.log(paths);
    /* 
    this will output an array in the following form:
      0: "todo list"
      1: "todo list.learn js"
      2: "todo list.learn js.hello world"
      3: "todo list.other level"
      4: "todo list.other level.deeper"
      5: "shopping list"
      6: "shopping list.costco"
      7: "procrastination list"

    you can now iterate through this array and insert it into your target object.
    */
  }

【讨论】:

    猜你喜欢
    • 2021-03-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-17
    • 1970-01-01
    • 2021-12-10
    相关资源
    最近更新 更多