Un reader de fichier CSV rapide comme l'éclair
Ajouté par inwebo, mis à jour le 2026-04-19 avec les tags : #Open-source #PHP
Un reader de fichier CSV rapide comme l'éclair
Comment transformer une frustration technique en un défi : écrire une librairie PHP qui fait mieux que le standard actuel sur toutes les métriques qui comptent vraiment en production, la consommation mémoire et la rapidité d'exécution. Voici mon périple.
D'une frustration à un défi
Il était une fois, dans un bureau de start-up, une équipe devant migrer des données d'une ancienne application vers une nouvelle version toute propre. Facile ! J'avais un fichier de nos clients au format CSV de plusieurs centaines de milliers de lignes. Le problème est identifié : mapper les anciennes colonnes sur les nouvelles colonnes de nos entités, filtrer les données non pertinentes (ex : un client sans adresse mail) et normaliser les données valables (ex : un format de numéro de téléphone pour les gouverner tous).
C'est un problème assez trivial et une tâche assez rébarbative. Ouvrir le fichier en lecture, ne pas lire la 1ère ligne car c'est le header, ajouter le header d'une colonne correspondant à une valeur puis écrire une function de rappel ad hoc pour nettoyer/valider les données. C'est toujours la même soupe, et ce sur des projets perso ou pro.
Étant un bon développeur, je ne souhaitais pas réinventer la roue, la suite était évidente. Voyons ce que la communauté nous propose, et la réponse est : league/csv. Librairie maintenue, une bonne documentation et des millions de téléchargements !
Merci l'open-source, je peux maintenant jeter un œil au code puis faire quelques benchmarks avec PHPBench. Je me suis rendu compte d'une chose après plusieurs benchmarks : la consommation mémoire est proportionnelle à la taille du fichier lu et la vitesse de lecture est systématiquement inférieure à un bon vieil objet de la SPL : SplFileObject.
Si l'abstraction se fait au détriment de la facilité de maintenance et de la vitesse d'exécution, c'est qu'il est peut-être temps de la réécrire !
Je me suis donc lancé un défi : écrire un lecteur de fichier CSV from scratch avec une API simple, une bonne doc et rester dans l'esprit K.I.S.S.
Voici le plan d'action
Ce projet n'est utile que si, et seulement si, la qualité du code produit est haute ! Pour cela, voici les contraintes que je me suis imposées avant toute écriture de code :
- PHPStan niveau 10
- 100 % code coverage
- KISS. Je dois pouvoir revenir sur ce code dans 6 mois et le comprendre facilement
- Consommation mémoire contenue
- Plus rapide que league/csv
En ce qui concerne PHPStan niveau 10, je n'avais jamais utilisé ce niveau, c'est l'occasion idéale ! Pas de var mixed, pas d'unsafe operation tout doit être typé. Combiné avec du 100 % code coverage, aucune surprise de bug en production.
Pour la consommation de mémoire, ce qui est le cœur de mon problème, la réponse est simple : pas de chargement du fichier complet en mémoire, mais plutôt l'utilisation d'un pointeur couplé avec un generator qui yield une ligne à la fois. Ce qui fait que le fichier fasse 1 000 lignes ou 1 000 000, la consommation mémoire est la même.
J'ai mis en place un CI avec les workflows GitHub. À chaque git push : php-cs-fixer corrige les standards de code, puis analyse statique avec PHPStan, et finalement lancement des tests unitaires. Si tout se déroule sans accroc, alors le package est prêt.
Je peux donc afficher les badges correspondants dans le repository et ainsi avoir un contrat d'exigence avec les utilisateurs !
Le design
Un des atouts de la SPL c'est que c'est une boîte à outils assez conséquente, et la chose encore mieux, c'est qu'il existe SplFileObject qui gère déjà la lecture des fichiers CSV. L'étendre signifie que je n'ai plus qu'à me concentrer sur le métier, qui est, je le rappelle : filtrer et normaliser des lignes. Cela me donne une API réduite (KISS). Il n'y a plus que 3 concepts à implémenter.
pushNormalizer(function (array &$row): void { $row['FirstName'] = trim($row['FirstName']); $row['Salary'] = (int) $row['Salary']; }) ->pushFilter(fn (array $row): bool => $row['Salary'] > 80000); foreach ($reader->rows() as $row) { // Only clean, filtered rows reach here }
Et voilà ! La librairie n'expose (presque) que 3 méthodes : Reader::pushNormalizer(), Reader::pushFilter() et Reader::rows() (notre generator).
Les résultats
J'ai donc écrit un benchmark avec l'excellente librairie PHPBench pour comparer la lecture, le filtrage et la normalisation des données. Et les résultats se trouvent sur la page github.
Lecture
Dans tous les scénarios de lecture, inwebo/csv-reader est plus rapide, entre 23 % et 42 % ! C'est un gain de temps énorme, notamment sur les gros fichiers CSV. Cela peut faire gagner beaucoup de temps, donc d'argent.
Filtrage
En ce qui concerne le filtrage des lignes, la différence est flagrante : entre 42 % et 58 %. Pas mal du tout. Et finalement, pour la normalisation, entre 23 % et 52 %.
Consommation mémoire
La consommation mémoire est limitée dans tous les tests à 741 Ko, que ce soit avec un fichier de 55 Ko ou un de 6 Mo ! Le contrat est donc rempli : plus rapide et moins de consommation mémoire que notre concurrent.
Detailed stats
Library Variant Peak memory Min time Average time Max time Relative standard deviation
| inwebo/csv-reader | Small C.S.V. file (~55 ko) | 741.808kb | 3.687ms | 3.921ms | 4.727ms | ± 8.63%
| inwebo/csv-reader | Medium C.S.V. file (~584 ko) | 741.808kb | 38.521ms | 43.217ms | 43.974ms | ± 5.18%
| inwebo/csv-reader | Large C.S.V. file (~6.1 Mo) | 741.808kb | 421.194ms | 437.022ms | 461.837ms | ± 3.40%
| league/csv | Small C.S.V. file (~55 ko) | 968.656kb | 6.070ms | 6.388ms | 6.466ms | ± 2.39%
| league/csv | Medium C.S.V. file (~584 ko) | 968.656kb | 57.896ms | 62.444ms | 65.491ms | ± 4.26%
| league/csv | Large C.S.V. file (~6.1 Mo) | 968.656kb | 603.112ms | 664.219ms | 670.120ms | ± 4.37%
Installation
La librairie est disponible sur packagist, et donc un classique :
composer req inwebo/csv-reader
fait l'affaire. Quelques exemples d'utilisation sont disponibles également dans le fichier README.md.
Conclusion
Déléguer le travail rébarbatif à la classe SplFileObject pour la lecture des fichiers, puis l'étendre pour ajouter mon métier, était la bonne solution. C'est exactement ce que doit être une librairie à valeur ajoutée : se concentrer sur le métier pour exposer une API simple de lecture, sans charge cognitive pour le dev.
PHPStan niveau 10 est vraiment une bonne idée. Cela m'a permis d'ENFIN mettre le nez dans les @templates. J'ai perdu un peu de temps à comprendre les mécanismes, mais maintenant mon IDE m'aide à utiliser correctement l'API.
Je suis heureux du résultat et ce fut un bon exercice ! Le code est disponible sous licence MIT et sur GitHub. Le benchmark se trouve sur ce repository.
Une étoile ou de la revue de code sont également les bienvenues.
Prochaine étape
Dans ce projet : pouvoir lire en parallèle des fichiers CSV énormes pour encore plus de performance avec les parallels [) mais cela est pour un autre billet.