Como ler e escrever arquivos CSV no Node.js usando Node-CSV

O autor selecionou a Sociedade de Engenheiras para receber uma doação como parte do programa Escreva para Doações.

Introdução

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.

Neste tutorial, você usará o módulo node-csv para ler um arquivo CSV usando fluxos no Node.js, o que permite ler grandes conjuntos de dados sem consumir muita memória. Você modificará o programa para mover dados analisados do arquivo CSV para um banco de dados SQLite. Você também recuperará dados do banco de dados, analisará com node-csv e usará fluxos no Node.js para escrevê-los em um arquivo CSV em partes.

Implante suas aplicações Node.js do GitHub usando Plataforma de Aplicativos da DigitalOcean. Deixe a DigitalOcean cuidar da escalabilidade do seu aplicativo.

Pré-requisitos

Para seguir este tutorial, você precisará:

Passo 1 — Configurando o Diretório do Projeto

Nesta seção, você irá criar o diretório do projeto e baixar pacotes para sua aplicação. Você também irá baixar um conjunto de dados CSV do Stats NZ, que contém dados de migração internacional na Nova Zelândia.

Para começar, crie um diretório chamado csv_demo e navegue até o diretório:

  1. mkdir csv_demo
  2. cd csv_demo

Em seguida, inicialize o diretório como um projeto npm usando o comando npm init:

  1. npm init -y

A opção -y notifica o npm init para responder “sim” a todas as solicitações. Este comando cria um package.json com valores padrão que você pode alterar a qualquer momento.

Com o diretório inicializado como um projeto npm, agora você pode instalar as dependências necessárias: node-csv e node-sqlite3.

Digite o seguinte comando para instalar o node-csv:

  1. npm install csv

O módulo node-csv é uma coleção de módulos que permite analisar e escrever dados em um arquivo CSV. O comando instala todos os quatro módulos que fazem parte do pacote node-csv: csv-generate, csv-parse, csv-stringify e stream-transform. Você usará o módulo csv-parse para analisar um arquivo CSV e o módulo csv-stringify para escrever dados em um arquivo CSV.

Em seguida, instale o módulo node-sqlite3:

  1. npm install sqlite3

O módulo node-sqlite3 permite que seu aplicativo interaja com o banco de dados SQLite.

Após instalar os pacotes em seu projeto, baixe o arquivo CSV de migração da Nova Zelândia com o 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

O arquivo CSV que você baixou tem um nome longo. Para facilitar o trabalho com ele, renomeie o nome do arquivo para um nome mais curto usando o comando mv:

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

O novo nome do arquivo CSV, migration_data.csv, é mais curto e fácil de trabalhar.

Usando o nano, ou seu editor de texto favorito, abra o arquivo:

  1. nano migration_data.csv

Uma vez aberto, você verá conteúdos semelhantes a estes:

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

A primeira linha contém os nomes das colunas, e todas as linhas subsequentes têm os dados correspondentes a cada coluna. Uma vírgula separa cada pedaço de dados. Este caractere é conhecido como delimitador porque delimita os campos. Você não está limitado a usar vírgulas. Outros delimitadores populares incluem dois pontos(:), ponto e vírgula(;) e tabulações(\t). Você precisa saber qual delimitador é usado no arquivo, já que a maioria dos módulos o requer para analisar os arquivos.

Depois de revisar o arquivo e identificar o delimitador, saia do seu arquivo migration_data.csv usando CTRL+X.

Você agora instalou as dependências necessárias para o seu projeto. Na próxima seção, você irá ler um arquivo CSV.

Passo 2 — Lendo Arquivos CSV

Nesta seção, você usará o node-csv para ler um arquivo CSV e registrar seu conteúdo no console. Você usará o método createReadStream() do módulo fs para ler os dados do arquivo CSV e criar um fluxo de leitura. Em seguida, você encaminhará o fluxo para outro fluxo inicializado com o módulo csv-parse para analisar os pedaços de dados. Assim que os pedaços de dados forem analisados, você poderá registrá-los no console.

Crie e abra um arquivo readCSV.js no seu editor preferido:

  1. nano readCSV.js

No seu arquivo readCSV.js, importe os módulos fs e csv-parse adicionando as seguintes linhas:

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

Na primeira linha, você define a variável fs e atribui a ela o objeto fs que o método require() do Node.js retorna ao importar o módulo.

Na segunda linha, você extrai o método parse do objeto retornado pelo método require() para a variável parse usando a sintaxe de destruturação.

Adicione as seguintes linhas para ler o arquivo CSV:

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

