Pour des besoins divers, nous avons souvent besoin de récupérer des informations dans un fichier csv afin d’en exploiter ses données dans un programme.
Dans cet article je vais vous présenter comment créer un parseur générique qui permettra de répondre à de nombreux besoins.

Les sources contenant le parseur et son implémentation : https://github.com/Lefanthome/FileParser

Le Parseur

Le parseur aura les caractéristiques suivantes :

  • Il prend les propriétés de l’objet devant être mappées avec les colonnes
  • Il permet de gérer les séparateurs « , » et « ; »
  • Une méthode Deserialize
  • Une méthode Serialize

Avant de voir le code, nous allons d’abord créer tous les éléments dont on a besoin

Le fichier

Le fichier csv contient en entête les noms des colonnes séparés par un « ; », puis les données elles-mêmes séparées par un « ; »

Sous Excel

excel1

Sous NotePad

ID;NAME;Company
1;Ludovic;Softfluent
2;Clément;XBrain
3;Lionel;Softfluent
4;Camille;Softfluent

Utilisation d’un Attribut.Net

Nous allons ensuite créer un attribut nommé “MappingColAttribute”  (qui hérite de la classe .Net “Attribute”). Cet attribut positionné sur les propriétés d’une classe permet de faire le mapping entre le fichier et les propriétés d’une classe. Cet attribut servira aussi à donner le nom de colonne utilisé dans le fichier.

[AttributeUsage(AttributeTargets.Property)]
public class MappingColAttribute : Attribute
{
	public string ColName { get; set; }
	public MappingColAttribute(string colName)
	{
	 ColName = colName;
	}
}

Ensuite nous créons la classe modèle “CsvModel” qui correspondra au fichier en ajoutant à ses propriétés l’attribut créé ci-dessus :

public class CsvModel
{
	[MappingCol("ID")]
	public int Id { get; set; }
	[MappingCol("NAME")]
	public string Name { get; set; }
	[MappingCol("Company")]
	public string Company { get; set; }
}

Le Parser

La définition de la classe

public class FileParser<T> where T : new()

Cette classe est générique, afin de manipuler n’importe quel objet qui hérite de “T”, ici la classe modèle.

L’appel au constructeur permet d’initialiser les éléments dont on a besoin, en l’occurrence la définition du séparateur, et d’appeler la méthode InitMapping qui contient le code pour l’instanciation des dictionnaires qui nous serviront pour le mapping entre les colonnes du fichier et l’objet modèle, et inversement.

Dans cette méthode, on utilise la réflexion .Net pour parcourir les propriétés publiques non statiques de notre objet et aller chercher l’attribut MappingCol et enregistrer ces informations dans deux dictionnaires. L’un va nous servir pour le mapping entre les colonnes du fichier et les propriétés, et l’autre dictionnaire pour faire l’inverse.

        public FileParser(char separator)
        {
            _separator = separator;
            InitMapping();
        }
private void InitMapping()
        {
            Dictionary<string, string> dicMapping = new Dictionary<string, string>();
            var type = typeof(T);
            var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance
                | BindingFlags.GetProperty | BindingFlags.SetProperty);
            var q = properties.Where(x => x.GetCustomAttributes(typeof(NonSerializedAttribute), false).Length == 0).AsQueryable();
            _properties = q.OrderBy(a => a.Name).ToList();
            _mappingExcelColumnsToObject = new Dictionary<string, string>();
            _mappingObjectToExcelColumns = new Dictionary<string, string>();
            foreach (var propertyInfo in _properties.Where(x => x.GetCustomAttributes(typeof(MappingColAttribute), false).Length > 0))
            {
                var mappingColName = (MappingColAttribute)propertyInfo.GetCustomAttributes(typeof(MappingColAttribute), false).FirstOrDefault();
                if (mappingColName != null)
                {
                    _mappingExcelColumnsToObject.Add(mappingColName.ColName, propertyInfo.Name);
                    _mappingObjectToExcelColumns.Add(propertyInfo.Name, mappingColName.ColName);
                }
            }
        }

Utilisation d’expressions régulières : Regex

Afin de séparer chaque ligne du fichier, j’utilise des regex en fonction du séparateur choisi soit ‘,’ ou soit ‘;’.

