【问题标题】:Parsing Lisp S-Expressions with known schema in C#在 C# 中使用已知模式解析 Lisp S-Expressions
【发布时间】:2010-06-16 07:02:45
【问题描述】:

我正在使用一种服务,该服务以类似 Lisp 的 S-Expression 字符串的形式提供数据。这些数据到达的速度又快又厚,我想尽可能快地翻阅它,最好是直接在字节流上(它只是单字节字符),没有任何回溯。这些字符串可能很长,我不希望为整个消息分配字符串的 GC 搅动。

我当前的实现使用带有语法的 CoCo/R,但它有一些问题。由于回溯,它将整个流分配给一个字符串。如果必须更改我的代码的用户,这也有点繁琐。我宁愿有一个纯 C# 解决方案。 CoCo/R 也不允许重用解析器/扫描器对象,所以我必须为每条消息重新创建它们。

从概念上讲,数据流可以被认为是一系列 S-Expressions:

(item 1 apple)(item 2 banana)(item 3 chainsaw)

解析这个序列将创建三个对象。每个对象的类型可以由列表中的第一个值来确定,在上面的例子中是“item”。传入流的架构/语法是众所周知的。

在我开始编码之前,我想知道是否有库已经这样做了。我确定我不是第一个遇到这个问题的人。


编辑

这里有一些关于我想要什么的详细信息,因为我认为最初的问题可能有点含糊。

给定一些 SExpressions,例如:

(Hear 12.3 HelloWorld)
(HJ LAJ1 -0.42)
(FRP lf (pos 2.3 1.7 0.4))

我想要一个与此等效的对象列表:

{
    new HearPerceptorState(12.3, "HelloWorld"),
    new HingeJointState("LAJ1", -0.42),
    new ForceResistancePerceptorState("lf", new Polar(2.3, 1.7, 0.4))
}

我正在处理的实际数据集是list of perceptors from a robot model in the RoboCup 3D simulated soccer league。我可能还需要反序列化another set of related data with a more complex structure

