2017-10-06 137 views
-5

我有结构性这样如何从C#中的字符串解析嵌套结构?

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解析这样的对象树?

+1

使用递归算法。 – jdweng

+10

第二个想法写一个解析器......为什么要用这个?使用JSON或YAML。解析器已经写入并可用 –

+0

这里的语法不仅仅是'id NAME {CONTENT}'。您需要提供如何解析此数据的完整详细信息。 – Enigmativity

回答

3

不是最高效的方式来解决这个问题,也没有清洁剂,也不是最全面的...但也许更短。

这使用正则表达式堆栈(我认为只能用.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(); 
} 

其输出

output

(NB:所述.Dump()是,仅仅转储所生成的对象的Linqpad扩展)

-3

它看起来就像你正在试图分析包含C#兼容的指令串。根据您打算如何处理这些信息做,它可能是有意义的使用Roslyn compiler for static analysis

你可以得到一个语法树是这样的...如何操作树

using Microsoft.CodeAnalysis; 
using Microsoft.CodeAnalysis.CSharp; 
using Microsoft.CodeAnalysis.CSharp.Syntax; 

SyntaxTree tree = CSharpSyntaxTree.ParseText(
@"using System; 
using System.Collections.Generic; 

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

var root = (CompilationUnitSyntax)tree.GetRoot(); 

说明在GitHub Wiki中提供。

https://github.com/dotnet/roslyn/wiki/Getting-Started-C%23-Syntax-Analysis

1

你有几个选择:

  1. 学习写,写,对于文档类型的分析器。有很多像HackerRank这样的在线资源可以帮助你做到这一点。如果你想采取这条路线,我会建议看一下弦乐部分,因为他们中的很多人都可以帮助教授这种操作。虽然这是迄今为止最困难的道路,但您学习它会更好。

  2. 使用库。 C#中有许多不同的解析器库,每个解析器库都有自己的优点和缺点。我此刻的个人最喜欢的是SuperPower,这似乎是最简单的一元解析库只选择和使用,而不必知道了一大堆关于函数式编程或一般的解析。

如果我更了解情况,我可以提供更好的指导,但是您提供的信息非常薄。这将是有用的信息:

  • 大小/速度要求。
  • 文档结构的更精确的定义。
2

这似乎相似,如果Paranthesis是平衡检查的另一个教科书问题。基本上是一个堆栈,解析字符串,当遇到开头的括号时......理解一个对象正在被创建。并且关闭支架有助于将对象标记为完整。

3

@ T.S。在评论中指出,除了教育目的,很少有重新发明轮子的好理由。当谈到写作解析器,正是这些极端案例,这使得它很难得到正确:格式

  • 验证,更具体的返回友好 消息给用户
  • 优化代码的性能,分配,并发性,...

这就是说,在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; 
      } 
     } 
    } 
} 

我为了离队多串一气呵成的id情况下使用的Queue

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

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