private readonly Regex _wordRegexCommo = new Regex("((?<=\")[^\"]*(?=\"(,|$)+)|(?<=,|^)[^,\"]*(?=,|$))", RegexOptions.Compiled);
private readonly Regex _wordRegexSeparatorPoint = new Regex("((?<=\")[^\"]*(?=\"(;|$)+)|(?<=;|^)[^;\"]*(?=;|$))",RegexOptions.Compiled);

Désérialisation du fichier

Pour déserialiser un fichier en une liste d’objets, nous avons la méthode Deserialize qui prend en paramètre un stream et qui retourne IList<T>

Cette méthode va dans un premier temps, récupérer le contenu du stream, en mettant dans une liste les colonnes puis une seconde liste avec le contenu des lignes suivantes

Une fois ces 2 listes remplissent, on va parcourir la liste des lignes et faire le traitement pour construire notre objet.

A la fin de cette méthode on obtient une liste de notre objet qui est le contenu de notre fichier.

public IList<T> Deserialize(Stream stream)
        {
            //Mapping
            string[] columns;
            string[] rows;
            try
            {
                using (var sr = new StreamReader(stream))
                {
                    // Get columns
                    columns = sr.ReadLine().Replace("\"", "").Split(_separator);
                    var content = sr.ReadToEnd();
                    var lineReturn = content.Contains(Environment.NewLine) ? Environment.NewLine : "\n";
                    // Get Lines
                    rows = content.Split(new[] { lineReturn }, StringSplitOptions.None);
                }
            }
            catch (Exception ex)
            {
                throw new InvalidParserFormatException("The CSV File is Invalid.", ex);
            }
            var data = new List<T>();
            for (int row = 0; row < rows.Length; row++)
            {
                var line = rows[row];
                // Ignore Empty line
                if (string.IsNullOrWhiteSpace(line))
                    continue;
                line = line.Replace("\"\"", "");
                MatchCollection matches = _wordRegex.Matches(line);
                var parts = (from object match in matches select match.ToString()).ToList();
                if (parts.Count == 1 && parts[0] != null && parts[0] == "EOF")
                    break;
                var datum = new T();
                for (var i = 0; i < parts.Count; i++)
                {
                    var value = parts[i];
                    var column = columns[i];
                    var columnsName = column;
                    if (_mappingExcelColumnsToObject.Count > 0)
                    {
                        if (_mappingExcelColumnsToObject.ContainsKey(column))
                        {
                            columnsName = _mappingExcelColumnsToObject[column];
                        }
                    }
                    var p = _properties.FirstOrDefault(a => a.Name.Equals(columnsName, StringComparison.InvariantCultureIgnoreCase));
                    if (p == null)
                        continue;
                    p.SetValue(datum, Convert.ChangeType(GetConvertedValue(value, p), p.PropertyType), null);
                }
                data.Add(datum);
            }
            return data;
        }

La méthode GetConvertedValue permet de convertir la valeur de type string dans le type correspondant à la propriété liée.

private dynamic GetConvertedValue(string value, PropertyInfo propertyInfo)
        {
            var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
            dynamic convertedvalue = value.ToUpper();
            if (value.ToUpper() == "NULL")
            {
                convertedvalue = null;
            }
            else if (propertyInfo.PropertyType == typeof(bool) || propertyInfo.PropertyType == typeof(bool?))
            {
                convertedvalue = value != "0";
            }
            else if (propertyInfo.PropertyType == typeof(int) || propertyInfo.PropertyType == typeof(int?))
            {
                if (string.IsNullOrEmpty(convertedvalue))
                    convertedvalue = 0;
                else
                    convertedvalue = converter.ConvertFrom(value);
            }
            else
            {
                convertedvalue = converter.ConvertFrom(value);
            }
            return convertedvalue;
        }

La sérialisation

Cette méthode permet de convertir la liste d’objets en un string qui sera le contenu du futur fichier. Le fichier contient une entête correspondant aux propriétés de notre objet.

public string Serialize(IList<T> data)
        {
            return BuildString(data).ToString().Trim();
        }
