Comparaison des parseurs CSV

23 août 2012

Tags : Comparatif, CSV, CsvEngine, Java

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 :
DocumentationMise en placeLectureÉcritureValidationTemps de traitement
BeanFiles--=XX15 120
JCsv++--X367 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.

Commentaires

Bench de MoteurCsv

22 février 2012

Tags : CSV, Java

Suite à la publication du billet MoteurCsv le JPA du CSV, quelqu'un m'a demandé si j'avais fait un bench. N'en ayant pas fait, je me suis lancé.
Les sources du bench sont disponibles sur github

Génération du fichier de test

Le "cahier des charges" fournit par "bbo" était le suivant :
  • ~10 000 000 de lignes.
  • ~200 caractères par ligne.
  • ~20 champs (4/5 gros champs autour de 20 caractères.)
C'est avec ces contraintes que j'ai créé la classe GenerationFchierCsv.
Cette classe génère donc un fichier de 10 000 000 de lignes contenant des lignes avec 20 champs :
  • Quatre champs de type String et de 30 caractères
  • Quatre champs de type Boolean (1 ou 0)
  • Quatre champs de type Integer et de 5 chiffres
  • Quatre champs de type Double et de 5 chiffres avant la virgule et 5 chiffres après la virgule
  • Quatre champs de type String et de 5 caractères

Voici le résultat de la génération du fichier :
Statistiques sur la taille des lignes :
  • Minimum : 215 caractères
  • Maximum : 227 caractères
  • Moyenne : 225,666 caractères
Taille du fichier : 2 266 666 035 octets soit 2,11 Go
Génération en 70 576ms
Le résultat est donc à peu près conforme au cahier des charges.

Constitution du bench

Le bench est réalisé par la classe Bench (je sais, je suis trop bon pour trouver des noms).
Pour le bench, je ne pouvais pas utiliser la méthode permettant de parser tout le fichier et qui renvoie une liste d'objet. Vu la taille du fichier, il est facile de comprendre que le mettre entièrement en mémoire ne serait pas une bonne idée.
Heureusement le moteur contient une autre méthode permettant de réaliser un traitement pour chaque ligne : MoteurCsv.parseFileAndInsert
Dans le cadre du bench j'ai donc utilisé cette méthode en ne réalisant aucun traitement :
public static void bench1() throws FileNotFoundException {
long startTime = System.currentTimeMillis();
moteur.parseFileAndInsert(new FileReader(fichier), ObjetCsv.class,
new InsertObject<objetcsv>() {
@Override
public void insertObject(ObjetCsv objet) {
// On ne fait rien dans le cadre du bench.
}
});
long elapsedTime = (System.currentTimeMillis() - startTime);
System.out.println("Lecture du fichier : " + elapsedTime + "ms");
}

J'ai également créé une méthode permettant de voir l'utilisation mémoire au fur et à mesure du test.
Le but de cette méthode est de regarder la mémoire occupée avant et après un GC entre chaque itération du bench. Cela permettra de vérifier entre autre qu'il n'y ait pas fuite mémoire.
public static void gestionMemoire() {
// Mémoire totale allouée
long totalMemory = Runtime.getRuntime().totalMemory();
// Mémoire utilisée
long currentMemory = totalMemory - Runtime.getRuntime().freeMemory();
System.out.println("Mémoire avant gc : " + (currentMemory / 1024) + "ko/" + (totalMemory / 1024) + "ko");
System.gc();
// Mémoire totale allouée
totalMemory = Runtime.getRuntime().totalMemory();
// Mémoire utilisée
currentMemory = totalMemory - Runtime.getRuntime().freeMemory();
System.out.println("Mémoire après gc : " + (currentMemory / 1024) + "ko/" + (totalMemory / 1024) + "ko");
}

Résultats

Les tests ont été menés sur un MacBookPro équipé d'un disque SSD et d'un Code i5.
ÉtapeTempsMémoire occupée avant GCMémoire occupée après GC
Avant de commencer/1 734ko338ko
Instanciation du moteur58 222µs14 049ko387ko
Lecture du fichier (itération 1)65 902ms14 049ko387ko
Lecture du fichier (itération 2)65 864ms13 796ko341ko
Lecture du fichier (itération 3)64 638ms14 059ko341ko
Lecture du fichier (itération 4)65 214ms13 700ko341ko
Lecture du fichier (itération 5)66 560ms14 027ko341ko

Ces résultats permettent de montrer plusieurs choses :
  • Temps pour instancier le moteur : quasi-null
  • Mémoire persistante pour le moteur : quelques Ko
  • Performances plutôt satisfaisantes avec un peu plus d'une minute pour lire un fichier de plus de 2Go

