Cómo leer y escribir archivos CSV en Node.js usando Node-CSV

El autor seleccionó a Sociedad de Ingenieras para recibir una donación como parte del programa Escribe para Donaciones.

Introducción

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.

En este tutorial, utilizarás el módulo node-csv para leer un archivo CSV usando flujos de Node.js, lo que te permite leer grandes conjuntos de datos sin consumir mucha memoria. Modificarás el programa para mover los datos analizados del archivo CSV a una base de datos SQLite. También recuperarás datos de la base de datos, los analizarás con node-csv y usarás flujos de Node.js para escribirlos en un archivo CSV por fragmentos.

Implementa tus aplicaciones de Node desde GitHub utilizando Plataforma de Aplicaciones de DigitalOcean. Deja que DigitalOcean se encargue de escalar tu aplicación.

Prerrequisitos

Para seguir este tutorial, necesitarás:

Paso 1: Configurar el directorio del proyecto

En esta sección, crearás el directorio del proyecto y descargarás paquetes para tu aplicación. También descargarás un conjunto de datos CSV de Stats NZ, que contiene datos de migración internacional en Nueva Zelanda.

Para empezar, crea un directorio llamado csv_demo y navega hasta él:

  1. mkdir csv_demo
  2. cd csv_demo

A continuación, inicializa el directorio como un proyecto npm utilizando el comando npm init:

  1. npm init -y

La opción -y le indica a npm init que responda “sí” a todas las solicitudes. Este comando crea un package.json con valores predeterminados que puedes cambiar en cualquier momento.

Con el directorio inicializado como un proyecto npm, ahora puedes instalar las dependencias necesarias: node-csv y node-sqlite3.

Ingresa el siguiente comando para instalar node-csv:

  1. npm install csv

El módulo node-csv es una colección de módulos que te permite analizar y escribir datos en un archivo CSV. El comando instala los cuatro módulos que forman parte del paquete node-csv: csv-generate, csv-parse, csv-stringify y stream-transform. Utilizarás el módulo csv-parse para analizar un archivo CSV y el módulo csv-stringify para escribir datos en un archivo CSV.

A continuación, instala el módulo node-sqlite3:

  1. npm install sqlite3

El módulo node-sqlite3 permite que tu aplicación interactúe con la base de datos SQLite.

Después de instalar los paquetes en tu proyecto, descarga el archivo CSV de migración de Nueva Zelanda con el 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

El archivo CSV que has descargado tiene un nombre largo. Para facilitar su trabajo, cambia el nombre del archivo a un nombre más corto usando el comando mv:

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

El nuevo nombre del archivo CSV, migration_data.csv, es más corto y más fácil de trabajar.

Usando nano, o tu editor de texto favorito, abre el archivo:

  1. nano migration_data.csv

Una vez abierto, verás un contenido similar a este:

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 primera línea contiene los nombres de las columnas, y todas las líneas subsiguientes tienen los datos correspondientes a cada columna. Una coma separa cada dato. Este carácter se conoce como delimitador porque delimita los campos. No estás limitado a usar comas. Otros delimitadores populares incluyen los dos puntos (:), los puntos y comas (;) y las tabulaciones (\t). Necesitas saber qué delimitador se utiliza en el archivo ya que la mayoría de los módulos lo requieren para analizar los archivos.

Después de revisar el archivo e identificar el delimitador, salga de su archivo migration_data.csv usando CTRL+X.

Ahora ha instalado las dependencias necesarias para su proyecto. En la siguiente sección, leerá un archivo CSV.

Paso 2 — Lectura de Archivos CSV

En esta sección, usará node-csv para leer un archivo CSV y registrar su contenido en la consola. Utilizará el método createReadStream() del módulo fs para leer los datos del archivo CSV y crear un flujo legible. Luego, enviará el flujo a otro flujo inicializado con el módulo csv-parse para analizar los fragmentos de datos. Una vez que los fragmentos de datos hayan sido analizados, puede registrarlos en la consola.

Cree y abra un archivo readCSV.js en su editor preferido:

  1. nano readCSV.js

En su archivo readCSV.js, importe los módulos fs y csv-parse agregando las siguientes líneas:

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

En la primera línea, define la variable fs y le asigna el objeto fs que el método require() de Node.js devuelve al importar el módulo.

En la segunda línea, extraes el método parse del objeto devuelto por el método require() en la variable parse utilizando la sintaxis de desestructuración.

