Come leggere e scrivere file CSV in Node.js utilizzando Node-CSV

L’autore ha selezionato la Società degli Ingegneri Donne per ricevere una donazione come parte del programma Scrivi per le Donazioni.

Introduzione

A CSV is a plain text file format for storing tabular data. The CSV file uses a comma delimiter to separate values in table cells, and a new line delineates where rows begin and end. Most spreadsheet programs and databases can export and import CSV files. Because CSV is a plain-text file, any programming language can parse and write to a CSV file. Node.js has many modules that can work with CSV files, such as node-csv, fast-csv, and papaparse.

In questo tutorial, utilizzerai il modulo node-csv per leggere un file CSV utilizzando gli stream di Node.js, che ti consente di leggere grandi set di dati senza consumare molta memoria. Modificherai il programma per spostare i dati analizzati dal file CSV in un database SQLite. Recupererai anche i dati dal database, li analizzerai con node-csv, e utilizzerai gli stream di Node.js per scriverli in un file CSV a blocchi.

Effettua il deploy delle tue applicazioni Node da GitHub utilizzando la Piattaforma App di DigitalOcean. Lascia che DigitalOcean si concentri sulla scalabilità della tua app.

Prerequisiti

Per seguire questo tutorial, avrai bisogno di:

Passaggio 1 — Configurazione della directory del progetto

In questa sezione, creerai la directory del progetto e scaricherai i pacchetti per la tua applicazione. Scaricherai anche un set di dati CSV da Stats NZ, che contiene dati sulla migrazione internazionale in Nuova Zelanda.

Per iniziare, crea una directory chiamata csv_demo e naviga nella directory:

  1. mkdir csv_demo
  2. cd csv_demo

Successivamente, inizializza la directory come un progetto npm utilizzando il comando npm init:

  1. npm init -y

L’opzione -y notifica a npm init di rispondere “sì” a tutte le richieste. Questo comando crea un package.json con valori predefiniti che puoi modificare in qualsiasi momento.

Con la directory inizializzata come progetto npm, puoi ora installare le dipendenze necessarie: node-csv e node-sqlite3.

Inserisci il seguente comando per installare node-csv:

  1. npm install csv

Il modulo node-csv è una raccolta di moduli che ti consente di analizzare e scrivere dati su un file CSV. Il comando installa tutti e quattro i moduli che fanno parte del pacchetto node-csv: csv-generate, csv-parse, csv-stringify e stream-transform. Utilizzerai il modulo csv-parse per analizzare un file CSV e il modulo csv-stringify per scrivere dati su un file CSV.

Successivamente, installa il modulo node-sqlite3:

  1. npm install sqlite3

Il modulo node-sqlite3 consente alla tua app di interagire con il database SQLite.

Dopo aver installato i pacchetti nel tuo progetto, scarica il file CSV sulle migrazioni in Nuova Zelanda con il comando wget:

  1. wget https://www.stats.govt.nz/assets/Uploads/International-migration/International-migration-September-2021-Infoshare-tables/Download-data/international-migration-September-2021-estimated-migration-by-age-and-sex-csv.csv

Il file CSV che hai scaricato ha un nome lungo. Per rendere più facile lavorarci, rinomina il file con un nome più corto usando il comando mv:

  1. mv international-migration-September-2021-estimated-migration-by-age-and-sex-csv.csv migration_data.csv

Il nuovo nome del file CSV, migration_data.csv, è più corto e più facile da gestire.

Utilizzando nano, o il tuo editor di testo preferito, apri il file:

  1. nano migration_data.csv

Una volta aperto, vedrai un contenuto simile a questo:

demo_csv/migration_data.csv
year_month,month_of_release,passenger_type,direction,sex,age,estimate,standard_error,status
2001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344,0,Final
2001-01,2020-09,Long-term migrant,Arrivals,Male,0-4 years,341,0,Final
...