【问题讨论】:

    标签: c# .net parsing s-expression


    【解决方案1】:

    在我看来,解析生成器不需要解析仅由列表、数字和符号组成的简单 S 表达式。一个手写的递归下降解析器可能更简单,至少同样快。一般模式如下所示(在 java 中,c# 应该非常相似):

    Object readDatum(PushbackReader in) {
        int ch = in.read();
        return readDatum(in, ch);
    }
    Object readDatum(PushbackReader in, int ch) {
        if (ch == '(')) {
            return readList(in, ch);
        } else if (isNumber(ch)) {
            return readNumber(in, ch);
        } else if (isSymbolStart(ch)) {
            return readSymbol(in, ch);
        } else {
            error(ch);
        }
    }
    List readList(PushbackReader in, int lookAhead) {
        if (ch != '(') {
            error(ch);
        }
        List result = new List();
        while (true) {
            int ch = in.read();
            if (ch == ')') {
                break;
            } else if (isWhiteSpace(ch)) {
                skipWhiteSpace(in);
            } else {
                result.append(readDatum(in, ch);
            }
        }
        return result;
    }
    String readSymbol(PushbackReader in, int ch) {
        StringBuilder result = new StringBuilder();
        result.append((char)ch);
        while (true) {
           int ch2 = in.read();
           if (isSymbol(ch2)) {
               result.append((char)ch2);
           } else if (isWhiteSpace(ch2) || ch2 == ')') {
               in.unread(ch2);
               break;
           } else if (ch2 == -1) {
               break;
           } else {
               error(ch2);
           }
        }
        return result.toString();
    }
    

    【讨论】:

    • 是的,我同意这既简单又快速,但最后我有一个字符串和数字树。我真正想要的是我自己的对象类型的一维列表。我有一个可能的 SExpressions 架构,我将看到它们应该映射到对象类型以进行反序列化。我希望有一种技术可以通过某种方式指定此映射,然后输入字符流并吸出不同类型的相应对象。
    • 我在原始问题中添加了一个示例。
    【解决方案2】:

    我使用OMeta# 在 C# 中编写了一个 S-Expression 解析器。它可以解析您在示例中给出的那种 S-Expressions,您只需将十进制数字添加到解析器。

    代码在 github 上以SExpression.NET 的形式提供,相关文章在here 提供。作为替代方案,我建议查看同样使用 OMeta# 编写的用于 .NET 的 YaYAML YAML 解析器。

    【讨论】:

      【解决方案3】:

      考虑使用Ragel。它是一个状态机编译器,可以生成相当快的代码。

      从主页可能看不出来,但 Ragel 确实支持 C#。 Here's 一个简单的例子,说明如何在 C# 中使用它

      【讨论】:

        【解决方案4】:

        查看gplexgppg

        或者,您可以简单地将 S 表达式转换为 XML,然后让 .NET 完成剩下的工作。

        【讨论】:

          【解决方案5】:

          Drew,也许你应该在问题中添加一些上下文,否则这个答案对其他用户来说毫无意义,但试试这个:

          CHARACTERS
          
              letter = 'A'..'Z' + 'a'..'z' .
              digit = "0123456789" .
              messageChar = '\u0020'..'\u007e' - ' ' - '(' - ')'  .
          
          TOKENS
          
              double = ['-'] digit { digit } [ '.' digit { digit } ] .
              ident = letter { letter | digit | '_' } .
              message = messageChar { messageChar } CONTEXT (")") .
          

          哦,我必须指出 '\u0020' 是 unicode SPACE,随后您将使用“- ' '”将其删除。哦,如果您不需要多个字符的前瞻,您可以使用CONTEXT (')')

          FWIW:CONTEXT 不使用封闭的序列,您仍必须在生产中使用它。

          编辑:

          好的,这似乎可行。真的,这次我是认真的:)

          CHARACTERS
              letter = 'A'..'Z' + 'a'..'z' .
              digit = "0123456789" .
          //    messageChar = '\u0020'..'\u007e' - ' ' - '(' - ')'  .
          
          TOKENS
          
              double = ['-'] digit { digit } [ '.' digit { digit } ] .
              ident = letter { letter | digit | '_' } .
          //    message = letter { messageChar } CONTEXT (')') .
          
          // MessageText<out string m> = message               (. m = t.val; .)
          // .
          
          HearExpr<out HeardMessage message> = (. TimeSpan time; Angle direction = Angle.NaN; string messageText; .)
              "(hear" 
                  TimeSpan<out time>
                  ( "self" | AngleInDegrees<out direction> )
          // MessageText<out messageText>    // REMOVED    
          { ANY } (. messageText = t.val; .) // MOD
              ')' (. message = new HeardMessage(time, direction, new Message(messageText)); .)
              .
          

          【讨论】:

          • 套用 Knuth 的话说:“我只证明它是正确的,没有测试它。”
          • 顺便说一句:如果您有无法使用 CONTEXT 解析的令牌,那么您可以省略右侧并在代码中处理它。
          • Pat Terry 对 CoCo/R 进行了一些修改,包括使用友好的“用户名”作为令牌的能力。如果您想手动滚动扫描仪的某些部分,这很方便。
          • @Andre,感谢您提供详细的回答。我忽略了问题的上下文,因为我真的想直接对字符流进行操作,不需要将其全部加载到内存中来解析它。然而,这可能是 CoCo/R 的人为限制。这个答案实际上更适合我的另一个问题!我会试试你的建议。它仍然没有强制限制字符范围(我认为ANY 就是它所说的),但对于这种情况,它是一个足够的解决方法。
          • 我在这个问题中希望的是一种比使用语法文件更简单的解析 SExpressions 的方法。鉴于 SExpressions 在结构上是如此规则,并且在我的情况下,我也有一个在此之上定义的模式,我希望在野外可能有一个很好的解决方案。
          【解决方案6】:

          这是一个相对简单(并且希望易于扩展)的解决方案:

          public delegate object Acceptor(Token token, string match);
          
          public class Symbol
          {
              public Symbol(string id) { Id = id ?? Guid.NewGuid().ToString("P"); }
              public override string ToString() => Id;
              public string Id { get; private set; }
          }
          
          public class Token : Symbol
          {
              internal Token(string id) : base(id) { }
              public Token(string pattern, Acceptor acceptor) : base(pattern) { Regex = new Regex(string.Format("^({0})", !string.IsNullOrEmpty(Pattern = pattern) ? Pattern : ".*"), RegexOptions.Compiled); ValueOf = acceptor; }
              public string Pattern { get; private set; }
              public Regex Regex { get; private set; }
              public Acceptor ValueOf { get; private set; }
          }
          
          public class SExpressionSyntax
          {
              private readonly Token Space = Token("\\s+", Echo);
              private readonly Token Open = Token("\\(", Echo);
              private readonly Token Close = Token("\\)", Echo);
              private readonly Token Quote = Token("\\'", Echo);
              private Token comment;
          
              private static Exception Error(string message, params object[] arguments) => new Exception(string.Format(message, arguments));
          
              private static object Echo(Token token, string match) => new Token(token.Id);
          
              private static object Quoting(Token token, string match) => NewSymbol(token, match);
          
              private Tuple<Token, string, object> Read(ref string input)
              {
                  if (!string.IsNullOrEmpty(input))
                  {
                      var found = null as Match;
                      var sofar = input;
                      var tuple = Lexicon.FirstOrDefault(current => (found = current.Item2.Regex.Match(sofar)).Success && (found.Length > 0));
                      var token = tuple != null ? tuple.Item2 : null;
                      var match = token != null ? found.Value : null;
                      input = match != null ? input.Substring(match.Length) : input;
                      return token != null ? Tuple.Create(token, match, token.ValueOf(token, match)) : null;
                  }
                  return null;
              }
          
              private Tuple<Token, string, object> Next(ref string input)
              {
                  Tuple<Token, string, object> read;
                  while (((read = Read(ref input)) != null) && ((read.Item1 == Comment) || (read.Item1 == Space))) ;
                  return read;
              }
          
              public object Parse(ref string input, Tuple<Token, string, object> next)
              {
                  var value = null as object;
                  if (next != null)
                  {
                      var token = next.Item1;
                      if (token == Open)
                      {
                          var list = new List<object>();
                          while (((next = Next(ref input)) != null) && (next.Item1 != Close))
                          {
                              list.Add(Parse(ref input, next));
                          }
                          if (next == null)
                          {
                              throw Error("unexpected EOF");
                          }
                          value = list.ToArray();
                      }
                      else if (token == Quote)
                      {
                          var quote = next.Item3;
                          next = Next(ref input);
                          value = new[] { quote, Parse(ref input, next) };
                      }
                      else
                      {
                          value = next.Item3;
                      }
                  }
                  else
                  {
                      throw Error("unexpected EOF");
                  }
                  return value;
              }
          
              protected Token TokenOf(Acceptor acceptor)
              {
                  var found = Lexicon.FirstOrDefault(pair => pair.Item2.ValueOf == acceptor);
                  var token = found != null ? found.Item2 : null;
                  if ((token == null) && (acceptor != Commenting))
                  {
                      throw Error("missing required token definition: {0}", acceptor.Method.Name);
                  }
                  return token;
              }
          
              protected IList<Tuple<string, Token>> Lexicon { get; private set; }
          
              protected Token Comment { get { return comment = comment ?? TokenOf(Commenting); } }
          
              public static Token Token(string pattern, Acceptor acceptor) => new Token(pattern, acceptor);
          
              public static object Commenting(Token token, string match) => Echo(token, match);
          
              public static object NewSymbol(Token token, string match) => new Symbol(match);
          
              public static Symbol Symbol(object value) => value as Symbol;
          
              public static string Moniker(object value) => Symbol(value) != null ? Symbol(value).Id : null;
          
              public static string ToString(object value)
              {
                  return
                      value is object[] ?
                      (
                          ((object[])value).Length > 0 ?
                          ((object[])value).Aggregate(new StringBuilder("("), (result, obj) => result.AppendFormat(" {0}", ToString(obj))).Append(" )").ToString()
                          :
                          "( )"
                      )
                      :
                      (value != null ? (value is string ? string.Concat('"', (string)value, '"') : (value is bool ? value.ToString().ToLower() : value.ToString())).Replace("\\\r\n", "\r\n").Replace("\\\n", "\n").Replace("\\t", "\t").Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\\"", "\"") : null) ?? "(null)";
              }
          
              public SExpressionSyntax()
              {
                  Lexicon = new List<Tuple<string, Token>>();
                  Include(Space, Open, Close, Quote);
              }
          
              public SExpressionSyntax Include(params Token[] tokens)
              {
                  foreach (var token in tokens)
                  {
                      Lexicon.Add(new Tuple<string, Token>(token.Id, token));
                  }
                  return this;
              }
          
              public object Parse(string input)
              {
                  var next = Next(ref input);
                  var value = Parse(ref input, next);
                  if ((next = Next(ref input)) != null)
                  {
                      throw Error("unexpected ", next.Item1);
                  }
                  return value;
              }
          }
          
          public class CustomSExpressionSyntax : SExpressionSyntax
          {
              public CustomSExpressionSyntax()
                  : base()
              {
                  Include
                  (
                      // "//" comments
                      Token("\\/\\/.*", SExpressionSyntax.Commenting),
          
                      // Obvious
                      Token("false", (token, match) => false),
                      Token("true", (token, match) => true),
                      Token("null", (token, match) => null),
                      Token("\\-?[0-9]+\\.[0-9]+", (token, match) => double.Parse(match)),
                      Token("\\-?[0-9]+", (token, match) => int.Parse(match)),
          
                      // String literals
                      Token("\\\"(\\\\\\n|\\\\t|\\\\n|\\\\r|\\\\\\\"|[^\\\"])*\\\"", (token, match) => match.Substring(1, match.Length - 2)),
          
                      // Identifiers
                      Token("[_A-Za-z][_0-9A-Za-z]*", NewSymbol)
                  );
              }
          }
          
          public class Node { }
          
          public class HearPerceptorState : Node
          {
              public string Ident { get; set; }
              public double Value { get; set; }
          }
          
          public class HingeJointState : Node
          {
              public string Ident { get; set; }
              public double Value { get; set; }
          }
          
          public class Polar : Tuple<double, double, double>
          {
              public Polar(double a, double b, double c) : base(a, b, c) { }
          }
          
          public class ForceResistancePerceptorState : Node
          {
              public string Ident { get; set; }
              public Polar Polar { get; set; }
          }
          
          public class Test
          {
              public static void Main()
              {
                  var input = @"
                      (
                          (Hear 12.3 HelloWorld)
                          (HJ LAJ1 -0.42)
                          (FRP lf (pos 2.3 1.7 0.4))
                      )
                  ";
          
                  // visit DRY helpers
                  Func<object, object[]> asRecord = value => (object[])value;
                  Func<object, Symbol> symbol = value => SExpressionSyntax.Symbol(value);
                  Func<object, string> identifier = value => symbol(value).Id;
          
                  // the SExpr visit, proper
                  Func<object[], Node[]> visitAll = null;
                  Func<object[], Node> visitHear = null;
                  Func<object[], Node> visitHJ = null;
                  Func<object[], Node> visitFRP = null;
          
                  visitAll =
                      all =>
                          all.
                          Select
                          (
                              item =>
                                  symbol(asRecord(item)[0]).Id != "Hear" ?
                                  (
                                      symbol(asRecord(item)[0]).Id != "HJ" ?
                                      visitFRP(asRecord(item))
                                      :
                                      visitHJ(asRecord(item))
                                  )
                                  :
                                  visitHear(asRecord(item))
                          ).
                          ToArray();
          
                  visitHear =
                      item =>
                          new HearPerceptorState { Value = (double)asRecord(item)[1], Ident = identifier(asRecord(item)[2]) };
          
                  visitHJ =
                      item =>
                          new HingeJointState { Ident = identifier(asRecord(item)[1]), Value = (double)asRecord(item)[2] };
          
                  visitFRP =
                      item =>
                          new ForceResistancePerceptorState
                          {
                              Ident = identifier(asRecord(item)[1]),
                              Polar =
                                  new Polar
                                  (
                                      (double)asRecord(asRecord(item)[2])[1],
                                      (double)asRecord(asRecord(item)[2])[2],
                                      (double)asRecord(asRecord(item)[2])[3]
                                  )
                          };
          
                  var syntax = new CustomSExpressionSyntax();
          
                  var sexpr = syntax.Parse(input);
          
                  var nodes = visitAll(asRecord(sexpr));
          
                  Console.WriteLine("SO_3051254");
                  Console.WriteLine();
                  Console.WriteLine(nodes.Length == 3);
                  Console.WriteLine(nodes[0] is HearPerceptorState);
                  Console.WriteLine(nodes[1] is HingeJointState);
                  Console.WriteLine(nodes[2] is ForceResistancePerceptorState);
              }
          }
          

          可在此处测试:

          https://repl.it/CnLC/1

          'HTH,

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-07-09
            相关资源
            最近更新 更多