J'ai également fait du profiling avec YourKit afin de vérifier le comportement interne du moteur, cela a montré que la majorité du temps est passé dans la librairie open-csv, l'overhead du moteur est donc plutôt faible.

Reproduire le bench

N'hésitez pas à reproduire le bench et à me dire les résultats que vous obtenez :
  • Cloner le projet depuis github : github.com/ybonnel/BenchMoteurCsv
  • Importer le projet en tant que projet maven dans Eclipse
  • Lancer d'abord le main de la classe GenerationFchierCsv afin de générer le fichier de test
  • Lancer ensuite le main de classe Bench afin de lancer le bench en lui-même
Si vous connaissez d'autres parseurs CSV n'hésitez pas à ajouter des benchs dans le projet pour comparer les performances avec d'autres parseurs.

Commentaires

MoteurCsv le JPA du CSV

20 février 2012

Tags : CSV, Java

Voici un titre qui va faire gonfler mes chevilles :)
Je vais donc vous présenter une petite librairie Java sans prétention que j'ai réalisé afin de pourvoir lire et écrire des fichiers CSV facilement.

C'est quoi le CSV?

Bon pour la définition du format CSV, je vous invite à consulter l'article wikipedia.

Pourquoi cette librairie?

Cette librairie a été crée afin de pouvoir gérer le format GTFS. Ce format est utilisé afin de décrire un réseau de Transports en commun. Techniquement, c'est un zip contenant des fichiers CSV.
J'ai eu à utiliser ce format suite à l'ouverture des données de transports à Rennes où j'ai réalisé une application Android (Transports Rennes) permettant de consulter les horaires de bus.
A l'époque où j'ai créé cette librairie, je baignais dans du JPA la journée, et j'avoue que j'aime beaucoup l'idée de décrire du mapping directement dans la classe associé en utilisant des annotations.
C'est comme ça que MoteurCsv est né.

Alors comment ça s'utilise?

J'ai essayé de simplifier au maximun l'utilisation de la librairie.

Installation

Maven

Si vous êtes sous maven, l'intégration dans votre projet est très simple.
Il suffit d'ajouter dans votre pom.xml :
<dependencies>
<dependency>
<groupId>fr.ybo</groupId>
<artifactId>moteurcsv</artifactId>
</dependency>
</dependencies>

<repositories>
<repository>
<id>ybonnel-release</id>
<url>https://repository-ybonnel.forge.cloudbees.com/release/</url>
</repository>
</repositories>

Autre

Si vous n'êtes pas sous maven, il vous suffit d'intégrer les deux jars suivant :

Utilisation

Maintenant le vif du sujet, comment utiliser cette petite librairie.
Afin de pouvoir lire ou écrire un CSV il faut commencer par décrire la classe correspondante.
On va prendre pour l'exemple un CSV décrivant des personnes avec deux colonnes : "nom" et "prenom".
Voici un exemple correspondant :
nom,prenom
Bonnel,Yan
Vador,Dark
Noël,Père

Déclaration de la classe

Nous allons donc créer la classe Personne associée avec les annotations permettant de faire le mapping :

// Annotation permettant de dire au moteur que cette classe est associée à un fichier CSV.
@FichierCsv
public class Personne {

// Annotation permettant de dire au moteur que cet attribut est mappé avec la colonne "nom" du CSV.
@BaliseCsv("nom")
private String nom;

// Annotation permettant de dire au moteur que cet attribut est mappé avec la colonne "prenom" du CSV.
@BaliseCsv("prenom")
private String prenom;
}

Création du moteur

MoteurCsv moteur = new MoteurCsv(Personne.class);

Lecture d'un fichier CSV

Voici maintenant le code permettant de lire le fichier, et de le transformer en une liste d'objets :
InputStream stream = new FileInputStream(new File("personnes.csv"));
List<Personne> personnes = moteur.parseInputStream(stream, Personne.class);

Écriture d'un fichier CSV

Et le core permettant d'écrire un fichier :
Writer writer = new FileWriter(new File("personnes.csv"));
moteur.writeFile(writer, personnes, Personne.class);

Conclusion

J'espère que cette petite librairie sera utile à quelqu'un d'autre que moi :)
Elle est open-source (LGPL v3) : github.com/ybonnel/MoteurCsv
N'hésitez donc pas à forker et à faire des pull request!
Site généré contenant entre autre la javadoc : ybonnel.github.com/MoteurCsv

Commentaires