Agrega las siguientes líneas para leer el archivo CSV:

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

El método createReadStream() del módulo fs acepta un argumento con el nombre del archivo que deseas leer, que en este caso es migration_data.csv. Luego, crea un flujo legible, que toma un archivo grande y lo divide en fragmentos más pequeños. Un flujo legible te permite solo leer datos de él y no escribir en él.

Después de crear el flujo legible, el método pipe() de Node envía fragmentos de datos desde el flujo legible a otro flujo. El segundo flujo se crea cuando se invoca el método parse() del módulo csv-parse dentro del método pipe(). El módulo csv-parse implementa un flujo de transformación (un flujo legible y escribible), que toma un fragmento de datos y lo transforma en otra forma. Por ejemplo, cuando recibe un fragmento como 2001-01,2020-09,Migrante a largo plazo,Llegadas,Femenino,0-4 años,344, el método parse() lo transformará en un array.

El método parse() recibe un objeto que acepta propiedades. Luego, el objeto configura y proporciona más información sobre los datos que el método analizará. El objeto toma las siguientes propiedades:

  • delimiter define el carácter que separa cada campo en la fila. El valor , indica al analizador que las comas delimitan los campos.

  • from_line define la línea donde el analizador debe comenzar a analizar las filas. Con el valor 2, el analizador omitirá la línea 1 y comenzará en la línea 2. Como insertarás los datos en la base de datos más tarde, esta propiedad te ayuda a evitar la inserción de los nombres de columna en la primera fila de la base de datos.

A continuación, adjuntas un evento de transmisión utilizando el método on() de Node.js. Un evento de transmisión permite que el método consuma un fragmento de datos si se emite cierto evento. El evento data se desencadena cuando los datos transformados del método parse() están listos para ser consumidos. Para acceder a los datos, pasas un callback al método on(), que toma un parámetro llamado row. El parámetro row es un fragmento de datos transformado en un array. Dentro del callback, registras los datos en la consola utilizando el método console.log().

Antes de ejecutar el archivo, agregarás más eventos de flujo. Estos eventos de flujo manejan errores y escriben un mensaje de éxito en la consola cuando se haya consumido todos los datos en el archivo CSV.

Todavía en tu archivo readCSV.js, agrega el código resaltado:

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);
  });

El evento end se emite cuando todos los datos en el archivo CSV han sido leídos. Cuando esto sucede, se invoca al callback y se registra un mensaje que indica que ha terminado.

Si ocurre un error en cualquier parte mientras se lee y se analizan los datos CSV, se emite el evento error, que invoca al callback y registra el mensaje de error en la consola.

El archivo completo debería lucir ahora como el siguiente:

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);
  });

Guarda y sal del archivo readCSV.js usando CTRL+X.

A continuación, ejecuta el archivo usando el comando node:

  1. node readCSV.js

La salida se verá similar a esto (editado por brevedad):

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

Todas las filas en el archivo CSV se han transformado en arrays usando el flujo de transformación csv-parse. Debido a que el registro ocurre cada vez que se recibe un fragmento del flujo, los datos parecen estar siendo descargados en lugar de mostrarse todos a la vez.

En este paso, leíste datos en un archivo CSV y los transformaste en arrays. A continuación, insertarás datos desde un archivo CSV en la base de datos.

Paso 3 — Inserción de datos en la base de datos

Insertar datos desde un archivo CSV en la base de datos utilizando Node.js te da acceso a una amplia biblioteca de módulos que puedes usar para procesar, limpiar o mejorar los datos antes de insertarlos en la base de datos.

En esta sección, establecerás una conexión con la base de datos SQLite utilizando el módulo node-sqlite3. Luego crearás una tabla en la base de datos, copiarás el archivo readCSV.js, y lo modificarás para insertar todos los datos leídos del archivo CSV en la base de datos.

Crea y abre un archivo db.js en tu editor:

  1. nano db.js

En tu archivo db.js, agrega las siguientes líneas para importar los módulos fs y node-sqlite3:

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

En la tercera línea, defines la ruta de la base de datos SQLite y la almacenas en la variable filepath. El archivo de la base de datos aún no existe, pero será necesario para que node-sqlite3 establezca una conexión con la base de datos.

En el mismo archivo, agrega las siguientes líneas para conectar Node.js a una base de datos 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;
  }
}

