J'essaie de trouver un bon moyen d'analyser une chaîne de message dans un objet. La chaîne est de longueur fixe et décrite ci-dessous.

Snippet of string spec

  • protocole = int (2)
  • type de message = chaîne (1)
  • mesure = chaîne (4)
  • etc

Faire un simple String.Split fonctionnera, mais je pense que cela peut être un peu fastidieux lorsque vous commencez à aller vers la fin de la chaîne. par exemple.:

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

J'ai envisagé d'utiliser un Regex et j'ai pensé que c'était peut-être encore plus déroutant.

J'essayais de penser à une solution élégante, où je pourrais créer un analyseur qui a passé une «config» qui détaillerait comment analyser la chaîne.

Quelque chose comme...

 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);

... mais je tourne en rond à la minute et je ne vais nulle part. Tous les pointeurs seraient d'une grande aide.

2
Matt 25 janv. 2017 à 14:09

7 réponses

Meilleure réponse

Je ne pense pas qu'une expression régulière prête à confusion si elle est faite de la bonne manière. Vous pouvez utiliser des groupes de capture nommés et vous pouvez le définir assez clairement (par exemple, pour les trois premiers champs, que vous pouvez étendre autant que vous le souhaitez):

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}})");

Vous pouvez également définir vos groupes et leurs longueurs dans un tableau:

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()));

Vous pouvez ensuite accéder aux groupes par nom chaque fois que vous en avez besoin:

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;
3
Sefe 25 janv. 2017 à 11:53

Vous pouvez définir une classe avec des propriétés pour chaque section de chaîne et un attribut personnalisé (ex. FieldItem) qui spécifie les positions de début / fin, dans le constructeur, vous pouvez passer la chaîne entière, puis écrire une logique interne basée sur les attributs de propriétés ( en utilisant la réflexion) pour charger chaque propriété à partir de la chaîne fournie (une méthode ReadString peut-être, ou autre), en fonction de l'utilisation de SubString (début, fin) avec des index extraits de l'attribut personnalisé. C'est plus propre de cette façon, je pense, qu'en définissant une expression régulière spéciale, et vous pouvez facilement modifier les définitions de champ en modifiant simplement les propriétés d'attribut.

0
Andrei Filimon 25 janv. 2017 à 11:19

Si les propriétés à l'intérieur de la chaîne d'entrée sont de largeur fixe, Regex est une surcharge en termes d'implémentation et de performances. L'idée de créer un analyseur générique est bonne, mais elle a du sens si vous avez plusieurs analyseurs à implémenter. Il n'y a donc aucune raison d'avoir une abstraction s'il n'y a qu'une seule implémentation particulière.

J'irais avec juste StringReader:

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

... puis en créant quelques méthodes d'extension d'aide comme celles-ci:

// 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
}

// ...

Et l'utilisation finale serait comme ceci:

using (var reader = new StringReader(input)) {
    var protocol = reader.ReadNumeric(2);
    var messageType = reader.ReadString(1);
    var measurement = reader.ReadString(4);
    // ...
}
2
admax 25 janv. 2017 à 11:49

Donc, une structure de message simple:

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

Combiné avec une classe qui sait comment le désérialiser:

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;
    }
}
4
TVOHM 25 janv. 2017 à 11:42

Une idée serait: GetNextCharacters(int position,int length, out newPosition) qui vous donne les length caractères suivants, la chaîne que vous vouliez et la nouvelle position pour le prochain appel.

De cette façon, vous ne modifiez le length qu'à chaque appel.

0
Guy Coder 25 janv. 2017 à 12:31

Comme vous l'avez dit, si votre chaîne est statique, vous pouvez utiliser la classe marshal, comme ceci:

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

Je pense que cette méthode peut rendre votre code très pur et maintenable.

0
M_Farahmand 2 févr. 2017 à 20:20

Vous pourrez peut-être tirer parti du TextFieldParser cours. Il peut accepter une liste de longueurs de champ à utiliser pour l'analyse.

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

Cependant, cela ne ferait que diviser vos données en un tableau de chaînes. Si tous vos types étaient IConvertible, vous pourriez peut-être faire quelque chose comme ...

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
}

Bien que Convert.ChangeType renvoie un object donc le type de vos variables serait également de type object à moins que vous ne les castiez:

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]);

Vous pourriez être en mesure d'exploiter le mot-clé dynamic dans ce cas, même si mon expérience avec celui-ci est très limitée et je ne suis pas sûr que cela fasse une différence:

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

Notez que le mot clé dynamic ainsi que la classe TextFieldParser, qui ont été documentés comme n'étant pas les plus performants, entraînent des pénalités de performances (voir simplement d'autres articles SO sur ce sujet), du moins avec chaînes / fichiers plus volumineux. Bien sûr, l'utilisation de TextFieldParser peut également être excessive pour votre cas si tout ce que vous faites est d'analyser une seule chaîne.

Si vous avez une classe dto / poco qui représente ces données, vous pouvez toujours passer le tableau de chaînes retourné par ReadFields() dans un constructeur sur votre dto qui peut remplir les données pour vous ... c'est-à-dire:

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
pinkfloydx33 25 janv. 2017 à 16:42