O método createReadStream() do módulo fs aceita um argumento do nome do arquivo que você deseja ler, que é migration_data.csv aqui. Em seguida, ele cria um fluxo legível, que divide um arquivo grande em pedaços menores. Um fluxo legível permite que você apenas leia dados dele e não escreva nele.

Após criar o fluxo legível, o método pipe() do Node encaminha pedaços de dados do fluxo legível para outro fluxo. O segundo fluxo é criado quando o método parse() do módulo csv-parse é invocado dentro do método pipe(). O módulo csv-parse implementa um fluxo de transformação (um fluxo legível e gravável), que recebe um pedaço de dados e o transforma em outra forma. Por exemplo, quando ele recebe um pedaço como 2001-01,2020-09,Migrante de longo prazo,Chegadas,Feminino,0-4 anos,344, o método parse() o transformará em um array.

O método parse() recebe um objeto que aceita propriedades. O objeto então configura e fornece mais informações sobre os dados que o método irá analisar. O objeto possui as seguintes propriedades:

  • delimiter define o caractere que separa cada campo na linha. O valor , indica ao analisador que vírgulas demarcam os campos.

  • from_line define a linha onde o analisador deve começar a analisar as linhas. Com o valor 2, o analisador irá pular a linha 1 e começar na linha 2. Como você irá inserir os dados no banco de dados posteriormente, esta propriedade ajuda a evitar a inserção dos nomes das colunas na primeira linha do banco de dados.

Em seguida, você anexa um evento de streaming usando o método on() do Node.js. Um evento de streaming permite que o método consuma um pedaço de dados se um determinado evento for emitido. O evento data é acionado quando os dados transformados do método parse() estão prontos para serem consumidos. Para acessar os dados, você passa um retorno de chamada para o método on(), que recebe um parâmetro chamado row. O parâmetro row é um pedaço de dados transformado em uma matriz. Dentro do retorno de chamada, você registra os dados no console usando o método console.log().

Antes de executar o arquivo, você irá adicionar mais eventos de fluxo. Esses eventos de fluxo lidam com erros e escrevem uma mensagem de sucesso no console quando todos os dados no arquivo CSV forem consumidos.

Ainda no seu arquivo readCSV.js, adicione o código destacado:

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

O evento end é emitido quando todos os dados no arquivo CSV foram lidos. Quando isso acontece, a função de retorno é invocada e registra uma mensagem que indica que terminou.

Se ocorrer um erro em qualquer lugar durante a leitura e análise dos dados CSV, o evento error é emitido, o que invoca a função de retorno e registra a mensagem de erro no console.

Seu arquivo completo agora deve se parecer com o seguinte:

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

Salve e saia do seu arquivo readCSV.js usando CTRL+X.

Em seguida, execute o arquivo usando o comando node:

  1. node readCSV.js

A saída será semelhante a isto (editada por brevidade):

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

Todas as linhas no arquivo CSV foram transformadas em arrays usando o fluxo de transformação csv-parse. Como o registro ocorre cada vez que um fragmento é recebido do fluxo, os dados parecem estar sendo baixados em vez de serem exibidos de uma vez.

Neste passo, você leu dados em um arquivo CSV e os transformou em arrays. Em seguida, você irá inserir dados de um arquivo CSV no banco de dados.

Passo 3 — Inserindo Dados no Banco de Dados

Inserir dados de um arquivo CSV no banco de dados usando o Node.js dá acesso a uma vasta biblioteca de módulos que você pode usar para processar, limpar ou aprimorar os dados antes de inseri-los no banco de dados.

Nesta seção, você estabelecerá uma conexão com o banco de dados SQLite usando o módulo node-sqlite3. Em seguida, você criará uma tabela no banco de dados, copiará o arquivo readCSV.js e o modificará para inserir todos os dados lidos do arquivo CSV no banco de dados.

Crie e abra um arquivo db.js no seu editor:

  1. nano db.js

No seu arquivo db.js, adicione as seguintes linhas para importar os módulos fs e node-sqlite3:

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

Na terceira linha, você define o caminho do banco de dados SQLite e o armazena na variável filepath. O arquivo do banco de dados ainda não existe, mas será necessário para que o node-sqlite3 estabeleça uma conexão com o banco de dados.

No mesmo arquivo, adicione as seguintes linhas para conectar o Node.js a um banco de dados 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;
  }
}