La prima riga contiene i nomi delle colonne e tutte le righe successive contengono i dati corrispondenti a ciascuna colonna. Una virgola separa ciascun dato. Questo carattere è conosciuto come delimitatore perché delimita i campi. Non sei limitato all’uso delle virgole. Altri delimitatori popolari includono i due punti (:), i punti e virgola (;) e le tabulazioni (\td). Devi sapere quale delimitatore viene utilizzato nel file poiché la maggior parte dei moduli lo richiede per analizzare i file.

Dopo aver revisionato il file e identificato il delimitatore, esci dal tuo file migration_data.csv utilizzando CTRL+X.

Hai ora installato le dipendenze necessarie per il tuo progetto. Nella prossima sezione, leggerai un file CSV.

Passaggio 2 — Lettura dei File CSV

In questa sezione, utilizzerai node-csv per leggere un file CSV e registrare il suo contenuto nella console. Utilizzerai il metodo createReadStream() del modulo fs per leggere i dati dal file CSV e creare uno stream leggibile. Successivamente, instraderai lo stream in un altro stream inizializzato con il modulo csv-parse per analizzare i frammenti di dati. Una volta analizzati i frammenti di dati, puoi registrarli nella console.

Crea e apri un file readCSV.js nel tuo editor preferito:

  1. nano readCSV.js

Nel tuo file readCSV.js, importa i moduli fs e csv-parse aggiungendo le seguenti righe:

demo_csv/readCSV.js
const fs = require("fs");
const { parse } = require("csv-parse");

Nella prima riga, definisci la variabile fs e assegnale l’oggetto fs che il metodo require() di Node.js restituisce quando importa il modulo.

Nella seconda riga, si estrae il metodo parse dall’oggetto restituito dal metodo require() nella variabile parse utilizzando la sintassi di destrutturazione.

Aggiungi le seguenti righe per leggere il file CSV:

demo_csv/readCSV.js
...
fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })

Il metodo createReadStream() del modulo fs accetta un argomento con il nome del file che si desidera leggere, che in questo caso è migration_data.csv. Quindi, crea uno stream leggibile, che prende un file grande e lo suddivide in chunk più piccoli. Uno stream leggibile consente di leggere solo i dati da esso e non scriverci.

Dopo aver creato lo stream leggibile, il metodo pipe() di Node inoltra i chunk di dati dallo stream leggibile a un altro stream. Il secondo stream viene creato quando il metodo parse() del modulo csv-parse viene invocato all’interno del metodo pipe(). Il modulo csv-parse implementa uno stream di trasformazione (uno stream leggibile e scrivibile), prendendo un chunk di dati e trasformandolo in un’altra forma. Ad esempio, quando riceve un chunk come 2001-01,2020-09,Emigrante a lungo termine,Arrivi,Femmina,0-4 anni,344, il metodo parse() lo trasformerà in un array.

Il metodo parse() accetta un oggetto che accetta proprietà. L’oggetto quindi configura e fornisce ulteriori informazioni sui dati che il metodo analizzerà. L’oggetto accetta le seguenti proprietà:

  • delimiter definisce il carattere che separa ogni campo nella riga. Il valore , indica al parser che le virgole delimitano i campi.

  • from_line definisce la riga da cui il parser dovrebbe iniziare a analizzare le righe. Con il valore 2, il parser salterà la riga 1 e inizierà dalla riga 2. Poiché inserirai i dati nel database in seguito, questa proprietà ti aiuta a evitare di inserire i nomi delle colonne nella prima riga del database.

In seguito, si allega un evento di streaming utilizzando il metodo on() di Node.js. Un evento di streaming consente al metodo di consumare un pezzo di dati se viene emesso un determinato evento. L’evento data viene attivato quando i dati trasformati dal metodo parse() sono pronti per essere consumati. Per accedere ai dati, si passa una callback al metodo on(), che prende un parametro chiamato row. Il parametro row è un pezzo di dati trasformato in un array. All’interno della callback, si registrano i dati nella console utilizzando il metodo console.log().

