【问题标题】:How to parse nested structures from string in C#?如何从 C# 中的字符串解析嵌套结构?
【发布时间】:2017-10-06 23:21:30
【问题描述】:

我有一个这样结构的字符串

id i { 
    any data; any [con=tent] 
    id j { 
         any inner data 
    }
    id k {
         bla 
         id m {
             any
         }
    thing }
}

id NAME { CONTENT } 是可嵌套对象的模式。所需的目标如下所示:

public class Node {
    public List<Node> InnerNodes;
    public string Contents; 
    public string Name; 
}

如何使用 C#.Net 使用开源包解析此类对象树?

【问题讨论】:

  • 使用递归算法。
  • 写一个解析器....转念一想,你为什么用这个?使用 JSON 或 YAML。并且它们的解析器已经编写并可用
  • 这里的语法不仅仅是id NAME { CONTENT }。您需要提供有关如何解析此数据的完整详细信息。
  • @DuckQueen - 我可以清楚地看到有一个id 的地方有一个NAME{,然后是CONTENT,然后是一个},但是CONTENT包含其他ids 和还有其他内容,例如"any data; any [con=tent]""bla""thing"。您的课程显示您想要一个嵌套结构,但您的语法没有解释如何解析 other stuff 以提取嵌套节点。您需要提供正确的语法。
  • @DuckQueen - “只有原始嵌套结构” - 这就是问题所在。它不是“原始的”——它包含一些 id NAME { CONTENT } 和其他一些东西。这都需要解析。你不能只解析一些而不解析其余的。

标签: c# .net string parsing tree


【解决方案1】:

不是解决这个问题的最有效的方法,也不是更干净,也不是最全面的......但可能更短。

这使用正则表达式堆栈(我认为仅适用于 .Net 正则表达式引擎)。 A good tutorial about those, applied to nested constructions matching here

void Main()
{
    var input = @"id i { 
        any data; any [con=tent] 
        id j { 
            any inner data 
        }
        id k {
            bla 
            id m {
                any
            }
        thing }
    }";
    Process(input).Dump();
}

public class Node {
    public List<Node> InnerNodes;
    public string Contents; 
    public string Name; 
}

