jeudi 23 août 2012

Comparaison des parseurs CSV

Mon petit projet "CsvEgine" commençant à ressembler à quelque chose, je vais me lancer dans un petit comparatifs des parseurs que je peux trouver sur internet.

Pour pouvoir comparer ces différents parseurs je vais utiliser l'exemple basique de l'utilisation de CsvEngine : Basics.
Il s'agit donc de parser un fichier CSV contenant des chiens avec comme attributs le nom, la race et le propriétaire.
On verra également comment écrire le fichier.
La troisième étape de comparaison sera la validation :
  • Le nom et la race sont obligatoires
  • La race doit faire partie d'une liste connues de races

La quatrième étape sera du parsing CSV complexe, ajout de retour à la ligne pour le propriétaire.

Et enfin pour finir la comparaison je finirai avec un petit bench, en reprenant ce que j'avais fait mon "CsvEngine" (qui à l'époque s'appelait MoteurCsv) : Bench MoteurCsv

Pour la liste des parseurs, je suis parti de cet article : Java et CSV tour d'horizon des solutions open-source
Je rajouterai quand même CsvEngine :)

Comme d'habitude, l'ensemble du code est disponible sur github : CsvJavaComparaison.

BeanFiles

Site : http://code.google.com/p/beanfiles/

Étape 0 : Documentation et mise en place

Voici tout d'abord les problèmes que j'ai rencontré avec BeanFiles pour la mise en place :
  • La documentation est très pauvre : une page sur le wiki, plus une classe de test.
  • BeanFiles est une librairie construite avec maven, mais je n'ai pas trouvé de repo maven associé, ce qui a compliqué la mise en place
  • La documentation est relativement pauvre, elle ne contient qu'un exemple de code, mais pas de tuto de mise en place, du coup j'ai été obligé d'aller voir dans le pom.xml du code source pour avoir les dépendances.


Étape 1 : Lecture du fichier CSV simple

La mise en place est relativement simple une fois que les problèmes ont été résolus :).
Mise à part les problèmes sités plus haut, BeanFiles n'aime pas du tout avoir des lignes vides à la fin du fichier.
Une autre limitation, les attributs de la classe doivent avoir les mêmes noms que les entêtes dans le fichier CSV.

