2017-01-25 4 views
2

Я пытаюсь найти хороший способ разобрать строку сообщения в объект. Строка имеет фиксированную длину и описывается ниже.Методы синтаксического анализа строк

Snippet of string spec

  • протокол = 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); 

... но я буду по кругу на минуту и ​​не получить в любом месте. Любые указатели были бы большой помощью.

+1

Используйте регулярное выражение для каждого отдельного раздела. Это будет просто и не путать –

+1

'Regex' с группами захвата, вероятно, так, как я бы это сделал, если честно –

+1

Regex - это правильный инструмент для извлечения и синтаксического анализа строки. Группа Regex отображает MyConfig, кроме типа, и почему вы создаете свой собственный? Тем не менее, я не знаю, как вы используете тип здесь. У вас большой переключатель? Это правда, что Regex трудно читать, но вы можете построить его тщательно со многими комментариями. Разделите его на кусочки, и это не будет путать. – qxg

ответ

4

Я не думаю, что регулярное выражение сбивает с толку, если сделано правильно.Вы можете использовать именованные захватив группы, и вы можете определить его довольно аккуратно (например, для первых трех полей, которые вы можете продлить столько, сколько вы хотите):

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; 
+0

Это не 'parseRegex.Groups', а' match.Groups'. И у вас отсутствует '' 'после вашего' Tuple'. – krlzlx

+0

@krlzlx: Спасибо, исправляю ... – Sefe

0

Идея: GetNextCharacters(int position,int length, out newPosition) которая дает вам следующие length символов, нужной вам строки и новую позицию для следующего вызова.

Таким образом, вы меняете только length в каждом звонке.

0

Вы можете определить класс со свойствами для каждого раздела строки и настраиваемый атрибут (пример. FieldItem), который задает начальную/конечную позиции, в конструкторе вы можете передать всю строку, а затем написать некоторую внутреннюю логику на основе атрибуты свойств (используя отражение) для загрузки каждого свойства из предоставленной строки (возможно, метод ReadString или что-то еще) на основе использования SubString (начало и конец) с индексами, взятыми из пользовательского атрибута. Я думаю, что это более чистая, чем определение специального регулярного выражения, плюс вы можете легко изменить определения полей, просто изменив свойства атрибута.

4

Так простая структура сообщений:

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

Если свойства внутри входной строки являются фиксированной ширины, то регулярное выражение накладных расходов в обоих реализации и производительности условиях. Идея создания общего парсера хороша, но имеет смысл, если у вас есть парсеры с множественными значениями для реализации. Поэтому нет причин иметь абстракцию, если есть только одна конкретная реализация.

Я бы с просто 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); 
    // ... 
} 
0

Возможно, вы сможете использовать класс 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]); 
    } 
} 
0

Как вы сказали, что если ваша строка статична вы можете использовать класс маршала, как это:

[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)) 
} 

Я думаю, что этот метод может сделать ваш код очень чистым и ремонтопригоден.