Aquí defines una función llamada connectToDatabase() para establecer una conexión a la base de datos. Dentro de la función, invocas el método existsSync() del módulo fs en una declaración if, que verifica si el archivo de la base de datos existe en el directorio del proyecto. Si la condición del if se evalúa como true, instancias la clase Database() de SQLite del módulo node-sqlite3 con la ruta del archivo de la base de datos. Una vez que se establece la conexión, la función devuelve el objeto de conexión y sale.

Sin embargo, si la declaración if se evalúa como false (si el archivo de la base de datos no existe), la ejecución saltará al bloque else. En el bloque else, instancias la clase Database() con dos argumentos: la ruta del archivo de la base de datos y un callback.

El primer argumento es la ruta del archivo de la base de datos SQLite, que es ./population.db. El segundo argumento es un callback que se invocará automáticamente cuando la conexión con la base de datos se haya establecido correctamente o si ocurrió un error. El callback toma un objeto error como parámetro, que es null si la conexión es exitosa. Dentro del callback, la declaración if verifica si el objeto error está establecido. Si se evalúa como true, el callback registra un mensaje de error y retorna. Si se evalúa como false, registras un mensaje de éxito confirmando que se ha establecido la conexión.

Actualmente, los bloques if y else establecen el objeto de conexión. Pasas un callback al invocar la clase Database en el bloque else para crear una tabla en la base de datos, pero solo si el archivo de la base de datos no existe. Si el archivo de la base de datos ya existe, la función ejecutará el bloque if, se conectará con la base de datos y devolverá el objeto de conexión.

Para crear una tabla si el archivo de la base de datos no existe, agrega el código resaltado:

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();

Ahora, la función connectToDatabase() invoca la función createTable(), que acepta el objeto de conexión almacenado en la variable db como argumento.

Fuera de la función connectToDatabase(), defines la función createTable(), que acepta el objeto de conexión db como parámetro. Invocas el método exec() en el objeto de conexión db que toma una declaración SQL como argumento. La declaración SQL crea una tabla llamada migration con 7 columnas. Los nombres de las columnas coinciden con los encabezados en el archivo migration_data.csv.

Finalmente, invocas la función connectToDatabase() y exportas el objeto de conexión devuelto por la función para que pueda ser reutilizado en otros archivos.

Guarda y cierra tu archivo db.js.

Con la conexión a la base de datos establecida, ahora copiarás y modificarás el archivo readCSV.js para insertar las filas que el módulo csv-parse analizó en la base de datos.

Copia y renombra el archivo a insertData.js con el siguiente comando:

  1. cp readCSV.js insertData.js

Abre el archivo insertData.js en tu editor:

  1. nano insertData.js

