
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
proporciona 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 volumosos, pois carrega o conteúdo inteiro 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 proveitoso 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.