Regex reg = new Regex(@"
    id\s+(?<name>\w+)\s*\{(?<body>(?<DEPTH>)
        (?>(?<DEPTH>)\{ | \}(?<-DEPTH>) | (?(DEPTH)[^\{\}]* | ) )*
        )\}(?<-DEPTH>)(?(DEPTH)(?!))
    ", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);

List<Node> Process(string body){
    return reg.Matches(body)
        .Cast<Match>()
        .Select(x=>new Node{
            Name=x.Groups["name"].Value,
            Contents=x.Groups["body"].Value.Trim(),
            InnerNodes=Process(x.Groups["body"].Value),
        }).ToList();
}

哪些输出

(nb:.Dump() 是 Linqpad 扩展,它只是转储生成的对象)

【讨论】:

    【解决方案2】:

    @T.S.在 cmets 中指出,除了教育目的之外,很少有理由重新发明轮子。在编写解析器时,正是那些极端情况让我们很难做到正确:

    • 格式验证,更具体地说是返回友好 给用户的消息
    • 优化代码的性能、分配、并发性……

    也就是说,有了 C# 7 对模式匹配的支持,基本的管道应该很容易实现。

    第一站,Node 类,添加初始化。

    public class Node
    {
        public Node(string name)
        {
            Name = name;
            InnerNodes = new List<Node>();
        }
        public string Name { get; }
        public string Contents { get; set; }
        public List<Node> InnerNodes { get; }
    }
    

    接下来,为我们语法的不同部分创建包装器。

    internal abstract class Token
    {
    }
    
    internal class OpenNodeToken : Token
    {
        public OpenNodeToken(string name) { Name = name; }
        public string Name { get; }
    }
    
    internal class CloseNodeToken : Token
    {
    }
    
    internal class ContentToken : Token
    {
        public ContentToken(string text) { Text = text; }
        public string Text { get; }
    }
    

    还有一个帮助类,用于将我们的输入字符串转换为标记序列。

    internal static class Tokenizer
    {
        public static IEnumerable<Token> Scan (string expression)
        {
            var words = new Queue<string>
                (expression.Split(new[] { ' ', '\n', '\r', '\t' }, StringSplitOptions.RemoveEmptyEntries));
    
            while (words.Any())
            {
                string word = words.Dequeue();
                switch (word)
                {
                    case "id":
                        yield return new OpenNodeToken(words.Dequeue());
                        words.Dequeue();
                        break;
    
                    case "}":
                        yield return new CloseNodeToken();
                        break;
    
                    default:
                        yield return new ContentToken(word);
                        break;
                }
            }
        }
    }
    

    我使用了Queue 以便一次性将多个字符串出列以处理id 的情况。

    最后是解析器,其中模式匹配允许简洁的代码。

    public static class Parser
    {
        private static Node currNode;
        private static Stack<Node> prevNodes;
        private static IEnumerable<Token> tokens;
    
        static Parser()
        {
            prevNodes = new Stack<Node>();
        }
    
        public static Node Deserialize(string input)
        {
            tokens = Tokenizer.Scan(input);
            if (!(tokens.FirstOrDefault() is OpenNodeToken rootToken)) 
                throw new FormatException("Missing root node");
            currNode = new Node(rootToken.Name);
    
            foreach(Token token in tokens.Skip(1))
            {
        switch (token)          
                {
                    case ContentToken c:
                        string s = string.IsNullOrEmpty(currNode.Contents) ? c.Text : " " + c.Text;
                        currNode.Contents += s;
                        break;
    
                    case OpenNodeToken n:
                        prevNodes.Push(currNode);
                        currNode = new Node(n.Name);
                        break;
    
                    case CloseNodeToken c:
                        if (prevNodes.Any())
                        {
                            Node childNode = currNode; 
                            currNode = prevNodes.Pop();
                            currNode.InnerNodes.Add(childNode);
                        }
                        break;
    
                    default: throw new NotImplementedException(token.GetType().Name);
                }
            }
            return currNode;
        }
    }
    

    在这里,我们使用Stack 将父节点推送到解析子节点之前。一旦满足子元素的右括号,我们弹出堆栈的父元素并将子元素添加到它的集合中。

    如前所述,一个体面的解析器也应该涵盖极端情况,我将实现这些的乐趣留给了读者。为了测试解析器,我在下面设置了控制台项目。

    namespace MySimpleParser
    {
        class Program
        {
            public static void Main(string[] args)
            {
                string s = GetInput();
                try
                {
                    Node root = Parser.Deserialize(s);
                    PrintBranch(root, 1);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
                }
    
                Console.ReadLine();
            }
    
            internal static string GetInput()
            {
                return @"id i { 
                            any data; any [con=tent]
                            id j {
                                any inner data
                            }
                            id k {
                                bla
                                id m {
                                    any
                                }
                                thing 
                            }
                        }";
            }
    
            internal static void PrintNode(Node n, int depth)
            {
                string indent = new string('-', 3 * depth);
                Console.WriteLine($"{indent} Name: {n.Name}");
                Console.WriteLine($"{indent} Contents: {n.Contents}");
                Console.WriteLine($"{indent} Child Nodes: {n.InnerNodes.Count}");
            }
    
            internal static void PrintBranch(Node root, int depth)
            {
                PrintNode(root, depth);
                foreach (Node child in root.InnerNodes) PrintBranch(child, depth + 1);
            }
        }
    }
    

    输出

    --- Name: i
    --- Contents: any data; any [con=tent]
    --- Child Nodes: 2
    ------ Name: j
    ------ Contents: any inner data
    ------ Child Nodes: 0
    ------ Name: k
    ------ Contents: bla thing
    ------ Child Nodes: 1
    --------- Name: m
    --------- Contents: any
    --------- Child Nodes: 0
    

    【讨论】:

      【解决方案3】:

      这似乎类似于检查 Paranthesis 是否平衡的另一个教科书问题。基本上取一个堆栈,解析字符串,当你遇到左括号时......了解正在创建一个对象。并且右括号有助于将对象标记为完整。

      【讨论】:

        【解决方案4】:

        你有几个选择:

        1. 学习编写和编写文档类型的解析器。有很多在线资源,例如 HackerRank,可以帮助您做到这一点。如果您想走这条路,我建议您查看字符串部分,因为其中许多有助于教授这种操作。虽然这是迄今为止最困难的道路,但你会因为学会它而变得更好。

        2. 使用库。 C# 有许多不同的解析器库,每个都有自己的优缺点。目前我个人最喜欢的是SuperPower,它似乎是最简单的一元解析器库,无需了解函数式编程或一般解析的大量知识即可上手使用。

        如果我对这种情况多了解一点,我就能提供更好的指导,但你提供的信息太少了。有用的信息:

        • 尺寸/速度要求。
        • 文档结构的更准确定义。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-23
          • 2021-12-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-01-16
          相关资源
          最近更新 更多