Artigo Build·Desenvolvimento·13 min de leitura

Como Ler CSV em JavaScript e Node.js: Guia Completo

Ler CSV parece trivial, mas casos como vírgulas dentro de campos, aspas escapadas, quebras de linha em valores e encoding Windows-1252 quebram parsers ingênuos. Este guia cobre FileReader, Papaparse, streaming, validação e os casos do mundo real.

Vitor Morais

Por Vitor Morais

Fundador do MochaLabz ·

↔️

Converta CSV para JSON online

Cole seu CSV e obtenha JSON formatado instantaneamente. 100% no navegador.

Usar conversor →

Ler CSV em JavaScript parece trivial: split por vírgula e pronto. Na prática, CSVs reais contêm vírgulas dentro de campos, aspas escapadas, quebras de linha em valores, encoding Windows-1252 em vez de UTF-8 e delimitadores variados (; no Brasil). Este guia cobre as formas corretas de ler CSV em browser e Node.js, quando parsing manual é suficiente (raro) e quando usar biblioteca (quase sempre), com exemplos prontos para casos reais.

O problema de “só fazer split(',')”

Quem começa em CSV faz a implementação ingênua. E quem recebe CSV real descobre rapidamente por que não funciona:

❌ CSV problemático: nome,descricao,preco "Smith, John","Dev senior, Node.js",5000 "Ana","Desc com \"aspas\" e vírgula, tudo junto",3000 ❌ Parsing ingênuo: text.split('\n').map(l => l.split(',')); → Resultado completamente quebrado: ['"Smith', ' John"', '"Dev senior', ' Node.js"', '5000'] Problemas reais do CSV: • Vírgulas dentro de campos entre aspas • Aspas duplas escapadas ("") dentro de string • Quebras de linha \n dentro de campos • Espaços extras, BOM no início • Delimitadores variados (, ; \t |) • Encoding diferente (Windows-1252 vs UTF-8)

RFC 4180: o padrão CSV “oficial”

CSV nunca teve padrão formal até 2005 (RFC 4180). Resumo das regras:

  • Campos separados por vírgula (padrão; na prática varia).
  • Linhas separadas por CRLF (\r\n) — mas LF (\n) é aceito na prática.
  • Campos com vírgula, aspas ou quebra de linha devem ser envolvidos em aspas duplas.
  • Aspas duplas dentro de campo são escapadas duplicando: "Ele disse ""oi""".
  • Primeira linha é opcional como cabeçalho.

Formas de ler CSV em JavaScript

Comparativo de abordagens para ler CSV em JS
CritérioAmbienteRobustezBundle sizeQuando usar
Parser manualBrowser + NodeBaixa0CSV controlado, super-simples
PapaparseBrowser + NodeAlta~45 KBPadrão de fato, cobre 99% dos casos
csv-parseNodeMuito alta~20 KBNode puro, streaming, TypeScript
fast-csvNodeAlta~30 KBPerformance em larga escala
d3-dsvBrowserAlta~10 KBJá usa d3 para outras coisas
Node fs.readFile + splitNodeMuito baixa0NÃO use — quebra em qualquer CSV real

No browser: FileReader API

<input type="file" id="csvFile" accept=".csv,text/csv" /> <script type="module"> const input = document.getElementById('csvFile'); input.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const text = event.target.result; const rows = parseCSV(text); console.log(rows); }; reader.onerror = () => console.error('Erro ao ler arquivo'); // Tente UTF-8 primeiro; se caracteres vierem quebrados, // tente Windows-1252 reader.readAsText(file, 'UTF-8'); }); </script>

Parser manual que respeita aspas

Se você precisa evitar dependência externa (bundle pequeno, browser sem tooling), um parser mínimo mas correto:

