Node.js, accompagné des bibliothèques disponibles sur NPM, facilite grandement la lecture de fichiers CSV. Mais dès que la taille du fichier augmente, les choses se compliquent : la gestion de la mémoire devient un problème. Cet article vous propose une progression du cas le plus simple au plus complexe.
Créer le projet
Nous utiliserons l’installateur de paquets PNPM.
Pour créer le projet, il suffit de taper la commande suivante :
pnpm init
Il faut ajouter l’attribut type avec la valeur module dans le fichier package.json pour pouvoir utiliser la syntaxe import/export au lieu de require.
{
"name": "csv",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.6.2"
}
Placez le fichier CSV à la racine du projet pour simplifier son accès.
.
├── main.js
├── memory-leaks.csv
└── package.json
Cas simple
Le fichier CSV est léger (quelques Mo au plus), avec peu de colonnes, les éléments sont séparés par le caractère ,
virgule et son contenu est facilement modifiable à la main.
Par exemple :
name,age,city
Alice,30,Paris
Bob,25,Lyon
David,35,Toulouse
Eva,22,Nantes
Frank,40,Bordeaux
Grace,27,Strasbourg
Hugo,31,Nice
Isabelle,29,Rennes
Julien,33,Lille
Lire le fichier
Node.js fournit trois façons lire un fichier nous excluons la callback car sa syntaxe est obsolète.
Lecture synchrone
La méthode que l’on privilégie dans le cas d’un script simple. On bloque le fil d’exécution du code jusqu’à ce que le fichier soit entièrement lu.
import { readFileSync } from "node:fs";
const file = readFileSync("memory-leaks.csv");
Lecture asynchrone
Lorsque votre application doit rester réactive, il est préférable d’utiliser une lecture asynchrone. Elle évite de bloquer le fil d’exécution principal.
import { readFile } from "node:fs/promises";
const file = await readFile("memory-leaks.csv");
Parser le fichier CSV
neat-csv est une surcouche pratique autour du module csv-parser qui permet d’extraire les données en une ligne de code.
import { readFileSync } from "node:fs";
import neatCsv from "neat-csv";
const file = readFileSync("memory-leaks.csv");
const csv = await neatCsv(file);
neatCsv retourne une promesse contenant un tableau d’objets, où chaque objet représente une ligne du fichier CSV.
Par exemple :
[
{ name: "Alice", age: "30", city: "Paris" },
{ name: "Bob", age: "25", city: "Lyon" },
];
Les options de parsing les plus courantes
neat-csv nous donne accès à l’ensemble des options de parsing de csv-parser. De nombreuses options sont données, nous ne faisons qu’aborder les plus utilisées.
separator
Exemple de fichier CSV avec point-virgule comme séparateur :
name;age;city
Alice;30;Paris
Bob;25;Lyon
David;35;Toulouse
Lecture du fichier avec un séparateur personnalisé :
import { readFileSync } from "node:fs";
import neatCsv from "neat-csv";
const file = readFileSync("memory-leaks.csv");
const csv = await neatCsv(file, { separator: ";" });
headers
Exemple de fichier CSV sans en-tête de colonnes :
Alice,30,Paris
Bob,25,Lyon
David,35,Toulouse
Lecture du fichier avec des en-têtes de colonnes :
import { readFileSync } from "node:fs";
import neatCsv from "neat-csv";
const file = readFileSync("memory-leaks.csv");
const csv = await neatCsv(file, { headers: ["name", "age", "city"] });
mapHeaders
Les en-têtes de colonnes deviennent les noms des attributs dans les objets générés à la lecture du CSV. Il arrive toutefois que les fichiers contiennent des en-têtes mal formées ou peu adaptées à une utilisation en code.
Nom complet,Annee de naissance,Code Postal
Alice,1994,Paris
Bob,1999,Lyon
David,1989,Toulouse
L’option mapHeaders permet de formater les en-têtes, on souhaite les convertir en camelcase. La bibliothèque camelcase nous permet de le faire.
import { readFileSync } from "node:fs";
import neatCsv from "neat-csv";
import camelcase from "camelcase";
const file = readFileSync("memory-leaks.csv");
const csv = await neatCsv(file, {
mapHeaders: ({ header }) => {
return camelcase(header);
},
});
L’output :
[
{ nomComplet: "Alice", anneeDeNaissance: "1994", codePostal: "Paris" },
{ nomComplet: "Bob", anneeDeNaissance: "1999", codePostal: "Lyon" },
{ nomComplet: "David", anneeDeNaissance: "1989", codePostal: "Toulouse" },
];
Autres options
Vous pouvez retrouver la liste sur cette page dans la section API.
Cas complexe
Structure imbriquée
Du CSV au JSON
Un CSV d’une structure imbriquée ressemble généralement à ceci :
identity.name,identity.year,identity.city,job.title,job.remote
Alice,1994,Paris,Developer,true
Bob,1999,Lyon,Designer,false
David,1989,Toulouse,Sysadmin,true
L’imbrication est indiquée par des points dans les noms de colonnes. Plutôt que d’écrire soi-même une fonction récursive, la bibliothèque csvtojson gère ce cas automatiquement.
import { readFileSync } from "node:fs";
import * as csvtojson from "csvtojson";
const file = readFileSync("memory-leaks.csv", "utf-8");
const csv = await csvtojson.default().fromString(file);
Résultat :
[
{ identity: { name: "Alice", year: "1994", city: "Paris" } },
{ identity: { name: "Bob", year: "1999", city: "Lyon" } },
{ identity: { name: "David", year: "1989", city: "Toulouse" } },
];
Du JSON au CSV
Si vous devez exporter des objets JavaScript vers un fichier CSV, la bibliothèque jsonexport reconnaît les structures imbriquées et les aplatit automatiquement.
import { writeFileSync } from "node:fs";
import jsonexport from "jsonexport";
const objects = [
{ identity: { name: "Alice", year: "1994", city: "Paris" } },
{ identity: { name: "Bob", year: "1999", city: "Lyon" } },
{ identity: { name: "David", year: "1989", city: "Toulouse" } },
];
const csv = await jsonexport(objects);
writeFileSync("memory-leaks.csv", csv);
Le fichier écrit :
identity.name,identity.year,identity.city
Alice,1994,Paris
Bob,1999,Lyon
David,1989,Toulouse
Fichier volumineux
Prenons un fichier concret : title.principals.tsv.gz sur IMDB. Une fois décompressé et converti en CSV, il atteint 4 Go.
Problème : un fichier trop gros pour Node.js
Pour les fichiers classiques, on utilise un code simple comme celui-ci :
import { readFileSync } from "node:fs";
import neatCsv from "neat-csv";
const file = readFileSync("title.principals.csv");
const csv = await neatCsv(file);
Mais ici, Node.js refuse de lire le fichier :
node:fs:405
throw new ERR_FS_FILE_TOO_LARGE(size);
^
RangeError [ERR_FS_FILE_TOO_LARGE]: File size (4083748583) is greater than 2 GiB
at tryCreateBuffer (node:fs:405:13)
at readFileSync (node:fs:458:14)
at file:///home/memory-leaks/Documents/memory-leaks-tutoriels/csv/main.js:4:14
at ModuleJob.run (node:internal/modules/esm/module_job:272:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:583:26)
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5) {
code: 'ERR_FS_FILE_TOO_LARGE'
}
Node.js v23.8.0
Impossible donc de charger ce fichier d’un seul coup en mémoire.
Streams : la solution adaptée
Les fichiers volumineux sont l’occasion d’utiliser l’une des fonctionnalités les plus puissantes de Node.js : les streams.
Jusqu’ici, on lisait tout le fichier en mémoire, puis on le passait au parser. Deux étapes séquentielles, coûteuses en RAM.
Avec les streams (flux), on peut traiter les données au fur et à mesure qu’elles arrivent. Cela permet deux optimisations majeures :
- Mémoire : on évite d’occuper plusieurs gigaoctets de RAM avec un buffer géant.
- Temps : le traitement commence immédiatement, sans attendre la fin de la lecture.
Mise en oeuvre
Voici un petit script Node.js qui lit le fichier title.principals.csv (extrait du dataset IMDb) en streaming et compte combien de fois chaque type de rôle (actor, director, etc.) apparaît. L’objectif est de traiter un gros fichier sans le charger entièrement en mémoire, en gardant le code simple et lisible.
import { createReadStream } from "node:fs";
import csv from "csv-parser";
// Objet pour compter combien de fois chaque catégorie apparaît
const categories = {};
// On lit le fichier CSV ligne par ligne en streaming
createReadStream("title.principals.csv")
.pipe(csv()) // On parse chaque ligne en objet JS
.on("data", (row) => {
const category = row.category;
// On incrémente le compteur pour cette catégorie
categories[category] = (categories[category] || 0) + 1;
})
.on("end", () =>
// Une fois le fichier entièrement lu, on affiche le résultat
console.table(categories);
);
Le résultat de console.table :
┌─────────────────────┬──────────┐
│ (index) │ Values │
├─────────────────────┼──────────┤
│ self │ 7002342 │
│ director │ 4019890 │
│ producer │ 3541725 │
│ cinematographer │ 1873897 │
│ composer │ 1530075 │
│ writer │ 5646830 │
│ editor │ 2468643 │
│ actor │ 11382929 │
│ actress │ 8434698 │
│ production_designer │ 562517 │
│ archive_footage │ 291785 │
│ casting_director │ 561023 │
│ archive_sound │ 4789 │
└─────────────────────┴──────────┘