La lecture est relativement simple :
public List<Dog> getDogs(InputStream stream) throws IOException {
    CSVReaderIterator<Dog> readerIterator = new CSVReaderIterator<Dog>(Dog.class, stream);
    stream.close();
    List<Dog> dogs = new ArrayList<Dog>();
    for (Dog dog : readerIterator) {
        dogs.add(dog);
    }
    return dogs;
}
J'aurais préféré que la fermeture du stream soit gérée par la librairie.
L'utilisation d'un iterator est pas bête, toutefois elle peux donner l'impression que la lecture du fichier se fait au fur et à mesure, alors que pas du tout :)
Du coup comme la lecture est finie après l'appel au constructeur, j'aurais bien aimé pouvoir récupérer directement la liste (un getAll() sur l'iterator).
Je trouve qu'avoir mis le parsing dans le constructeur n'est pas génial...

Étape 2 : écriture de fichier CSV

Il n'est pas possible d'écrire avec cette librairie.

Étape 3 : validation

Pas de validation non plus.

Étape 4 : parsing complexe.

Aucun problème avec les retour à la ligne pour BeanFiles.

Étape 5 : bench

Pour le bench, j'ai généré un fichier "moyen" (100 000 lignes) et un gros fichier (1 000 000 lignes).
Pour le fichier moyen, le temps moyen de traitement est de 1292ms.

Pour le gros fichier, il a fallu que je monte la JVM à 2Go. C'est un problème que je vois à BeanFiles, il n'y a pas de possibilité d'ajouter un handler pour effectuer un traitement au fur et à mesure de la lecture. Pour traiter de gros fichiers, c'est donc un gros problème.
Pour traiter ce fichier, le temps moyen de traitement est de 15120ms avec une consommation mémoire d'un peu moins de 1,6Go.

BeanIO

BeanIO est sûrement très bien, mais j'aime pas trop les configuration XML, donc je passe.

Commons-Csv

Je ne l'ai pas étudié non plus, pour deux raisons :
  • La version actuelle est la version 1.0-SNAPSHOT
  • C'est bas niveau, un peu comme open-csv


CsvToSql

Encore du XML... et en plus, ça n'a pas l'air adapté à ce que je veux faire.

FlatPack

Encore du XML...

JavaCSV

Les exemples de code montre que cette librairie ne fait pas vraiment du mapping : JavaCSV Samples

JCsv

Site : http://code.google.com/p/jcsv/

Étape 0 : Documentation et mise en place

La mise en place est très simple et documentée, juste la dépendance à ajouter et c'est parti.
C'est le gros point positif pour cette librairie la documentation est très riche.

Étape 1 : Lecture du fichier CSV simple

Étant fan du principe des annotations, je choisi d'utiliser cette méthode pour mon parsing :
public class Dog {
    @MapToColumn( column = 0)
    private String name;
    @MapToColumn( column = 1)
    private String race;
    @MapToColumn( column = 2)
    private String proprietary;
}
J'aurais préféré pouvoir utiliser la ligne d'entête mais bon...

La lecture n'est pas très compliquée non plus :
public List<Dog> getDogs(InputStream stream) throws IOException {
    Reader reader = new InputStreamReader(stream);

    ValueProcessorProvider provider = new ValueProcessorProvider();
    CSVEntryParser<Dog> entryParser = new AnnotationEntryParser<Dog>(Dog.class, provider);
    CSVReader<Dog> csvDogReader = new CSVReaderBuilder<Dog>(reader)
            .entryParser(entryParser)
            .strategy(new CSVStrategy(',', '"', '#', true, true)).build();

    return csvDogReader.readAll();
}
Lors de mon premier essai, je n'avais pas mis de "strategy", et l'exception remontée n'était pas très parlante (ArrayIndexOutBoundException)... Mis à part ça, je n'ai pas eu de d'autres problèmes.

Étape 2 : écriture de fichier CSV

L'écriture n'est pas très compliquée, par contre on ne peut pas utiliser les annotations, ce qui est un peu dommage.
public void writeFile(List<Dog> dogs, File file) throws IOException {

    CSVEntryConverter<Dog> entryConverter = new CSVEntryConverter<Dog>() {
        @Override
        public String[] convertEntry(Dog dog) {
            String[] columns = new String[3];
            columns[0] = dog.getName();
            columns[1] = dog.getRace();
            columns[2] = dog.getProprietary();

            return columns;
        }
    };
    CSVWriter<Dog> csvDogWriter = new CSVWriterBuilder<Dog>(new FileWriter(file))
            .entryConverter(entryConverter)
            .strategy(new CSVStrategy(',', '"', '#', true, true))
            .build();
    csvDogWriter.writeAll(dogs);
    csvDogWriter.close();
}
Le résultat n'est pas très bon, si les champs contiennent des retours à la ligne, il n'ajoute pas les caractères '"' avant et après. Il n'ajoute pas l'entête.
Bref, l'écriture existe, mais elle n'est pas satisfaisante de mon point de vue.

Étape 3 : validation

Pas de principe de validation.

Étape 4 : parsing complexe.

Aucun problème avec les retours à la ligne pour JCsv.

Étape 5 : bench

J'utilise les mêmes fichiers que pour BeanFiles.
Pour le fichier moyen, le temps moyen de traitement est de 39 219ms.

Pas de problème de consommation mémoire, JCsv permet de lire ligne par ligne, on a donc pas besoin de tout stocker dans une liste. Par contre les performances sont très mauvaises, cela provient du fait que JCsv appelle getAnnotations pour chaque ligne, et ne met rien en cache.

Le gros fichier confirme le bench avec le fichier moyen :
  • Temps de traitement moyen : 367 449ms


JSefa

Site : http://jsefa.sourceforge.net/

Étape 0 : Documentation et mise en place

La mise en place n'est pas documentée et il n'existe pas de repo maven (en tout cas je l'ai pas trouvé), par contre il suffit d'ajouter le jar.
Au niveau documentation, il existe une page avec les exemples basiques, pour des trucs plus complexes, il faut regarder la javadoc ou le code source.

Étape 1 : Lecture du fichier CSV simple

La déclaration du mapping via les annotations est plutôt simple :
@CsvDataType
public class Dog {
    @CsvField(pos = 0)
    private String name;
    @CsvField(pos = 1)
    private String race;
    @CsvField(pos = 2)
    private String proprietary;
}
J'aurais préféré pouvoir utiliser la ligne d'entête mais bon...

La lecture n'est pas très compliquée non plus :
public List<Dog> getDogs(InputStream stream) throws IOException {
    CsvConfiguration config = new CsvConfiguration();
    config.setFieldDelimiter(',');
    Deserializer deserializer = CsvIOFactory.createFactory(config, Dog.class).createDeserializer();

    List<Dog> dogs = new ArrayList<Dog>();

    deserializer.open(new InputStreamReader(stream));
    while (deserializer.hasNext()) {
        dogs.add(deserializer.<Dog>next());
    }
    deserializer.close(true);

    return dogs;
}
Pour filtrer l'entête on est obligé d'ajouter un Filter, un simple boolean dans les config aurait été appréciable...

Étape 2 : écriture de fichier CSV

L'écriture n'est pas très compliquée non plus :
public void writeFile(List<Dog> dogs, File file) throws IOException {
    CsvConfiguration config = new CsvConfiguration();
    config.setFieldDelimiter(',');
    Serializer serializer = CsvIOFactory.createFactory(config, Dog.class).createSerializer();

    serializer.open(new FileWriter(file));
    for (Dog dog : dogs) {
        serializer.write(dog);
    }
    serializer.close(true);
}
Toujours pas moyen d'ajouter l'entête.

Étape 3 : validation

Il existe une couche de validation, par contre elle n'est pas documentée, et je n'ai pas réussi à la faire fonctionner (n'hésitez pas à corriger mon code sur github si vous savez comment faire :) ).

Étape 4 : parsing complexe.

Aucun problème avec les retour à la ligne pour JSefa.

Étape 5 : bench

J'utilise les mêmes fichiers que pour BeanFiles.
Pour le fichier moyen, le temps moyen de traitement est de 791ms.

Le gros fichier confirme le bench avec le fichier moyen :
  • Temps de traitement moyen : 7 652ms


open-csv

C'est une librairie bas niveau (utilisée par la plupart des parseurs haut niveaux), je l'étudierai donc pas ici.

Ostermiller CSV

Dans cette librairie, le mapping se fait à la main, je ne l'étudierai donc pas.

Skife CSV

Encore une librairie bas niveau.

Super CSV

Site : http://supercsv.sourceforge.net/

Étape 0 : Documentation et mise en place

La mise en place n'est pas documentée, c'est pas du maven, dont pas si simple que ça, faut bien penser à mettre les deux jar dans les dépendances...
De manière générale la documentation est plutôt pas mal (malgré le fait qu'il n'existe pas de documentation pour la mise en place).

Étape 1 : Lecture du fichier CSV simple

Pas d'annotation pour cette librairie, tout se fait par le nom des attributs.
Pour les options il faut passer par des CellProcessors, j'y reviendrai pour la validation.

La lecture n'est pas très compliquée :
public List<Dog> getDogs(InputStream stream) throws IOException {
    List<Dog> dogs = new ArrayList<Dog>();

    ICsvBeanReader inFile = new CsvBeanReader(new InputStreamReader(stream), CsvPreference.STANDARD_PREFERENCE);
    final String[] header = inFile.getCSVHeader(true);
    Dog dog;
    while( (dog = inFile.read(Dog.class, header)) != null) {
        dogs.add(dog);
    }
    inFile.close();
    return dogs;
}
La gestion de l'entête et l'itération se fait à la main, je trouve ça dommage. Pour le reste c'est plutôt efficace.

Étape 2 : écriture de fichier CSV

Pour l'écriture, il n'y a pas de gestion de mapping, tout ce fait à la main, du coup je ne l'étudierai pas ici.

Étape 3 : validation

Pour la validation, c'est plutôt efficace, par contre cela repose sur l'ordre des champs, un peu dommage.
Il faut donc déclarer un tableau de CellProcessor :
public static final CellProcessor[] userProcessors = new CellProcessor[] {
        new NotNull(),
        new IsIncludedIn(new HashSet<Object>(DogValid.POSSIBLE_RACES)),
        null
};
On passe ensuite ce tableau pour le parsing des lignes :
inFile.read(DogValid.class, header, userProcessors)


Étape 4 : parsing complexe.

Aucun problème avec les retours à la ligne pour Super Csv.

Étape 5 : bench

J'utilise les mêmes fichiers que pour BeanFiles.
Pour le fichier moyen, le temps moyen de traitement est de 749ms.

Le gros fichier confirme le bench avec le fichier moyen :
  • Temps de traitement moyen : 7 523ms


CsvEngine

Pour ceux qui ne le savent pas, je suis le développeur de cette librairie, je ne suis donc sans doute pas très objectif :).
Site : https://github.com/ybonnel/CsvEngine

Étape 0 : Documentation et mise en place

La mise en place est très simple et documentée : Wiki install.
De manière générale, entre le wiki, la javadoc et les tests, je pense objectivement que CsvEngine est la librairie la plus documentée de celles que j'ai testées.

Étape 1 : Lecture du fichier CSV simple

Il faut tout d'abord ajouter les annotations à la classe dog :
@CsvDataType
@CsvFile
public class Dog {
    @CsvColumn("name")
    private String name;
    @CsvColumn("race")
    private String race;
    @CsvColumn("proprietary")
    private String proprietary;
}
Un truc qu'il faudra ajouter dans CsvEngine et le fait de rendre le nom du champs CSV facultatif (déduit du nom de l'attribut).

La lecture est très simple :
public List<Dog> getDogs(InputStream stream) throws IOException, CsvErrorsExceededException {
    CsvEngine engine = new CsvEngine(Dog.class);
    return engine.parseInputStream(stream, Dog.class).getObjects();
}


Étape 2 : écriture de fichier CSV

L'écriture n'est pas plus compliquée que la lecture :
public void writeFile(List<Dog> dogs, File file) throws IOException {
    CsvEngine engine = new CsvEngine(Dog.class);
    engine.writeFile(new FileWriter(file), dogs, Dog.class);
}


Étape 3 : validation

Pour la validation tout passe par les annotations :
@CsvFile
public class DogValid {
    @CsvColumn(value = "name", mandatory = true)
    private String name;
    @CsvValidation(ValidatorRace.class)
    @CsvColumn(value = "race", mandatory = true)
    private String race;
    @CsvColumn("proprietary")
    private String proprietary;
    
    public static class ValidatorRace extends ValidatorCsv {
        @Override
        public void validate(String field) throws ValidateException {
            if (!POSSIBLE_RACES.contains(field)) {
                throw new ValidateException("The race \"" + field + "\" isn't correct");
            }
        }
    }
}
On lit ensuite la fichier comme d'habitude :
public List<DogValid> readDogsValid(InputStream stream) throws CsvErrorsExceededException {
   CsvEngine engine = new CsvEngine(DogValid.class);
   return engine.parseInputStream(stream, DogValid.class).getObjects();
}


Étape 4 : parsing complexe.

Aucun problème avec les retour à la ligne pour CsvEngine.

Étape 5 : bench

J'utilise les mêmes fichiers que pour BeanFiles. Pour le fichier moyen, le temps moyen de traitement est de 693ms.

Le gros fichier confirme le bench avec le fichier moyen :
  • Temps de traitement moyen : 7 028ms


Conclusion

Première conclusion, écrire un article aussi long avec l'éditeur de Blogger est une corvée... faut que je trouve autre chose pour mon blog.

Voici un petit tableau récapitulatif des tests que j'ai pu mené sur ces librairies :
Documentation Mise en place Lecture Écriture Validation Temps de traitement
BeanFiles - - = X X 15 120
JCsv + + - - X 367 449
JSefa - = - = - 7 652
Super CSV + = + - + 7 523
CsvEngine + + + + + 7 028
Légende :
  • X : N'existe pas
  • - : Existe mais pas terrible
  • = : Existe et marche plutôt bien
  • + : Existe et est vraiment bien fait :)
De mon point de vue tout à fait objectif CsvEngine est la meilleure librairie sur tout les points, son seul défaut est sans doute mon Anglais très approximatif.