Aqui, você define uma função chamada connectToDatabase() para estabelecer uma conexão com o banco de dados. Dentro da função, você invoca o método existsSync() do módulo fs em uma declaração if, que verifica se o arquivo do banco de dados existe no diretório do projeto. Se a condição if avaliar como true, você instancia a classe Database() do SQLite do módulo node-sqlite3 com o caminho do banco de dados. Uma vez que a conexão é estabelecida, a função retorna o objeto de conexão e sai.

No entanto, se a declaração if avaliar como false (se o arquivo do banco de dados não existir), a execução irá pular para o bloco else. No bloco else, você instancia a classe Database() com dois argumentos: o caminho do arquivo do banco de dados e um retorno de chamada.

O primeiro argumento é o caminho do arquivo do banco de dados SQLite, que é ./population.db. O segundo argumento é um retorno de chamada que será invocado automaticamente quando a conexão com o banco de dados for estabelecida com sucesso ou se ocorrer um erro. O retorno de chamada recebe um objeto de error como parâmetro, que é null se a conexão for bem-sucedida. Dentro do retorno de chamada, a declaração if verifica se o objeto de error está definido. Se ele avaliar como true, o retorno de chamada registra uma mensagem de erro e retorna. Se ele avaliar como false, você registra uma mensagem de sucesso confirmando que a conexão foi estabelecida.

Atualmente, os blocos if e else estabelecem o objeto de conexão. Você passa um retorno de chamada ao invocar a classe Database no bloco else para criar uma tabela no banco de dados, mas apenas se o arquivo do banco de dados não existir. Se o arquivo do banco de dados já existir, a função executará o bloco if, conectará-se ao banco de dados e retornará o objeto de conexão.

Para criar uma tabela se o arquivo do banco de dados não existir, adicione o código destacado:

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

Agora, o connectToDatabase() invoca a função createTable(), que aceita o objeto de conexão armazenado na variável db como argumento.

Fora da função connectToDatabase(), você define a função createTable(), que aceita o objeto de conexão db como parâmetro. Você invoca o método exec() no objeto de conexão db que recebe uma instrução SQL como argumento. A instrução SQL cria uma tabela chamada migration com 7 colunas. Os nomes das colunas correspondem aos cabeçalhos no arquivo migration_data.csv.

Por fim, você invoca a função connectToDatabase() e exporta o objeto de conexão retornado pela função para que possa ser reutilizado em outros arquivos.

Salve e saia do seu arquivo db.js.

Com a conexão ao banco de dados estabelecida, agora você irá copiar e modificar o arquivo readCSV.js para inserir as linhas que o módulo csv-parse analisou no banco de dados.

Copie e renomeie o arquivo para insertData.js com o seguinte comando:

  1. cp readCSV.js insertData.js

Abra o arquivo insertData.js no seu editor:

  1. nano insertData.js

Adicione o código destacado:

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

Na terceira linha, você importa o objeto de conexão do arquivo db.js e o armazena na variável db.

Dentro do retorno de chamada do evento data anexado ao fluxo do módulo fs, você invoca o método serialize() no objeto de conexão. O método garante que uma instrução SQL termine de ser executada antes que outra comece a ser executada, o que pode ajudar a evitar condições de corrida no banco de dados, onde o sistema executa operações concorrentes simultaneamente.

