【问题标题】:String parsing techniques字符串解析技术
【发布时间】:2017-01-25 11:09:03
【问题描述】:

我正在尝试找到一种将消息字符串解析为对象的好方法。 该字符串是固定长度的,如下所述。

  • 协议 = int(2)
  • 消息类型 = 字符串 (1)
  • 测量 = 字符串 (4)

做一个简单的String.Split 会起作用,但我认为当你开始接近字符串的末尾时可能会有点麻烦。例如:

var field1 = s.SubString(0,2);
var field2 = s.SubString(2,4);
....
var field99 = s.SubString(88,4); // difficult magic numbers

我考虑过使用正则表达式,并认为这可能更令人困惑。

我试图想出一个优雅的解决方案,在那里我可以创建一个解析器,它传递一个“配置”,详细说明如何解析字符串。

类似...

 MyConfig config = new MyConfig()
 config.Add("Protocol",    Length=2, typeof(int));
 config.Add("MessageType", Length=1, typeof(char));


 Parser p = new Parser(config);
 var parserResult = p.Parse(message);

...但是我现在正在兜圈子,却没有到达任何地方。任何指针都会有很大帮助。

【问题讨论】:

  • 对每一个单独使用正则表达式。这会很简单,不会令人困惑
  • Regex 与捕获组可能是我会做的方式,如果我诚实的话
  • 正则表达式是提取和解析字符串的合适工具。一个正则表达式组映射到 MyConfig 除了类型,你为什么要自己构建?但是,我不知道您如何在这里利用类型。你有大开关吗?确实,Regex 很难阅读,但您可以使用许多 cmets 仔细构建它。把它分成几块,一点也不混乱。
  • 我最近回答了一个类似的问题。我创建了一个函数,它使用像这样的输入数组来解析固定宽度的字符串 string[] tabLocations = {1,20,30,50,70};

标签: c# .net string parsing


【解决方案1】:

一个简单的消息结构:

class Message
{
    public DateTime DateTime { get; set; }
    public int Protocol { get; set; }
    public string Measurement { get; set; }
    public string Type { get; set; }
    //....
}

结合一个知道如何反序列化它的类:

class MessageSerializer
{
    public Message Deserialize(string str)
    {
        Message message = new Message();
        int index = 0;
        message.Protocol = DeserializeProperty(str, ref index, 2, Convert.ToInt32);
        message.Type = DeserializeProperty(str, ref index, 1, Convert.ToString);
        message.Measurement = DeserializeProperty(str, ref index, 4, Convert.ToString);
        message.DateTime = DeserializeProperty<DateTime>(str, ref index, 16, (s) =>
        {
            // Parse date time from 2013120310:28:55 format
            return DateTime.ParseExact(s, "yyyyMMddhh:mm:ss", CultureInfo.CurrentCulture);
        });
        //...
        return message;
    }

    static T DeserializeProperty<T>(string str, ref int index, int count, 
        Func<string, T> converter)
    {
        T property = converter(str.Substring(index, count));
        index += count;
        return property;
    }
}