Prima di eseguire il file, aggiungerai ulteriori eventi di flusso. Questi eventi di flusso gestiscono gli errori e scrivono un messaggio di successo sulla console quando tutti i dati nel file CSV sono stati consumati.

Ancora nel tuo file readCSV.js, aggiungi il codice evidenziato:

demo_csv/readCSV.js
...
fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })
  .on("end", function () {
    console.log("finished");
  })
  .on("error", function (error) {
    console.log(error.message);
  });

L’evento end viene emesso quando tutti i dati nel file CSV sono stati letti. Quando ciò accade, viene richiamata la callback e viene registrato un messaggio che indica che ha terminato.

Se si verifica un errore durante la lettura e il parsing dei dati CSV, viene emesso l’evento error, che richiama la callback e registra il messaggio di errore sulla console.

Il tuo file completo dovrebbe ora assomigliare al seguente:

demo_csv/readCSV.js
const fs = require("fs");
const { parse } = require("csv-parse");

fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })
  .on("end", function () {
    console.log("finished");
  })
  .on("error", function (error) {
    console.log(error.message);
  });

Salva ed esci dal tuo file readCSV.js usando CTRL+X.

Successivamente, esegui il file usando il comando node:

  1. node readCSV.js

L’output sarà simile a questo (modificato per brevità):

Output
[ '2001-01', '2020-09', 'Long-term migrant', 'Arrivals', 'Female', '0-4 years', '344', '0', 'Final' ] ... [ '2021-09', ... '70', 'Provisional' ] finished

Tutte le righe nel file CSV sono state trasformate in array usando il flusso di trasformazione csv-parse. Poiché la registrazione avviene ogni volta che viene ricevuto un pezzo dal flusso, i dati sembrano essere scaricati anziché essere visualizzati tutti in una volta sola.

In questo passaggio, hai letto i dati in un file CSV e li hai trasformati in array. Successivamente, inserirai i dati da un file CSV nel database.

Passaggio 3 — Inserimento dei Dati nel Database

Insertare i dati da un file CSV nel database usando Node.js ti dà accesso a una vasta libreria di moduli che puoi utilizzare per elaborare, pulire o migliorare i dati prima di inserirli nel database.

In questa sezione, stabilirai una connessione con il database SQLite utilizzando il modulo node-sqlite3. Successivamente, creerai una tabella nel database, copierai il file readCSV.js e lo modificherai per inserire tutti i dati letti dal file CSV nel database.

Crea e apri un file db.js nel tuo editor:

  1. nano db.js

Nel tuo file db.js, aggiungi le seguenti righe per importare i moduli fs e node-sqlite3:

demo_csv/db.js
const fs = require("fs");
const sqlite3 = require("sqlite3").verbose();
const filepath = "./population.db";
...

Nella terza riga, definisci il percorso del database SQLite e lo memorizzi nella variabile filepath. Il file del database non esiste ancora, ma sarà necessario per node-sqlite3 per stabilire una connessione con il database.

Nello stesso file, aggiungi le seguenti righe per connettere Node.js a un database SQLite:

demo_csv/db.js
...
function connectToDatabase() {
  if (fs.existsSync(filepath)) {
    return new sqlite3.Database(filepath);
  } else {
    const db = new sqlite3.Database(filepath, (error) => {
      if (error) {
        return console.error(error.message);
      }
      console.log("Connected to the database successfully");
    });
    return db;
  }
}

Ecco, si definisce una funzione chiamata connectToDatabase() per stabilire una connessione al database. All’interno della funzione, si invoca il metodo existsSync() del modulo fs in un’istruzione if, che controlla se il file del database esiste nella directory del progetto. Se la condizione dell’if si valuta a true, si istanzia la classe Database() di SQLite del modulo node-sqlite3 con il percorso del database. Una volta stabilita la connessione, la funzione restituisce l’oggetto di connessione ed esce.

Tuttavia, se l’istruzione if si valuta a false (se il file del database non esiste), l’esecuzione salterà al blocco else. Nel blocco else, si istanzia la classe Database() con due argomenti: il percorso del file del database e un callback.