O método serialize() recebe um retorno de chamada. Dentro do retorno de chamada, você invoca o método run no objeto de conexão db. O método aceita três argumentos:

  • O primeiro argumento é uma instrução SQL que será passada e executada no banco de dados SQLite. O método run() aceita apenas instruções SQL que não retornam resultados. A instrução INSERT INTO migration VALUES (?, ..., ? insere uma linha na tabela migration, e os ? são espaços reservados que são posteriormente substituídos pelos valores no segundo argumento do método run().

  • O segundo argumento é um array [row[0], ... row[5], row[6]]. Na seção anterior, o método parse() recebe um pedaço de dados do fluxo legível e o transforma em um array. Como os dados são recebidos como um array, para obter o valor de cada campo, você deve usar os índices do array para acessá-los como [row[1], ..., row[6]], etc.

  • O terceiro argumento é um retorno de chamada que é executado quando os dados foram inseridos ou se ocorreu um erro. O retorno de chamada verifica se ocorreu um erro e registra a mensagem de erro. Se não houver erros, a função registra uma mensagem de sucesso no console usando o método console.log(), informando que uma linha foi inserida junto com o ID.

Finalmente, remova os eventos end e error do seu arquivo. Devido à natureza assíncrona dos métodos do node-sqlite3, os eventos end e error são executados antes que os dados sejam inseridos no banco de dados, então eles não são mais necessários.

Salve e saia do seu arquivo.

Execute o arquivo insertData.js usando node:

  1. node insertData.js

Dependendo do seu sistema, pode levar algum tempo, mas o node deve retornar a saída abaixo:

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

A mensagem, especialmente os IDs, comprova que a linha do arquivo CSV foi salva no banco de dados.

Agora você pode ler um arquivo CSV e inserir seu conteúdo no banco de dados. Em seguida, você escreverá um arquivo CSV.

Passo 4 — Escrevendo Arquivos CSV

Nesta seção, você irá recuperar dados do banco de dados e escrevê-los em um arquivo CSV usando streams.

Crie e abra o arquivo writeCSV.js no seu editor:

  1. nano writeCSV.js

No seu arquivo writeCSV.js, adicione as seguintes linhas para importar os módulos fs e csv-stringify e o objeto de conexão do banco de dados de db.js:

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

O módulo csv-stringify transforma dados de um objeto ou array em formato de texto CSV.

Em seguida, adicione as seguintes linhas para definir uma variável que contenha o nome do arquivo CSV no qual você deseja gravar os dados e um fluxo gravável no qual você irá escrever os dados:

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

O método createWriteStream recebe um argumento com o nome do arquivo no qual você deseja gravar o fluxo de dados, que é o nome do arquivo saved_from_db.csv armazenado na variável filename.

Na quarta linha, você define a variável columns, que armazena um array contendo os nomes dos cabeçalhos para os dados CSV. Esses cabeçalhos serão escritos na primeira linha do arquivo CSV quando você começar a escrever os dados no arquivo.

Ainda no seu arquivo writeCSV.js, adicione as seguintes linhas para recuperar dados do banco de dados e escrever cada linha no arquivo 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");

Primeiro, você invoca o método stringify com um objeto como argumento, que cria um fluxo de transformação. O fluxo de transformação converte os dados de um objeto em texto CSV. O objeto passado para o método stringify() possui duas propriedades:

  • header aceita um valor booleano e gera um cabeçalho se o valor booleano estiver definido como true.
  • columns recebe um array contendo os nomes das colunas que serão escritas na primeira linha do arquivo CSV se a opção header estiver definida como true.

Em seguida, você invoca o método each() do objeto de conexão db com dois argumentos. O primeiro argumento é a instrução SQL select * from migration que recupera as linhas uma por uma no banco de dados. O segundo argumento é um retorno de chamada invocado cada vez que uma linha é recuperada do banco de dados. O retorno de chamada recebe dois parâmetros: um objeto error e um objeto row contendo dados recuperados de uma única linha no banco de dados. Dentro do retorno de chamada, você verifica se o objeto error está definido na instrução if. Se a condição for avaliada como true, uma mensagem de erro é registrada no console usando o método console.log(). Se não houver erro, você invoca o método write() em stringifier, que escreve os dados no fluxo de transformação stringifier.

Quando o método each() termina de iterar, o método pipe() no fluxo de stringifier começa a enviar dados em blocos e escrevê-los no writableStream. O fluxo gravável salvará cada bloco de dados no arquivo saved_from_db.csv. Uma vez que todos os dados tenham sido escritos no arquivo, console.log() irá registrar uma mensagem de sucesso.

O arquivo completo agora terá o seguinte aspecto:

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

Salve e feche seu arquivo, em seguida, execute o arquivo writeCSV.js no terminal:

  1. node writeCSV.js

Você receberá a seguinte saída:

Output
Finished writing data

Para confirmar que os dados foram gravados, inspecione o conteúdo no arquivo usando o comando cat:

  1. cat saved_from_db.csv

cat retornará todas as linhas escritas no arquivo (editado por brevidade):

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

Agora você pode recuperar dados do banco de dados e escrever cada linha em um arquivo CSV usando streams.

Conclusão

Neste artigo, você leu um arquivo CSV e inseriu seus dados em um banco de dados usando os módulos node-csv e node-sqlite3. Em seguida, você recuperou dados do banco de dados e os escreveu em outro arquivo CSV.

Agora você pode ler e escrever arquivos CSV. Como próximo passo, você pode trabalhar com grandes conjuntos de dados CSV usando a mesma implementação com streams eficientes em memória, ou você pode procurar por um pacote como event-stream que facilita muito o trabalho com streams.

Para explorar mais sobre node-csv, visite a documentação deles Projeto CSV – Pacote CSV Node.js. Para aprender mais sobre node-sqlite3, visite a documentação deles no Github. Para continuar aprimorando suas habilidades em Node.js, veja a série Como Codificar em Node.js.

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