private StringBuilder BuildString(IEnumerable<T> data)
        {
            var sb = new StringBuilder();
            var values = new List<string>();
            sb.AppendLine(GetHeader());
            foreach (var item in data)
            {
                values.Clear();
                values.AddRange(_properties.Select(p => p.GetValue(item, null)).Select(raw => raw == null ? "" : raw.ToString().Replace(_separator.ToString(CultureInfo.InvariantCulture), string.Empty)));
                sb.AppendLine(string.Join(_separator.ToString(CultureInfo.InvariantCulture), values.ToArray()));
            }
            return sb;
        }
        private string GetHeader()
        {
            var columns = _properties.Select(a => a.Name).ToArray();
            if (_mappingObjectToExcelColumns.Count > 0)
            {
                for (int i = 0; i < columns.Count(); i++)
                {
                    if (_mappingObjectToExcelColumns.ContainsKey(columns[i]))
                    {
                        columns[i] = _mappingObjectToExcelColumns[columns[i]];
                    }
                }
            }
            var header = string.Join(_separator.ToString(CultureInfo.InvariantCulture), columns);
            return header;
        }

Mise en pratique du Parseur

Maintenant que le parseur est prêt, nous allons passer à son utilisation. Pour cela, j’ai créé une application console (sur le github, elle s’appelle “ConsoleFileParser”).

Conversion d’un fichier en liste d’objets

On récupère le fichier puis on utilise la méthode Deserialize, après avoir initialisé la classe FileParser. Cette méthode nous permet de récupérer le contenu pour ensuite l’afficher sur la console.

// Instanciation du Parser
var csvSerializer = new FileParser<CsvModel>(separator);
IList<CsvModel> reportLines = null;
if (!File.Exists(path))
{
	Console.WriteLine($"PathFile not exist:{path}");
	return null;
}
// Deserialize File
using (FileStream fs = new FileStream(path, FileMode.Open))
{
	reportLines = csvSerializer.Deserialize(fs);
}
if (reportLines == null)
	Console.WriteLine($"Path File:{path} file is null");
else
	Console.WriteLine($"Path File:{path} Total Lines:{reportLines.Count}");

Le résultat affiché sur la console :

resultConsole

Conversion d’une liste d’objets en fichier

On va maintenant voir comment utiliser notre classe FileParser afin d’obtenir un fichier.

On doit créer un modèle avec des propriétés décorées par l’attribut de mapping nous permettant de donner le nom que l’on veut à nos colonnes.

public class ContactModel
{
	[MappingCol("Id")]
	public int Id { get; set; }
	[MappingCol("Name")]
	public string Name { get; set; }
	[MappingCol("Societe")]
	public string Company { get; set; }
	[MappingCol("Pays")]
	public string Country { get; set; }
}

Ensuite, il suffit de créer une liste d’objets ContactModel : on appelle la méthode Serialize qui va nous retourner un string que l’on va ensuite enregistrer grâce à l’utilisation de FileStream.

Ci-dessous son implémentation :

if(File.Exists(@"G:\gihub\WebFileParser\ConsoleFileParser\NewContactList.csv"))
{
	File.Delete(@"G:\gihub\WebFileParser\ConsoleFileParser\NewContactList.csv");
}
List<ContactModel> contacts = new List<ContactModel>();
contacts.Add(new ContactModel { Id = 1, Name = "Ludovic", Company = "Softfluent", Country = "France" });
contacts.Add(new ContactModel { Id = 2, Name = "Alexendra", Company = "MS", Country = "UK" });
contacts.Add(new ContactModel { Id = 3, Name = "Thierry", Company = "Softfluent", Country = "USA" });
var csvSerializer = new FileParser<ContactModel>(';');
using (FileStream fs = File.Create(@"G:\gihub\WebFileParser\ConsoleFileParser\NewContactList.csv"))
{
	Byte[] info = new UTF8Encoding(true).GetBytes(csvSerializer.Serialize(contacts));
	// Add some information to the file.
	fs.Write(info, 0, info.Length);
}

Le contenu du fichier qui contient les colonnes définies par l’attribut « MappingCol » ainsi que le séparateur ‘ ;’

Sous Excel

excel2

Sous NotePad

Societe;Pays;Id;Name
Softfluent;France;1;Ludovic
MS;UK;2;Alexendra
Softfluent;USA;3;Thierry

Conclusion

A travers cet article, je vous ai montré comment créer un petit parseur. Il reste bien sûr des améliorations à faire comme l’ordre des colonnes, la gestion des colonnes obligatoires dans le fichier.

Ne ratez plus aucune actualité avec la newsletter mensuelle de SoftFluent

Newsletter SoftFluent