Il primo argomento è il percorso del file del database SQLite, che è ./population.db. Il secondo argomento è un callback che verrà invocato automaticamente quando la connessione al database è stata stabilita con successo o se si è verificato un errore. Il callback prende come parametro un oggetto error, che è null se la connessione ha avuto successo. All’interno del callback, l’istruzione if controlla se l’oggetto error è impostato. Se si valuta a true, il callback registra un messaggio di errore e ritorna. Se si valuta a false, si registra un messaggio di successo confermando che la connessione è stata stabilita.

Attualmente, i blocchi if e else stabiliscono l’oggetto di connessione. Passi una callback quando si invoca la classe Database nel blocco else per creare una tabella nel database, ma solo se il file del database non esiste. Se il file del database esiste già, la funzione eseguirà il blocco if, si connetterà al database e restituirà l’oggetto di connessione.

Per creare una tabella se il file del database non esiste, aggiungi il codice evidenziato:

demo_csv/db.js
const fs = require("fs");
const sqlite3 = require("sqlite3").verbose();
const filepath = "./population.db";

function connectToDatabase() {
  if (fs.existsSync(filepath)) {
    return new sqlite3.Database(filepath);
  } else {
    const db = new sqlite3.Database(filepath, (error) => {
      if (error) {
        return console.error(error.message);
      }
      createTable(db);
      console.log("Connected to the database successfully");
    });
    return db;
  }
}

function createTable(db) {
  db.exec(`
  CREATE TABLE migration
  (
    year_month       VARCHAR(10),
    month_of_release VARCHAR(10),
    passenger_type   VARCHAR(50),
    direction        VARCHAR(20),
    sex              VARCHAR(10),
    age              VARCHAR(50),
    estimate         INT
  )
`);
}

module.exports = connectToDatabase();

Adesso la funzione connectToDatabase() invoca la funzione createTable(), che accetta l’oggetto di connessione memorizzato nella variabile db come argomento.

All’esterno della funzione connectToDatabase(), definisci la funzione createTable(), che accetta l’oggetto di connessione db come parametro. Invochi il metodo exec() sull’oggetto di connessione db che prende come argomento un’istruzione SQL. L’istruzione SQL crea una tabella chiamata migration con 7 colonne. I nomi delle colonne corrispondono agli header nel file migration_data.csv.

Infine, invochi la funzione connectToDatabase() ed esporti l’oggetto di connessione restituito dalla funzione in modo che possa essere riutilizzato in altri file.

Salva ed esci dal tuo file db.js.

Con la connessione al database stabilita, ora copierai e modificherai il file readCSV.js per inserire le righe che il modulo csv-parse ha analizzato nel database.

Copia e rinomina il file in insertData.js con il seguente comando:

  1. cp readCSV.js insertData.js

Apri il file insertData.js nel tuo editor:

  1. nano insertData.js

Aggiungi il codice evidenziato:

demo_csv/insertData.js
const fs = require("fs");
const { parse } = require("csv-parse");
const db = require("./db");

fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    db.serialize(function () {
      db.run(
        `INSERT INTO migration VALUES (?, ?, ? , ?, ?, ?, ?)`,
        [row[0], row[1], row[2], row[3], row[4], row[5], row[6]],
        function (error) {
          if (error) {
            return console.log(error.message);
          }
          console.log(`Inserted a row with the id: ${this.lastID}`);
        }
      );
    });
  });

Nella terza riga, importi l’oggetto di connessione dal file db.js e lo salvi nella variabile db.

All’interno del callback dell’evento data collegato allo stream del modulo fs, invochi il metodo serialize() sull’oggetto di connessione. Il metodo assicura che una dichiarazione SQL termini l’esecuzione prima che un’altra inizi, il che può aiutare a prevenire le condizioni di gara del database in cui il sistema esegue operazioni concorrenti contemporaneamente.

