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.
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
| Critério | Ambiente | Robustez | Bundle size | Quando usar |
|---|---|---|---|---|
| Parser manual | Browser + Node | Baixa | 0 | CSV controlado, super-simples |
| Papaparse | Browser + Node | Alta | ~45 KB | Padrão de fato, cobre 99% dos casos |
| csv-parse | Node | Muito alta | ~20 KB | Node puro, streaming, TypeScript |
| fast-csv | Node | Alta | ~30 KB | Performance em larga escala |
| d3-dsv | Browser | Alta | ~10 KB | Já usa d3 para outras coisas |
| Node fs.readFile + split | Node | Muito baixa | 0 | NÃ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:
\uFEFFinicial 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.
Continue lendo
JSON vs CSV (2026): Quando Usar Cada Formato e Como Converter
Comparativo completo JSON vs CSV: diferenças técnicas, performance, casos de uso, limitações, parquet como alternativa e código de conversão entre os formatos.
Como Converter JSON para CSV em JavaScript (2026): Guia Completo com Código
Guia definitivo de conversão JSON → CSV em JavaScript: abordagens manuais, PapaParse, edge cases com arrays aninhados, encoding UTF-8 com BOM e exemplos prontos em Node.js e browser.
Como Exportar Dados JSON para Excel (2026): Guia Completo com SheetJS
Guia definitivo de exportação de JSON para Excel (.xlsx): SheetJS para JS, OpenPyXL para Python, múltiplas planilhas, formatação, fórmulas e estratégias para arquivos grandes.
Como Importar CSV para Banco de Dados (2026): PostgreSQL, MySQL, SQLite e MongoDB
Guia completo de ingestão de CSV em banco: COPY do Postgres, LOAD DATA do MySQL, SQLite .import, mongoimport, estratégias para arquivos grandes e validação antes do insert.