/** * Parser CSV simples que respeita aspas e aspas escapadas. * NÃO suporta quebras de linha dentro de campos (para isso, use lib). */ export function parseCSV(text, delimiter = ',') { const lines = text.trim().split(/\r?\n/); const headers = splitCsvLine(lines[0], delimiter).map((h) => h.trim()); return lines.slice(1).map((line, rowIdx) => { const values = splitCsvLine(line, delimiter); return headers.reduce((obj, header, i) => { obj[header] = values[i]?.trim() ?? ''; return obj; }, { _rowIdx: rowIdx }); }); } function splitCsvLine(line, delimiter) { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; const next = line[i + 1]; if (char === '"') { if (inQuotes && next === '"') { // Aspas escapadas ("") viram uma aspa dentro do campo current += '"'; i++; // pula o próximo " } else { inQuotes = !inQuotes; } } else if (char === delimiter && !inQuotes) { result.push(current); current = ''; } else { current += char; } } result.push(current); return result; } // Teste parseCSV(`nome,idade "Smith, John",30 "Ana Paula",28`); // [ // { nome: 'Smith, John', idade: '30' }, // { nome: 'Ana Paula', idade: '28' }, // ]

Limitação do parser manual

O parser acima NÃO suporta quebras de linha dentro de campos entre aspas (RFC 4180 permite). Para CSVs de produção, use biblioteca. Para CSVs simples e controlados, funciona bem.

Papaparse: o padrão para CSV em JS

Papaparse é a biblioteca mais popular. Funciona em browser e Node, é robusta e tem ótima documentação:

Browser — leitura de arquivo

// npm install papaparse // OU CDN: <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js" /> import Papa from 'papaparse'; Papa.parse(file, { header: true, // primeira linha vira keys dos objetos dynamicTyping: true, // converte números e booleans automaticamente skipEmptyLines: true, // ignora linhas em branco encoding: 'UTF-8', // ou 'Windows-1252' para Excel pt-BR delimiter: '', // '' = auto-detecta // delimiter: ';', // forçar ; em CSVs brasileiros complete: (results) => { console.log('Dados:', results.data); // array de objetos console.log('Erros:', results.errors); // array de erros console.log('Meta:', results.meta); // delimitador detectado, linhas, etc. }, error: (error) => { console.error('Falha no parse:', error); }, });

Node.js — string ou stream

import Papa from 'papaparse'; import fs from 'fs'; // Síncrono — CSV pequeno como string const text = fs.readFileSync('dados.csv', 'utf8'); const { data, errors } = Papa.parse(text, { header: true, dynamicTyping: true, }); // Streaming — CSV grande (processamento linha por linha) const stream = fs.createReadStream('grande.csv'); Papa.parse(stream, { header: true, step: (row, parser) => { // Chamado para cada linha processar(row.data); // Parar se necessário if (parouCondicao) parser.abort(); }, complete: () => console.log('Fim do arquivo'), });

csv-parse: alternativa moderna para Node

Para Node puro, csv-parse tem tipagem TypeScript excelente e API streams-first:

// npm install csv-parse import { parse } from 'csv-parse'; import fs from 'fs'; // Simples com callback const text = fs.readFileSync('dados.csv', 'utf8'); parse(text, { columns: true, // primeira linha como keys skip_empty_lines: true, cast: true, // auto-typing cast_date: true, delimiter: ';', // para CSVs brasileiros }, (err, records) => { if (err) return console.error(err); console.log(records); }); // Async iteration (moderno, preferido) import { parse } from 'csv-parse/sync'; const records = parse(text, { columns: true, skip_empty_lines: true, }); // Streaming para arquivos grandes import { parse as parseStream } from 'csv-parse'; const parser = fs.createReadStream('grande.csv').pipe( parseStream({ columns: true, cast: true }) ); for await (const row of parser) { await processar(row); }

Lidando com encoding (Windows-1252 no Excel BR)

Excel em Windows pt-BR exporta frequentemente em Windows-1252 (Latin-1), não UTF-8. Caracteres como ã, ç, é vêm como Ã, ç, é:

// Browser — FileReader com encoding explícito reader.readAsText(file, 'Windows-1252'); // Browser — Papaparse com encoding Papa.parse(file, { encoding: 'Windows-1252', header: true, complete: ..., }); // Node — com iconv-lite import iconv from 'iconv-lite'; import fs from 'fs'; const buffer = fs.readFileSync('dados.csv'); const text = iconv.decode(buffer, 'Windows-1252'); // Detecção automática com chardet import chardet from 'chardet'; const encoding = chardet.detectFile('dados.csv'); // ex: 'ISO-8859-1' ou 'UTF-8'

Detecção de BOM

Se o arquivo começa com \uFEFF (BOM), é provavelmente UTF-8. Se começa com 0xFF 0xFE, é UTF-16 LE. Se primeiros bytes têm caracteres entre 0x80 e 0x9F frequentes, provavelmente Windows-1252. Papaparse remove BOM automaticamente.

Delimitador: vírgula, ponto-e-vírgula ou tab

No Brasil, Excel exporta CSV com ponto-e-vírgula porque usamos vírgula como separador decimal. Outros delimitadores comuns: tab (TSV), pipe (|):

// Papaparse: auto-detecta se delimiter está vazio Papa.parse(text, { header: true }); // Forçar delimitador específico Papa.parse(text, { header: true, delimiter: ';' }); // Detecção manual simples function detectarDelimitador(firstLine) { const candidatos = [',', ';', '\t', '|']; return candidatos.reduce((a, b) => (firstLine.split(b).length > firstLine.split(a).length) ? b : a ); }

Validação de tipos e schema

Papaparse com dynamicTyping: true converte “123” em 123 e “true” em boolean. Mas para validação real, valide cada linha contra schema:

// Com Zod (recomendado) import { z } from 'zod'; const UsuarioSchema = z.object({ nome: z.string().min(1), email: z.string().email(), idade: z.coerce.number().int().min(0).max(150), ativo: z.coerce.boolean(), }); const { data, errors } = Papa.parse(csv, { header: true }); const validos = []; const invalidos = []; for (const [i, row] of data.entries()) { const result = UsuarioSchema.safeParse(row); if (result.success) { validos.push(result.data); } else { invalidos.push({ linha: i + 2, erros: result.error.issues }); } } console.log(`${validos.length} válidos, ${invalidos.length} inválidos`);

Exemplos práticos

Ler CSV de usuários no browser + preview

// React + Papaparse import { useState } from 'react'; import Papa from 'papaparse'; export default function CsvImport() { const [rows, setRows] = useState([]); const [errors, setErrors] = useState([]); const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; Papa.parse<Record<string, string>>(file, { header: true, skipEmptyLines: true, encoding: 'UTF-8', complete: (results) => { setRows(results.data); setErrors(results.errors); }, }); }; return ( <div> <input type="file" accept=".csv" onChange={handleFile} /> <table> <thead> <tr> {Object.keys(rows[0] ?? {}).map((k) => <th key={k}>{k}</th>)} </tr> </thead> <tbody> {rows.slice(0, 10).map((row, i) => ( <tr key={i}> {Object.values(row).map((v, j) => <td key={j}>{v}</td>)} </tr> ))} </tbody> </table> {errors.length > 0 && <p>{errors.length} erros no CSV</p>} </div> ); }

Importar CSV para Postgres em batches

import { parse } from 'csv-parse'; import fs from 'fs'; import { pool } from './db'; const BATCH_SIZE = 1000; let batch = []; async function flushBatch() { if (batch.length === 0) return; const values = batch.map((u) => [u.nome, u.email, u.idade]); await pool.query( 'INSERT INTO usuarios (nome, email, idade) VALUES $1', [values] ); batch = []; } const parser = fs.createReadStream('usuarios.csv').pipe( parse({ columns: true, cast: true }) ); for await (const row of parser) { batch.push(row); if (batch.length >= BATCH_SIZE) await flushBatch(); } await flushBatch(); console.log('Importação concluída');

Converter CSV para JSON

import Papa from 'papaparse'; import fs from 'fs'; const csv = fs.readFileSync('dados.csv', 'utf8'); const { data } = Papa.parse(csv, { header: true, dynamicTyping: true, skipEmptyLines: true, }); fs.writeFileSync('dados.json', JSON.stringify(data, null, 2));

Streaming para arquivos grandes

// CSV de 5 GB — não cabe em memória import { parse } from 'csv-parse'; import fs from 'fs'; let total = 0; let erros = 0; const parser = fs.createReadStream('huge.csv').pipe( parse({ columns: true, skip_records_with_error: true }) ); parser.on('data', (row) => { try { processar(row); total++; } catch { erros++; } }); parser.on('end', () => { console.log(`${total} processadas, ${erros} erros`); }); parser.on('error', (err) => { console.error('Fatal:', err); });

Erros comuns ao ler CSV

Armadilhas frequentes

  • split(',') ingênuo: quebra em qualquer CSV com vírgula dentro de campos.
  • Esquecer encoding: ã vira à em CSVs do Excel pt-BR.
  • Forçar UTF-8 em arquivo Latin-1: byte inválido quebra o parser.
  • Não tratar BOM: \uFEFF inicial aparece como lixo na primeira célula.
  • Delimitador hard-coded: ; em CSV brasileiro, , em americano — precisa detectar ou oferecer opção.
  • Carregar 1 GB em memória: use streaming.
  • Abortar na primeira linha ruim: colete erros e continue processando as válidas.
  • Confiar em dynamicTyping para datas: Papaparse NÃO converte datas — faça manualmente.
  • Não validar schema: CSV pode vir com colunas faltando ou renomeadas.

Checklist da leitura correta de CSV

  • ✅ Biblioteca robusta (Papaparse, csv-parse, fast-csv).
  • ✅ Encoding correto (UTF-8 ou Windows-1252 dependendo da origem).
  • ✅ Delimitador auto-detectado ou configurável.
  • ✅ Suporte a aspas escapadas e quebras de linha em campos.
  • ✅ Schema validado linha por linha (Zod, JSON Schema).
  • ✅ Linhas com erro coletadas, não aborta o processamento.
  • ✅ Streaming para arquivos > 100 MB.
  • ✅ Preview de poucas linhas antes de processar o arquivo inteiro.
  • ✅ Tratamento de BOM (\uFEFF) automático.
  • ✅ Auto-typing para números e booleans; datas convertidas manualmente.

Para o caminho inverso (gerar/exportar CSV) e casos relacionados, veja JSON para CSV em JavaScript e importar CSV para banco de dados. Para decidir entre JSON e CSV, veja JSON vs CSV: quando usar cada um.

Perguntas frequentes

Vale a pena parsear CSV manualmente ou usar biblioteca?+

Para qualquer CSV real de produção, use biblioteca. Papaparse (browser + Node), fast-csv (Node), csv-parse (Node) tratam corretamente: aspas escapadas, vírgulas dentro de campos, quebras de linha em valores, múltiplos delimitadores, encoding. Parsing manual só vale para CSV super-simples e controlado (ex: exportação interna conhecida). Bibliotecas têm anos de testes contra casos reais.

Qual biblioteca é a melhor para CSV em JavaScript?+

Papaparse é o padrão de fato para browser e Node — robusto, bem documentado, streaming, auto-detecção de delimitador, suporte a worker thread. Para Node puro em alta escala, fast-csv ou csv-parse têm performance ligeiramente melhor. Se você precisa de TypeScript first-class com tipagem forte, csv-parse tem tipos excelentes.

Por que CSVs brasileiros usam ponto-e-vírgula?+

Porque usamos vírgula como separador decimal (R$ 1.234,56). Se CSV usasse vírgula como separador de campos também, R$ 1,56 viraria dois campos. A convenção BR é usar ; como delimitador. Excel em pt-BR exporta com ; por padrão. Sempre tente auto-detectar delimitador ou deixe o usuário escolher ao importar.

Como lidar com encoding errado (caracteres estranhos)?+

Excel em Windows pt-BR exporta frequentemente em Windows-1252 (Latin-1), não UTF-8. Ao ler com encoding errado, ã vira Ã, ç vira ç, é vira é. Solução: detectar encoding (via BOM ou chardet), ou dar ao usuário opção de escolher. No FileReader, use reader.readAsText(file, 'Windows-1252'). No Node, use fs.readFile com { encoding: 'latin1' } ou iconv-lite.

Como ler CSV de 1GB+ em Node sem estourar memória?+

Use streaming: fs.createReadStream().pipe(parse({ headers: true })). Processa linha por linha sem carregar tudo em memória. Papaparse tem step callback para streaming. fast-csv tem stream API nativa. Para CSVs gigantes (5GB+), considere processar em chunks e agregar resultados, ou use ferramentas dedicadas (DuckDB, Polars) em vez de JS.

Como validar tipos de CSV (números, datas)?+

Papaparse tem dynamicTyping: true que converte automaticamente "123" em 123, "true" em boolean. Mas para validação real, use JSON Schema ou Zod depois do parse: defina o schema esperado, valide cada linha, colete erros. Não confie que CSV vem "limpo" — sempre valide tipos antes de persistir no banco.

CSV com quebras de linha dentro de campos funciona?+

Sim — CSV (RFC 4180) permite quebras de linha dentro de campos, desde que o campo esteja entre aspas duplas. "Primeira linha\nSegunda linha" é válido. Mas muitos parsers mal implementados quebram. Use sempre biblioteca que respeita RFC 4180 (Papaparse, csv-parse). Parsers ingênuos que fazem split('\n') quebram nesses casos.

Como pular linhas com erro em vez de abortar?+

Papaparse tem error callback por linha (via step) que você pode ignorar. csv-parse tem skip_records_with_error: true. Em produção, prefira: (1) parsear tudo permitindo erros, (2) coletar linhas inválidas em array separado, (3) processar as válidas, (4) retornar relatório com linhas rejeitadas. Não abortar em primeira linha ruim.

#csv#javascript#typescript#node.js#papaparse#csv-parse#filereader#encoding#streaming#rfc 4180

Continue lendo