Il metodo serialize() richiede un callback. All’interno del callback, invochi il metodo run sull’oggetto di connessione db. Il metodo accetta tre argomenti:

  • Il primo argomento è una dichiarazione SQL che verrà passata ed eseguita nel database SQLite. Il metodo run() accetta solo dichiarazioni SQL che non restituiscono risultati. La dichiarazione INSERT INTO migration VALUES (?, ..., ?) inserisce una riga nella tabella migration, e i ? sono segnaposto che vengono successivamente sostituiti con i valori nel secondo argomento del metodo run().

  • Il secondo argomento è un array [riga[0], ... riga[5], riga[6]]. Nella sezione precedente, il metodo parse() riceve un blocco di dati dal flusso leggibile e lo trasforma in un array. Poiché i dati vengono ricevuti come un array, per ottenere il valore di ciascun campo, è necessario utilizzare gli indici dell’array per accedervi come [riga[1], ..., riga[6]], ecc.

  • Il terzo argomento è una callback che viene eseguita quando i dati sono stati inseriti o se si è verificato un errore. La callback verifica se si è verificato un errore e registra il messaggio di errore. Se non ci sono errori, la funzione registra un messaggio di successo nella console utilizzando il metodo console.log(), informandoti che è stata inserita una riga insieme all’id.

Infine, rimuovi gli eventi end e error dal tuo file. A causa della natura asincrona dei metodi di node-sqlite3, gli eventi end e error vengono eseguiti prima che i dati vengano inseriti nel database, quindi non sono più necessari.

Salva ed esci dal tuo file.

Esegui il file insertData.js utilizzando node:

  1. node insertData.js

A seconda del tuo sistema, potrebbe richiedere del tempo, ma node dovrebbe restituire l’output qui sotto:

Output
Connected to the database successfully Inserted a row with the id: 1 Inserted a row with the id: 2 ... Inserted a row with the id: 44308 Inserted a row with the id: 44309 Inserted a row with the id: 44310

Il messaggio, specialmente gli ID, dimostra che la riga dal file CSV è stata salvata nel database.

Ora puoi leggere un file CSV e inserire il suo contenuto nel database. Successivamente, scriverai un file CSV.

Passaggio 4 — Scrittura di File CSV

In questa sezione, recupererai i dati dal database e li scriverai in un file CSV utilizzando gli stream.

Crea e apri writeCSV.js nel tuo editor:

  1. nano writeCSV.js

Nel tuo file writeCSV.js, aggiungi le seguenti righe per importare i moduli fs e csv-stringify e l’oggetto di connessione al database da db.js:

demo_csv/writeCSV.js
const fs = require("fs");
const { stringify } = require("csv-stringify");
const db = require("./db");

Il modulo csv-stringify trasforma i dati da un oggetto o un array in un formato testuale CSV.

Successivo, aggiungi le seguenti linee per definire una variabile che contiene il nome del file CSV in cui desideri scrivere i dati e uno stream scrivibile su cui scriverai i dati:

demo_csv/writeCSV.js
...
const filename = "saved_from_db.csv";
const writableStream = fs.createWriteStream(filename);

const columns = [
  "year_month",
  "month_of_release",
  "passenger_type",
  "direction",
  "sex",
  "age",
  "estimate",
];

Il metodo createWriteStream richiede un argomento del nome del file in cui desideri scrivere il tuo flusso di dati, che è il nome del file saved_from_db.csv memorizzato nella variabile filename.

Nella quarta riga, definisci una variabile columns, che memorizza un array contenente i nomi degli header per i dati CSV. Questi header saranno scritti nella prima riga del file CSV quando inizierai a scrivere i dati nel file.

Ancora nel tuo file writeCSV.js, aggiungi le seguenti linee per recuperare i dati dal database e scrivere ogni riga nel file CSV:

demo_csv/writeCSV.js
...
const stringifier = stringify({ header: true, columns: columns });
db.each(`select * from migration`, (error, row) => {
  if (error) {
    return console.log(error.message);
  }
  stringifier.write(row);
});
stringifier.pipe(writableStream);
console.log("Finished writing data");

