
Lendo um arquivo CSV em um array
Simplificando, um arquivo CSV (Comma-Separated Values - Valores Separados por Vírgula) é um formato de arquivo de texto
que utiliza vírgulas para separar valores individuais. Neste tutorial, demonstrarei como utilizar Streams e Node.js para
ler arquivos e convertê-los em arrays bidimensionais de strings (Array<Array<string>>). Utilizaremos como exemplo o
arquivo de Pokémon.
1. Lendo um arquivo CSV
No Node.js, o módulo fs oferece duas abordagens para interagir com arquivos: uma síncrona e outra assíncrona, através
de fs e fs/promises, respectivamente. A primeira segue o modelo das funções POSIX padrão, enquanto a segunda oferece
uma abordagem baseada em promessas. É crucial observar que essas operações não são sincronizadas ou "thread-safe",
exigindo cautela com operações concorrentes.
1.1 Utilizando ReadStream para leitura de arquivos CSV
O ReadStream, uma implementação específica de Stream no Node.js, simplifica a manipulação de dados em streaming. Ele permite a leitura de dados de um arquivo de forma eficiente. Vejamos um exemplo prático de como utilizá-lo:
import { createReadStream } from "fs";
const csvData = await createReadStream(pathToFile, {
encoding: "utf-8",
}).toArray();Esta abordagem gera um array contendo todo o arquivo em uma única linha. Embora simples, não é a mais eficiente para
arquivos grandes, pois carrega todo o conteúdo na memória. Para casos de uso onde isso não representa um problema, é
possível dividir o arquivo em um array de linhas utilizando o método split:
import { createReadStream } from "fs";
const csvData: string[] = await createReadStream(pathToFile, {
encoding: "utf-8",
}).toArray();
const rows = csvData.flatMap((txt) => txt.split(NEW_LINE)).map((line) => line.split(COMMA_DELIMITER));Para processar o arquivo em partes, ou "chunks", empregamos a classe Transform. Esta classe permite a conversão de
chunks de dados do ReadStream em um formato específico. No nosso contexto, transformaremos o buffer em um array de
linhas e, subsequentemente, cada linha em um array de colunas:
import { Transform, TransformCallback } from "stream";
class LineTransform extends Transform {
#lastLine = "";
_transform(chunk: Buffer, encoding: BufferEncoding, callback: TransformCallback) {
let chunkAsString = (this.#lastLine + chunk.toString()).split(NEW_LINE);
this.#lastLine = chunkAsString.pop() || "";
const lines = chunkAsString.map((line) => line.split(COMMA_DELIMITER));
this.push(lines, encoding);
callback();
}
_flush(callback: TransformCallback): void {
if (this.#lastLine.length) {
this.push([this.#lastLine.split(COMMA_DELIMITER)]);
}
callback();
}
}O método _transform manipula um chunk de dados, convertendo-o em um array de linhas antes de encaminhá-lo ao próximo
stream, enquanto _flush é chamado ao final da leitura, processando qualquer dado remanescente. É importante considerar
que um chunk pode terminar no meio de uma linha, exigindo uma gestão cuidadosa para preservar a integridade dos dados.
Integrando o ReadStream com o LineTransform, é possível ler o arquivo em chunks eficientemente. Destacam-se algumas
configurações específicas: habilitar o objectMode para processamento de objetos, ajustar o highWaterMark para
controlar o tamanho dos chunks e utilizar o método iterator para iterar sobre os chunks processados.
const csvToRow = new LineTransform({
objectMode: true,
});
const readFileWithReadStream = async (fileFolder: string) => {
const pathToFile = join(process.cwd(), fileFolder);
const readStreamTransform = createReadStream(pathToFile, {
encoding: "utf-8",
highWaterMark: 512,
}).pipe(csvToRow);
const streamIterator = readStreamTransform.iterator();
for await (let chunk of streamIterator) {
// process chunk
}
};Espero que este guia tenha sido esclarecedor e útil para suas aplicações em Node.js. Este tutorial ilustra apenas uma faceta das possibilidades oferecidas pela manipulação de arquivos em Node.js, antecipando técnicas mais avançadas e otimizações que serão abordadas em futuros conteúdos.
No Node.js, o módulo fs oferece duas formas de interagir com arquivos: síncrona e assíncrona, através de
fs e fs/promises. O primeiro adota o modelo das funções POSIX padrão, enquanto o segundo proporciona uma
abordagem assíncrona baseada em promises. É importante notar que as operações não são sincronizadas ou
'thread-safe', exigindo cuidado com operações concorrentes.
1.1 Lendo um arquivo CSV usando readStream
O ReadStream é uma implementação de Stream no Node.js que facilita o trabalho com streaming de dados. Streams podem ser de leitura, escrita ou duplex. O ReadStream permite a leitura de dados de um arquivo. Eis um exemplo de uso para a leitura de arquivos:
import { createReadStream } from "fs";
const csvData = await createReadStream(pathToFile, {
encoding: "utf-8",
}).toArray();Esta operação resulta em um array contendo o arquivo inteiro em uma única linha. Apesar de ser uma abordagem simples,
ela não é eficiente para arquivos grandes, devido ao carregamento de todo o conteúdo na memória. Se isso não for um
problema para o seu caso de uso, você pode dividir o arquivo em um array de linhas usando o método split:
import { createReadStream } from "fs";
const csvData: string[] = await createReadStream(pathToFile, {
encoding: "utf-8",
}).toArray();
const rows = csvData.flatMap((txt) => txt.split(NEW_LINE)).map((line) => line.split(COMMA_DELIMITER));Para processar o arquivo em partes, ou chunks, utilizamos a classe Transform. A classe Transform recebe chunks de
dados do ReadStream e os converte em um formato específico. No nosso caso, transformaremos o buffer em um array de
linhas e cada linha em um array de colunas:
import { Transform, TransformCallback } from "stream";
class LineTransform extends Transform {
#lastLine = "";
_transform(chunk: Buffer, encoding: BufferEncoding, callback: TransformCallback) {
let chunkAsString = (this.#lastLine + chunk.toString()).split(NEW_LINE);
this.#lastLine = chunkAsString.pop() || "";
const lines = chunkAsString.map((line) => {
return line.split(COMMA_DELIMITER);
});
this.push(lines, encoding);
callback();
}
_flush(callback: TransformCallback): void {
if (this.#lastLine.length) {
this.push([this.#lastLine.split(COMMA_DELIMITER)]);
}
callback();
}
}Basicamente, o método _transform recebe um chunk de dados, transforma em um array de linhas e o envia para o próximo
stream. O método _flush é chamado quando não há mais dados para serem lidos, e envia o último chunk. Um detalhe
importante é que o chunk pode ser cortado no meio de uma linha. Por isso, suponho que a última linha de cada chunk possa
estar incompleta e, consequentemente, precisa ser concatenada com o início do próximo chunk para garantir a integridade
dos dados. Para lidar com isso, utilizo a propriedade #lastLine para armazenar o final do chunk atual, que será então
combinado com o início do chunk seguinte.
Agora, combinando o ReadStream com o LineTransform, consigo ler o arquivo em chunks. Há alguns detalhes que gostaria de
ressaltar: na linha 2, defino o objectMode como true, o que indica que o stream processará objetos. Na linha 9,
altero o valor do highWaterMark para 512 bytes. Isso significa que o stream receberá chunks de 512 bytes,
diferentemente do valor padrão que é 16KB. Por fim, na linha 12, emprego o método iterator para iterar sobre os chunks
recebidos.
const csvToRow = new LineTransform({
objectMode: true,
});
const readFileWithReadStream = async (fileFolder: string) => {
const pathToFile = join(process.cwd(), fileFolder);
const readStreamTransform = createReadStream(pathToFile, {
encoding: "utf-8",
highWaterMark: 512,
}).pipe(csvToRow);
const streamIterator = readStreamTransform.iterator();
for await (let chunk of streamIterator) {
// process chunk
}
};Espero que este tutorial tenha sido esclarecedor e útil em suas aplicações de Node.js.