【讨论】:

    【解决方案2】:

    如果使用正确的方式,我认为正则表达式不会令人困惑。您可以使用命名捕获组,并且可以非常简洁地定义它(例如前三个字段,您可以随意扩展):

    const string GRP_PROTOCOL = "protocol";
    const string GRP_MESSAGE_TYPE = "msgtype";
    const string GRP_MEASUREMENT = "measurement";
    
    Regex parseRegex = new Regex(
        $"(?<{GRP_PROTOCOL}>.{{2}})" +
        $"(?<{GRP_MESSAGE_TYPE}>.{{1}})" +
        $"(?<{GRP_MEASUREMENT}>.{{4}})");
    

    您还可以在数组中定义您的组及其长度:

    const string GRP_PROTOCOL = "protocol";
    const string GRP_MESSAGE_TYPE = "msgtype";
    const string GRP_MEASUREMENT = "measurement";
    
    Tuple<string, int>[] groups = {
        Tuple.Create( GRP_PROTOCOL, 2 ),
        Tuple.Create( GRP_MESSAGE_TYPE, 1 ),
        Tuple.Create( GRP_MEASUREMENT, 4 )
    };
    
    Regex parseRegex =
        new Regex(String.Join("", groups.Select(grp => $"(?<{grp.Item1}>.{{{grp.Item2}}})").ToArray()));
    

    然后,您可以在需要时按名称访问组:

    Match match = parseRegex.Match(message);
    string protocol = match.Groups[GRP_PROTOCOL].Value;
    string msgType = match.Groups[GRP_MESSAGE_TYPE].Value;
    string measurement = match.Groups[GRP_MEASUREMENT].Value;
    

    【讨论】:

    • 不是parseRegex.Groups,而是match.Groups。在您的Tuple 之后,您缺少一个;
    【解决方案3】:

    如果输入字符串中的属性是固定宽度的,那么 Regex 在实现和性能方面都是开销。创建一个通用解析器的想法很好,但如果您有多个解析器要实现,这很有意义。因此,如果只有一个特定的实现,就没有理由进行抽象。

    我会选择 StringReader:

    using (var reader = new StringReader(input)) {
    }
    

    ...然后创建一些辅助扩展方法,如下所示:

    // just a sample code, to get the idea
    
    public static string ReadString(this TextReader reader, int count)
    {
        var buffer = new char[count];
        reader.Read(buffer, 0, count);
        return string.Join(string.Empty, buffer);
    }
    
    public static int ReadNumeric(this TextReader reader, int count)
    {
        var str = reader.ReadString(count);
        int result;
        if (int.TryParse(str, out result))
        {
            return result;
        }
        // handle error
    }
    
    // ...
    

    最终的用法是这样的:

    using (var reader = new StringReader(input)) {
        var protocol = reader.ReadNumeric(2);
        var messageType = reader.ReadString(1);
        var measurement = reader.ReadString(4);
        // ...
    }
    

    【讨论】:

      【解决方案4】:

      您可以为每个字符串部分定义一个具有属性的类,以及一个指定开始/结束位置的自定义属性(例如 FieldItem),在构造函数中您可以传递整个字符串,然后根据properties 属性(使用反射)从提供的字符串(可能是 ReadString 方法,或其他)加载每个属性,基于从自定义属性获取的索引的 SubString(start,end) 用法。 我认为这种方式比定义特殊的正则表达式更干净,而且您只需编辑属性属性即可轻松更改字段定义。

      【讨论】:

        【解决方案5】:

        一个想法是:GetNextCharacters(int position,int length, out newPosition) 它会为您提供下一个 length 字符、您想要的字符串以及下一次调用的新位置。

        这样您只需在每次通话中更改length

        【讨论】:

          【解决方案6】:

          您也许可以利用TextFieldParser 类。它可以接受用于解析的字段长度列表。

          using (var parser = new TextFieldParser(new StringReader(s))){
               parser.TextFieldType = FieldType.FixedWidth;
               parser.SetFieldWidths(2,1,4 /*etc*/);
               while (!parser.EndOfData)
               {
                   var data = parser.ReadFields(); //string[]
               }
          }
          

          但是,这只会将您的数据拆分为字符串数组。如果您的所有类型都是IConvertible,您可能会执行类似...

          var types = new[] {typeof(int), typeof(string), typeof(string), typeof(DateTime), /*etc..*/ };
          var data = parser.ReadFields();
          var firstVal = Convert.ChangeType(data[0], types[0]); 
          var secondVal = Convert.ChangeType(data[1], types[1]); 
          // etc..
          // or in a loop: 
          for (var i = 0; i<data.Length;++i){
            var valAsString = data[i];
            var thisType = types[i];
            var value = Convert.ChangeType(valAsString , thisType);
            // do something with value
          }
          

          虽然Convert.ChangeType 返回一个object,所以你的变量的类型也是object 类型,除非你要强制转换它们:

          var firstVal = (int)Convert.ChangeType(data[0], types[0]);
          // because unfortunately this is not valid:
          var firstVal = (types[0])Convert.ChangeType(data[0], types[0]);
          

          可能在这种情况下可以利用 dynamic 关键字,尽管我对此的经验很少,我不确定它是否会有所作为:

          dynamic firstVal = Convert.ChangeType(data[0], types[0]);

          请注意,dynamic 关键字和 TextFieldParser 类都涉及性能损失,该类已被记录为不是性能最高的(请参阅有关此问题的其他 SO 帖子),至少在更大的情况下字符串/文件。当然,如果您所做的只是解析单个字符串,那么使用 TextFieldParser 也可能对您的情况有点过分。

          如果您有一个代表此数据的 dto/poco 类,您始终可以将 ReadFields() 返回的字符串数组传递给您的 dto 上的构造函数,该构造函数可以为您填充数据...即:

          class Message {
              public DateTime DateTime { get; set; }
              public int Protocol { get; set; }
              public string Type { get; set; }
              public string Measurement {get;set;}
              public Message(string[] data) {
                 Protocol = int.Parse(data[0]);
                 Type = data[1];
                 Measurement = data[2];
                 DateTime = DateTime.Parse(data[3]);
              }
          }
          

          【讨论】:

            【解决方案7】:

            正如你所说,如果你的字符串是静态的,你可以使用 marshal 类,像这样:

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)]
            public struct TData
            {
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 2)]
                public protocol string;
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst =1)]
                public messageType string;
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
                public measurement 
            ...
                public int getProtocol(){return Convert.ToInt32(protocol);}
            ...
            }
            
            public string get(){
               var strSource="03EMSTR...";
                IntPtr pbuf = Marshal.StringToBSTR(buf);
                TData data= (TData)Marshal.PtrToStructure(pbuf,typeof(TData))
            }
            

            我认为这种方法可以让你的代码非常纯净和可维护。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2013-08-15
              • 1970-01-01
              • 2013-09-13
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-02-25
              • 1970-01-01
              相关资源
              最近更新 更多