Prima, invochi il metodo stringify con un oggetto come argomento, che crea uno stream di trasformazione. Lo stream di trasformazione converte i dati da un oggetto in testo CSV. L’oggetto passato al metodo stringify() ha due proprietà:

  • header accetta un valore booleano e genera un’intestazione se il valore booleano è impostato su true.
  • columns prende un array contenente i nomi delle colonne che saranno scritti nella prima riga del file CSV se l’opzione header è impostata su true.

Successivo, si invoca il metodo each() dall’oggetto di connessione db con due argomenti. Il primo argomento è l’istruzione SQL select * from migration che recupera le righe una per una nel database. Il secondo argomento è un callback invocato ogni volta che viene recuperata una riga dal database. Il callback prende due parametri: un oggetto error e un oggetto row contenente i dati recuperati da una singola riga nel database. All’interno del callback, si controlla se l’oggetto error è impostato nell’istruzione if. Se la condizione viene valutata come true, viene registrato un messaggio di errore nella console utilizzando il metodo console.log(). Se non ci sono errori, si invoca il metodo write() su stringifier, che scrive i dati nel flusso di trasformazione stringifier.

Quando il metodo each() finisce di iterare, il metodo pipe() sul flusso stringifier inizia a inviare dati a pezzi e a scriverli nel writableStream. Il flusso scrivibile salverà ogni pezzo di dati nel file saved_from_db.csv. Una volta che tutti i dati sono stati scritti nel file, console.log() registrerà un messaggio di successo.

Il file completo avrà ora il seguente aspetto:

demo_csv/writeCSV.js
const fs = require("fs");
const { stringify } = require("csv-stringify");
const db = require("./db");
const filename = "saved_from_db.csv";
const writableStream = fs.createWriteStream(filename);

const columns = [
  "year_month",
  "month_of_release",
  "passenger_type",
  "direction",
  "sex",
  "age",
  "estimate",
];

const stringifier = stringify({ header: true, columns: columns });
db.each(`select * from migration`, (error, row) => {
  if (error) {
    return console.log(error.message);
  }
  stringifier.write(row);
});
stringifier.pipe(writableStream);
console.log("Finished writing data");

Salvare e chiudere il file, quindi eseguire il file writeCSV.js nel terminale:

  1. node writeCSV.js

Riceverai il seguente output:

Output
Finished writing data

Per confermare che i dati siano stati scritti, ispeziona i contenuti nel file utilizzando il comando cat:

  1. cat saved_from_db.csv

Il comando cat restituirà tutte le righe scritte nel file (modificate per brevità):

Output
year_month,month_of_release,passenger_type,direction,sex,age,estimate 2001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344 2001-01,2020-09,Long-term migrant,Arrivals,Male,0-4 years,341 2001-01,2020-09,Long-term migrant,Arrivals,Female,10-14 years, ...

Ora puoi recuperare i dati dal database e scrivere ogni riga in un file CSV utilizzando gli stream.

Conclusione

In questo articolo, hai letto un file CSV e inserito i suoi dati in un database utilizzando i moduli node-csv e node-sqlite3. Successivamente hai recuperato i dati dal database e li hai scritti in un altro file CSV.

Ora puoi leggere e scrivere file CSV. Come prossimo passo, puoi ora lavorare con grandi set di dati CSV utilizzando la stessa implementazione con stream efficienti in memoria, oppure potresti esaminare un pacchetto come event-stream che rende molto più semplice lavorare con gli stream.

Per esplorare di più su node-csv, visita la loro documentazione Progetto CSV – Pacchetto Node.js CSV. Per saperne di più su node-sqlite3, visita la loro documentazione su Github. Per continuare a sviluppare le tue competenze in Node.js, consulta la serie How To Code in Node.js.

Source:
https://www.digitalocean.com/community/tutorials/how-to-read-and-write-csv-files-in-node-js-using-node-csv