Agrega el código resaltado:

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}`);
        }
      );
    });
  });

En la tercera línea, importas el objeto de conexión desde el archivo db.js y lo almacenas en la variable db.

Dentro del callback del evento data adjunto al flujo del módulo fs, invocas el método serialize() en el objeto de conexión. El método asegura que una declaración SQL finalice su ejecución antes de que comience otra, lo que puede ayudar a prevenir condiciones de carrera en la base de datos donde el sistema ejecuta operaciones competidoras simultáneamente.

El método serialize() toma un callback. Dentro del callback, invocas el método run en el objeto de conexión db. El método acepta tres argumentos:

  • El primer argumento es una declaración SQL que se pasará y ejecutará en la base de datos SQLite. El método run() solo acepta declaraciones SQL que no devuelven resultados. La declaración INSERT INTO migration VALUES (?, ..., ?) inserta una fila en la tabla migration, y los ? son marcadores de posición que luego se sustituyen con los valores en el segundo argumento del método run().

  • El segundo argumento es un array [fila[0], ... fila[5], fila[6]]. En la sección anterior, el método parse() recibe un fragmento de datos del flujo legible y lo transforma en un array. Dado que los datos se reciben como un array, para obtener el valor de cada campo, debes usar los índices del array para acceder a ellos como [fila[1], ..., fila[6]], etc.

  • El tercer argumento es una devolución de llamada que se ejecuta cuando los datos han sido insertados o si ocurrió un error. La devolución de llamada verifica si ocurrió un error y registra el mensaje de error. Si no hay errores, la función registra un mensaje de éxito en la consola utilizando el método console.log(), informándote que se ha insertado una fila junto con el id.

Finalmente, elimine los eventos end y error de su archivo. Debido a la naturaleza asincrónica de los métodos de node-sqlite3, los eventos end y error se ejecutan antes de que los datos se inserten en la base de datos, por lo que ya no son necesarios.

Guarde y salga de su archivo.

Ejecute el archivo insertData.js usando node:

  1. node insertData.js

Dependiendo de su sistema, puede tomar algún tiempo, pero node debería devolver la salida a continuación:

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

El mensaje, especialmente los ids, demuestra que la fila del archivo CSV ha sido guardada en la base de datos.

Ahora puede leer un archivo CSV e insertar su contenido en la base de datos. A continuación, escribirá un archivo CSV.

Paso 4 — Escribiendo Archivos CSV

En esta sección, recuperará datos de la base de datos y los escribirá en un archivo CSV utilizando streams.

Cree y abra writeCSV.js en su editor:

  1. nano writeCSV.js

En su archivo writeCSV.js, agregue las siguientes líneas para importar los módulos fs y csv-stringify y el objeto de conexión a la base de datos desde db.js:

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

El módulo csv-stringify transforma datos de un objeto o array en un formato de texto CSV.

A continuación, agregue las siguientes líneas para definir una variable que contenga el nombre del archivo CSV al que desea escribir datos y un flujo de escritura al que escribirá datos:

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",
];

El método createWriteStream toma un argumento del nombre de archivo al que desea escribir su flujo de datos, que es el nombre de archivo saved_from_db.csv almacenado en la variable filename.

En la cuarta línea, se define una variable columns, que almacena una matriz que contiene los nombres de los encabezados para los datos CSV. Estos encabezados se escribirán en la primera línea del archivo CSV cuando comience a escribir los datos en el archivo.

Todavía en su archivo writeCSV.js, agregue las siguientes líneas para recuperar datos de la base de datos y escribir cada fila en el archivo 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");

Primero, se invoca el método stringify con un objeto como argumento, que crea un flujo de transformación. El flujo de transformación convierte los datos de un objeto en texto CSV. El objeto pasado al método stringify() tiene dos propiedades:

  • header acepta un valor booleano y genera un encabezado si el valor booleano está configurado en true.
  • columns toma una matriz que contiene los nombres de las columnas que se escribirán en la primera línea del archivo CSV si la opción header está configurada en true.

A continuación, invocas el método each() desde el objeto de conexión db con dos argumentos. El primer argumento es la declaración SQL select * from migration que recupera las filas una por una en la base de datos. El segundo argumento es un callback invocado cada vez que se recupera una fila de la base de datos. El callback toma dos parámetros: un objeto error y un objeto row que contiene datos recuperados de una sola fila en la base de datos. Dentro del callback, se verifica si el objeto error está configurado en la declaración if. Si la condición se evalúa como true, se registra un mensaje de error en la consola usando el método console.log(). Si no hay error, se invoca el método write() en stringifier, que escribe los datos en el flujo de transformación stringifier.

Cuando el método each() termina de iterar, el método pipe() en el flujo stringifier comienza a enviar datos en trozos y a escribirlo en el writableStream. El flujo escribible guardará cada trozo de datos en el archivo saved_from_db.csv. Una vez que todos los datos hayan sido escritos en el archivo, console.log() registrará un mensaje de éxito.

El archivo completo ahora se verá así:

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");

Guarda y cierra tu archivo, luego ejecuta el archivo writeCSV.js en la terminal:

  1. node writeCSV.js

Recibirás la siguiente salida:

Output
Finished writing data

Para confirmar que los datos han sido escritos, inspecciona el contenido del archivo usando el comando cat:

  1. cat saved_from_db.csv

cat devolverá todas las filas escritas en el archivo (editado por brevedad):

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, ...

Ahora puedes recuperar datos de la base de datos y escribir cada fila en un archivo CSV utilizando flujos.

Conclusión

En este artículo, leíste un archivo CSV e insertaste sus datos en una base de datos utilizando los módulos node-csv y node-sqlite3. Luego recuperaste datos de la base de datos y los escribiste en otro archivo CSV.

Ahora puedes leer y escribir archivos CSV. Como próximo paso, ahora puedes trabajar con grandes conjuntos de datos CSV utilizando la misma implementación con flujos eficientes en memoria, o podrías investigar un paquete como event-stream que facilita mucho trabajar con flujos.

Para explorar más sobre node-csv, visita su documentación Proyecto CSV – Paquete CSV de Node.js. Para aprender más sobre node-sqlite3, visita su documentación en Github. Para seguir desarrollando tus habilidades en Node.js, consulta la serie Cómo Codificar en Node.js.

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