grimboite/articles/dev/2015-10-01-csv-reader.md

4.0 KiB

Title Date Slug Tags
Un p'tit parser de fichiers en C# 2015-10-01 a-little-csharp-files-parser dev, dotnet, csv, fichier, parser

Pour faciliter l'analyse de fichiers .csv, j'ai écrit un petit ensemble de classes (loin d'être parfait, mais qui a le mérite de plutôt bien fonctionner pour mon utilisation). Cet analyseur ne traite pas les cas où un délimiteur se trouve perdu au beau milieu d'un champ (ce qui est quand même la base, je sais bien), il faudrait donc améliorer la partie lecture du fichier pour qu'elle crache un tableau de chaînes de caractères et ce sera bon (genre ici ou ).

Les interfaces de construction

L'interface IBuilder est juste l'interface dont dépendent les classes de représentation des records. Si un fichier représente des personnes, on définira ensuite une classe People qui implémente l'interface ci-dessous (et en particulier la méthode Build(string[] content);).

public interface IBuilder<T>
{
    T Build(string[] content);
}

public class BuilderException : Exception
{
    public BuilderException(string message) : base(message) { }
}

Ensuite, on a une interface IManager, pour définir la manière dont sont gérés les records. Plusieurs lignes du fichier lu peuvent par exemple représenter une et une seule personne (et plusieurs de ses contrats, par exemple). Cela permet d'avoir un gestionnaire qui va stocker chaque personne dans une clé, et qui renverra la liste des éléments connus au travers de la méthode Items().

public interface IManager<T> where T : new()
{
    /// <summary>
    /// Ajoute un élément à la liste interne.
    /// </summary>
    /// <param name="item">L'élément à ajouter.</param>
    void Add(T item);

    /// <summary>
    /// Retourne tous les éléments de la liste.
    /// </summary>
    /// <returns>Une instance de liste générique de type T.</returns>
    List<T> Items();
}

Et finalement, la méthode Read(), qui lit le fichier passé en paramètre (plutôt un tableau contenant les lignes, en fait) et qui retourne la liste des éléments de type T au travers du gestionnaire (implémentant l'interface IManager), s'il existe.

public List<T> Read<T>(string[] lines,
                        char[] delimiters = null,
                        bool processFirstLine = false,
                        Managers.IManager<T> manager = null)
        where T : Builders.IBuilder<T>, new() // implements a default ctor and IBuilder
{
    if (delimiters == null || delimiters.Length == 0)
        delimiters = new char[2] { ';', '\t' };

    List<T> list = new List<T>();

    for (int i = 0; i < lines.Count(); i++)
    {
        if (!processFirstLine && i == 0) // avoid line zero, as it contains titles
            continue;

        string line = lines[i];

        try
        {
            string[] array = line.Split(delimiters, StringSplitOptions.None);

            // checks at least one item is not empty or white spaces
            if (array.All(c => String.IsNullOrWhiteSpace(c)))
                continue;

            // init element with default ctor
            T elem = new T();

            // call Builder method with string array
            elem.Build(array);

            if (manager != null)
            {
                manager.Add(elem);
            }
            else
            {
                // add item to returned list
                list.Add(elem);
            }
        }
        catch (Builders.BuilderException buildex)
        {
            System.Diagnostics.Debug.WriteLine(buildex.ToString());
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.ToString());
        }
    }

    if (manager != null)
        return manager.Items();

    return list;
}

Du coup, les parties à revoir seraient le split sur la ligne, qui n'est franchement pas top, et la gestion des exceptions, puisque dans l'exemple ci-dessus, on ne log strictement rien.