DevOps

Trabalhando com arquivos CSV Já leu

16 min de leitura

Trabalhando com arquivos CSV
JSON domina as APIs modernas, mas o CSV domina o mundo real dos dados. Planilhas financeiras, exportações de bancos de dados, relatórios de e-commerce, dados de sensores, listas de contatos — tudo isso vive em CSV. Todo

JSON domina as APIs modernas, mas o CSV domina o mundo real dos dados. Planilhas financeiras, exportações de bancos de dados, relatórios de e-commerce, dados de sensores, listas de contatos — tudo isso vive em CSV. Todo desenvolvedor, cedo ou tarde, precisa ler, processar e gerar arquivos .csv.

Neste artigo vamos aprender a trabalhar com CSV do zero, sem bibliotecas externas, e depois ver quando e como usar uma biblioteca para casos complexos.


O que é CSV?

CSVComma-Separated Values — é um formato de texto simples onde cada linha representa um registro e os campos são separados por vírgulas (ou outro delimitador):

nome,email,idade,cidade
Ana Paula,ana@email.com,28,São Paulo
Carlos Silva,carlos@email.com,32,Curitiba
Beatriz Costa,bia@email.com,25,Rio de Janeiro

Simples assim. A primeira linha normalmente é o cabeçalho com os nomes das colunas.


Lendo CSV manualmente — o básico

const csvTexto = `nome,email,idade,cidade
Ana Paula,ana@email.com,28,São Paulo
Carlos Silva,carlos@email.com,32,Curitiba
Beatriz Costa,bia@email.com,25,Rio de Janeiro`;

function parsearCSV(texto) {
  const linhas = texto.trim().split("
");
  const cabecalho = linhas[0].split(",");

  return linhas.slice(1).map(linha => {
    const valores = linha.split(",");
    return cabecalho.reduce((obj, col, i) => {
      obj[col.trim()] = valores[i]?.trim();
      return obj;
    }, {});
  });
}

const dados = parsearCSV(csvTexto);
console.log(dados);
/*
[
  { nome: "Ana Paula", email: "ana@email.com", idade: "28", cidade: "São Paulo" },
  { nome: "Carlos Silva", email: "carlos@email.com", idade: "32", cidade: "Curitiba" },
  { nome: "Beatriz Costa", email: "bia@email.com", idade: "25", cidade: "Rio de Janeiro" }
]
*/

Funciona para casos simples. Mas o mundo real é mais complicado.


O problema das vírgulas dentro dos campos

O CSV tem uma armadilha clássica: e se o campo contiver uma vírgula? A solução padrão é envolver o campo em aspas duplas:

nome,descricao,preco
Notebook Pro,"Processador i7, 16GB RAM, SSD 512GB",3500.00
Mouse Gamer,"Clique preciso, 6 botões",189.90

E se o campo contiver aspas? Elas são escapadas dobrando: "":

produto,observacao
Caneta,"Cor ""azul"" especial"

Isso torna o parse manual muito mais complexo:

function parsearCSVRobusto(texto, delimitador = ",") {
  const linhas = texto.trim().split("
");
  const cabecalho = parsearLinha(linhas[0], delimitador);

  return linhas.slice(1).map(linha => {
    const valores = parsearLinha(linha, delimitador);
    return cabecalho.reduce((obj, col, i) => {
      obj[col.trim()] = valores[i] ?? "";
      return obj;
    }, {});
  });
}

function parsearLinha(linha, delimitador = ",") {
  const resultado = [];
  let campo = "";
  let dentroDeAspas = false;

  for (let i = 0; i < linha.length; i++) {
    const char = linha[i];
    const proximo = linha[i + 1];

    if (char === '"' && !dentroDeAspas) {
      dentroDeAspas = true;
      continue;
    }

    if (char === '"' && dentroDeAspas) {
      if (proximo === '"') {
        // Aspas escapadas "" → uma aspas literal
        campo += '"';
        i++; // pula o próximo "
      } else {
        dentroDeAspas = false;
      }
      continue;
    }

    if (char === delimitador && !dentroDeAspas) {
      resultado.push(campo.trim());
      campo = "";
      continue;
    }

    campo += char;
  }

  resultado.push(campo.trim()); // último campo
  return resultado;
}

// Testando
const csv = `produto,descricao,preco
Notebook,"Processador i7, 16GB RAM",3500.00
Caneta,"Cor ""azul"" especial",2.50
Mouse Gamer,"6 botões, RGB",189.90`;

const dados = parsearCSVRobusto(csv);
console.log(dados[0].descricao); // "Processador i7, 16GB RAM"
console.log(dados[1].descricao); // 'Cor "azul" especial'

Convertendo tipos automaticamente

O CSV é sempre texto puro. Você precisa converter tipos manualmente:

function converterTipos(obj) {
  const resultado = {};

  for (const [chave, valor] of Object.entries(obj)) {
    // Vazio → null
    if (valor === "" || valor === "null" || valor === "NULL") {
      resultado[chave] = null;
      continue;
    }

    // Boolean
    if (valor === "true" || valor === "TRUE") { resultado[chave] = true; continue; }
    if (valor === "false" || valor === "FALSE") { resultado[chave] = false; continue; }

    // Número
    const numero = Number(valor);
    if (!isNaN(numero) && valor !== "") {
      resultado[chave] = numero;
      continue;
    }

    // Data ISO
    const data = new Date(valor);
    if (!isNaN(data.getTime()) && valor.includes("-")) {
      resultado[chave] = data;
      continue;
    }

    // String simples
    resultado[chave] = valor;
  }

  return resultado;
}

// Uso junto com o parser
function parsearCSVComTipos(texto, delimitador = ",") {
  return parsearCSVRobusto(texto, delimitador).map(converterTipos);
}

const csv = `nome,idade,ativo,salario,admissao
Ana,28,true,5000.50,2022-03-15
Bruno,35,false,7200,2019-08-01`;

const dados = parsearCSVComTipos(csv);
console.log(dados[0].idade + 1);          // 29 — número
console.log(dados[0].ativo === true);     // true — boolean
console.log(dados[0].salario + 500);      // 5500.5 — número
console.log(dados[0].admissao.getFullYear()); // 2022 — Date

Gerando CSV a partir de dados

function gerarCSV(dados, opcoes = {}) {
  const {
    delimitador = ",",
    incluirCabecalho = true,
    campos = null, // null = todos os campos
  } = opcoes;

  if (dados.length === 0) return "";

  const colunas = campos || Object.keys(dados[0]);

  function escaparCampo(valor) {
    if (valor === null || valor === undefined) return "";

    const texto = valor instanceof Date
      ? valor.toISOString()
      : String(valor);

    // Envolve em aspas se contém delimitador, aspas ou quebra de linha
    if (
      texto.includes(delimitador) ||
      texto.includes('"') ||
      texto.includes("
")
    ) {
      return `"${texto.replace(/"/g, '""')}"`;
    }

    return texto;
  }

  const linhas = [];

  if (incluirCabecalho) {
    linhas.push(colunas.map(escaparCampo).join(delimitador));
  }

  dados.forEach(item => {
    const linha = colunas.map(col => escaparCampo(item[col]));
    linhas.push(linha.join(delimitador));
  });

  return linhas.join("
");
}

// Testando
const funcionarios = [
  { nome: "Ana", cargo: "Dev", salario: 5000, ativo: true },
  { nome: "Bruno, Jr.", cargo: "Designer", salario: 4000, ativo: false },
  { nome: 'Clara "CB"', cargo: "QA", salario: 4500, ativo: true },
];

const csv = gerarCSV(funcionarios);
console.log(csv);
/*
nome,cargo,salario,ativo
Ana,Dev,5000,true
"Bruno, Jr.",Designer,4000,false
"Clara ""CB""",QA,4500,true
*/

Exportando CSV para download no navegador

function baixarCSV(dados, nomeArquivo = "exportacao.csv", opcoes = {}) {
  const csvTexto = gerarCSV(dados, opcoes);

  // BOM para compatibilidade com Excel no Windows (UTF-8)
  const bom = "uFEFF";
  const blob = new Blob([bom + csvTexto], {
    type: "text/csv;charset=utf-8;",
  });

  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = nomeArquivo;
  link.style.display = "none";

  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);

  // Libera a URL da memória
  setTimeout(() => URL.revokeObjectURL(url), 100);
}

// Uso
const vendas = [
  { mes: "Janeiro", receita: 48000, pedidos: 320, ticket: 150 },
  { mes: "Fevereiro", receita: 52000, pedidos: 347, ticket: 149.9 },
  { mes: "Março", receita: 61000, pedidos: 410, ticket: 148.8 },
];

document.querySelector("#btn-exportar").addEventListener("click", () => {
  baixarCSV(vendas, "relatorio-vendas-2025.csv");
});

Lendo CSV enviado pelo usuário

function lerArquivoCSV(arquivo) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = (evento) => {
      const texto = evento.target.result;
      try {
        const dados = parsearCSVComTipos(texto);
        resolve(dados);
      } catch (erro) {
        reject(new Error(`Erro ao parsear CSV: ${erro.message}`));
      }
    };

    reader.onerror = () => reject(new Error("Erro ao ler o arquivo."));
    reader.readAsText(arquivo, "UTF-8");
  });
}

// No HTML: <input type="file" id="arquivo-csv" accept=".csv">
const inputArquivo = document.querySelector("#arquivo-csv");

inputArquivo.addEventListener("change", async (e) => {
  const arquivo = e.target.files[0];
  if (!arquivo) return;

  if (!arquivo.name.endsWith(".csv")) {
    console.error("Por favor, selecione um arquivo .csv");
    return;
  }

  try {
    const dados = await lerArquivoCSV(arquivo);
    console.log(`${dados.length} registros carregados.`);
    console.log("Primeiro registro:", dados[0]);
    processarDados(dados);
  } catch (erro) {
    console.error(erro.message);
  }
});

Buscando CSV de uma URL com Fetch

async function buscarCSV(url, opcoes = {}) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`Erro ${response.status} ao buscar CSV.`);
  }

  const texto = await response.text();
  return parsearCSVComTipos(texto, opcoes.delimitador);
}

// Exemplo com dados públicos do IBGE (ou qualquer CSV público)
async function carregarDadosPublicos() {
  try {
    const dados = await buscarCSV(
      "https://raw.githubusercontent.com/datasets/population/main/data/population.csv"
    );

    console.log(`${dados.length} registros carregados.`);

    // Filtra só o Brasil
    const brasil = dados.filter(d =>
      d["Country Name"] === "Brazil" || d["Country Code"] === "BRA"
    );

    console.log("Dados do Brasil:", brasil);
  } catch (erro) {
    console.error("Erro:", erro.message);
  }
}

Delimitadores alternativos — TSV e ponto e vírgula

Nem todo "CSV" usa vírgula. O Excel brasileiro gera com ponto e vírgula por padrão (pois a vírgula é separador decimal no Brasil). Arquivos TSV usam tab:

// Detectar delimitador automaticamente
function detectarDelimitador(primeiraLinha) {
  const delimitadores = [",", ";", "	", "|"];
  const contagens = delimitadores.map(d => ({
    delimitador: d,
    contagem: (primeiraLinha.match(new RegExp(`\${d}`, "g")) || []).length,
  }));

  return contagens.sort((a, b) => b.contagem - a.contagem)[0].delimitador;
}

function parsearCSVAuto(texto) {
  const primeiraLinha = texto.split("
")[0];
  const delimitador = detectarDelimitador(primeiraLinha);
  console.log(`Delimitador detectado: "${delimitador === "	" ? "TAB" : delimitador}"`);
  return parsearCSVRobusto(texto, delimitador);
}

// Funciona com qualquer um:
parsearCSVAuto("nome;email;idade
Ana;ana@email.com;28");    // ponto e vírgula
parsearCSVAuto("nome	email	idade
Ana	ana@email.com	28"); // tab
parsearCSVAuto("nome,email,idade
Ana,ana@email.com,28");     // vírgula

Quando usar uma biblioteca — PapaParse

Para casos complexos em produção, a biblioteca PapaParse é o padrão da indústria:

<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
// PapaParse — simples, robusto, rápido

// Parse de string
const resultado = Papa.parse(csvTexto, {
  header: true,           // usa primeira linha como chaves
  dynamicTyping: true,    // converte tipos automaticamente
  skipEmptyLines: true,   // ignora linhas vazias
  delimiter: "",          // auto-detecta
});

console.log(resultado.data);   // array de objetos
console.log(resultado.errors); // erros encontrados
console.log(resultado.meta);   // delimitador usado, encoding, etc.

// Parse de arquivo (streaming — bom para arquivos grandes)
Papa.parse(arquivo, {
  header: true,
  dynamicTyping: true,
  step: (resultado) => {
    // Processa linha por linha — não carrega tudo na memória
    processarLinha(resultado.data);
  },
  complete: (resultado) => {
    console.log(`${resultado.data.length} linhas processadas.`);
  },
  error: (erro) => {
    console.error("Erro:", erro.message);
  },
});

// Parse de URL remota
Papa.parse("https://dados.exemplo.com/relatorio.csv", {
  download: true,
  header: true,
  dynamicTyping: true,
  complete: (resultado) => {
    console.log(resultado.data);
  },
});

// Gerar CSV
const csv = Papa.unparse(dados, {
  quotes: true,    // sempre envolve campos em aspas
  delimiter: ";",  // ponto e vírgula para Excel BR
  newline: "
", // Windows line ending
});

JSON vs CSV — quando usar cada um

| Critério | JSON | CSV |

|----------|------|-----|

| Estrutura aninhada | ✅ Suporta objetos/arrays | ❌ Apenas tabelas planas |

| Tipos de dado | ✅ Preserva string, number, boolean, null | ⚠️ Tudo é string |

| Legibilidade | ✅ Clara e auto-descritiva | ✅ Simples para tabelas |

| Tamanho | ⚠️ Maior (chaves repetidas) | ✅ Compacto |

| Excel / Planilhas | ❌ Não abre diretamente | ✅ Abre com duplo clique |

| APIs REST | ✅ Padrão da indústria | ⚠️ Raro |

| Exportação de relatórios | ⚠️ Necessita processamento | ✅ Ideal |

| Big Data / Analytics | ⚠️ Pesado para volumes grandes | ✅ Eficiente |

| Configuração | ✅ Ideal | ❌ Não faz sentido |


Exemplo completo — importador de contatos

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Importador de Contatos CSV</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Segoe UI', sans-serif;
      background: #f0f2f5;
      padding: 2rem 1rem;
    }
    .app {
      max-width: 700px;
      margin: 0 auto;
      background: white;
      border-radius: 12px;
      padding: 2rem;
      box-shadow: 0 4px 24px rgba(0,0,0,.08);
    }
    h1 { margin-bottom: 1.5rem; font-size: 1.4rem; }
    .zona-drop {
      border: 2px dashed #d0d5dd;
      border-radius: 10px;
      padding: 2.5rem;
      text-align: center;
      cursor: pointer;
      transition: all .2s;
      margin-bottom: 1.5rem;
      color: #667085;
    }
    .zona-drop:hover, .zona-drop.arrastando {
      border-color: #5c6bc0;
      background: #f5f4ff;
      color: #5c6bc0;
    }
    .zona-drop input { display: none; }
    .zona-drop .icone { font-size: 2.5rem; margin-bottom: .5rem; }
    .acoes {
      display: flex;
      gap: .75rem;
      margin-bottom: 1.5rem;
      flex-wrap: wrap;
    }
    button {
      padding: .6rem 1.25rem;
      border: none;
      border-radius: 8px;
      cursor: pointer;
      font-size: .9rem;
      font-weight: 600;
      transition: all .2s;
    }
    .btn-primary { background: #5c6bc0; color: white; }
    .btn-primary:hover { background: #3949ab; }
    .btn-secondary { background: #f2f4f7; color: #344054; }
    .btn-secondary:hover { background: #e4e7ec; }
    .btn-danger { background: #fef3f2; color: #b42318; }
    .stats {
      display: flex;
      gap: 1rem;
      margin-bottom: 1.5rem;
      flex-wrap: wrap;
    }
    .stat {
      background: #f9fafb;
      border: 1px solid #eaecf0;
      border-radius: 8px;
      padding: .75rem 1.25rem;
      font-size: .85rem;
    }
    .stat strong { display: block; font-size: 1.4rem; color: #5c6bc0; }
    table {
      width: 100%;
      border-collapse: collapse;
      font-size: .9rem;
    }
    th {
      background: #f9fafb;
      padding: .65rem 1rem;
      text-align: left;
      font-size: .8rem;
      color: #667085;
      text-transform: uppercase;
      letter-spacing: .05em;
      border-bottom: 1px solid #eaecf0;
    }
    td {
      padding: .65rem 1rem;
      border-bottom: 1px solid #f2f4f7;
      color: #344054;
    }
    tr:last-child td { border: none; }
    tr:hover td { background: #f9fafb; }
    .badge {
      display: inline-block;
      padding: .15rem .6rem;
      border-radius: 999px;
      font-size: .75rem;
      font-weight: 600;
    }
    .badge-sim { background: #ecfdf3; color: #027a48; }
    .badge-nao { background: #fef3f2; color: #b42318; }
    .vazio { text-align: center; color: #98a2b3; padding: 2rem; }
    .erro { background: #fef3f2; border: 1px solid #fecdca;
            border-radius: 8px; padding: 1rem; color: #b42318;
            margin-bottom: 1rem; display: none; }
    .erro.visivel { display: block; }
  </style>
</head>
<body>
<div class="app">
  <h1>📋 Importador de Contatos</h1>

  <div class="zona-drop" id="zona-drop">
    <div class="icone">📂</div>
    <p><strong>Arraste um arquivo CSV aqui</strong></p>
    <p style="font-size:.85rem; margin-top:.4rem">ou clique para selecionar</p>
    <input type="file" id="input-arquivo" accept=".csv,.tsv,.txt">
  </div>

  <div class="erro" id="erro"></div>

  <div class="acoes">
    <button class="btn-primary" id="btn-exportar" disabled>⬇ Exportar CSV</button>
    <button class="btn-secondary" id="btn-exemplo">📄 Carregar exemplo</button>
    <button class="btn-danger" id="btn-limpar" disabled>🗑 Limpar</button>
  </div>

  <div class="stats" id="stats" style="display:none"></div>
  <div id="tabela-container">
    <p class="vazio">Nenhum dado carregado. Importe um CSV para começar.</p>
  </div>
</div>

<script>
  // ── Dados de exemplo ──────────────────────────────
  const csvExemplo = `nome,email,telefone,cidade,ativo
Ana Paula,ana@email.com,(11) 99999-0001,São Paulo,true
Carlos Silva,carlos@email.com,(41) 99999-0002,Curitiba,false
Beatriz Costa,bia@email.com,(21) 99999-0003,Rio de Janeiro,true
Diego Mendes,diego@email.com,(51) 99999-0004,Porto Alegre,true
Elena Souza,elena@email.com,(31) 99999-0005,Belo Horizonte,false`;

  // ── Estado ────────────────────────────────────────
  let contatosCarregados = [];

  // ── Referências ───────────────────────────────────
  const zonaDropEl = document.querySelector("#zona-drop");
  const inputArquivo = document.querySelector("#input-arquivo");
  const erroEl = document.querySelector("#erro");
  const statsEl = document.querySelector("#stats");
  const tabelaContainer = document.querySelector("#tabela-container");
  const btnExportar = document.querySelector("#btn-exportar");
  const btnLimpar = document.querySelector("#btn-limpar");

  // ── Parser CSV ────────────────────────────────────
  function parsearLinha(linha, delimitador) {
    const resultado = [];
    let campo = "";
    let dentroDeAspas = false;

    for (let i = 0; i < linha.length; i++) {
      const char = linha[i];
      if (char === '"' && !dentroDeAspas) { dentroDeAspas = true; continue; }
      if (char === '"' && dentroDeAspas) {
        if (linha[i + 1] === '"') { campo += '"'; i++; }
        else { dentroDeAspas = false; }
        continue;
      }
      if (char === delimitador && !dentroDeAspas) {
        resultado.push(campo.trim());
        campo = "";
        continue;
      }
      campo += char;
    }

    resultado.push(campo.trim());
    return resultado;
  }

  function detectarDelimitador(linha) {
    const opts = [",", ";", "	", "|"];
    return opts.sort((a, b) =>
      (linha.split(b).length - 1) - (linha.split(a).length - 1)
    )[0];
  }

  function parsearCSV(texto) {
    const linhas = texto.trim().split(/
?
/).filter(l => l.trim());
    if (linhas.length < 2) throw new Error("CSV precisa ter pelo menos cabeçalho e uma linha.");

    const delimitador = detectarDelimitador(linhas[0]);
    const cabecalho = parsearLinha(linhas[0], delimitador);

    return linhas.slice(1).map((linha, i) => {
      const valores = parsearLinha(linha, delimitador);
      if (valores.length !== cabecalho.length) {
        throw new Error(`Linha ${i + 2} tem ${valores.length} campos (esperado: ${cabecalho.length}).`);
      }
      return cabecalho.reduce((obj, col, j) => {
        const val = valores[j];
        obj[col] = val === "true" ? true : val === "false" ? false : val;
        return obj;
      }, {});
    });
  }

  function gerarCSV(dados) {
    if (!dados.length) return "";
    const cols = Object.keys(dados[0]);

    function escapar(v) {
      const s = String(v ?? "");
      return s.includes(",") || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
    }

    return [
      cols.join(","),
      ...dados.map(row => cols.map(c => escapar(row[c])).join(","))
    ].join("
");
  }

  // ── Renderização ──────────────────────────────────
  function renderizarTabela(dados) {
    if (!dados.length) {
      tabelaContainer.innerHTML = '<p class="vazio">Nenhum registro encontrado.</p>';
      statsEl.style.display = "none";
      return;
    }

    const colunas = Object.keys(dados[0]);
    const ativos = dados.filter(d => d.ativo === true).length;

    statsEl.style.display = "flex";
    statsEl.innerHTML = `
      <div class="stat"><strong>${dados.length}</strong> Contatos</div>
      <div class="stat"><strong>${ativos}</strong> Ativos</div>
      <div class="stat"><strong>${dados.length - ativos}</strong> Inativos</div>
      <div class="stat"><strong>${colunas.length}</strong> Colunas</div>
    `;

    const thead = `<thead><tr>${colunas.map(c =>
      `<th>${c.charAt(0).toUpperCase() + c.slice(1)}</th>`
    ).join("")}</tr></thead>`;

    const tbody = `<tbody>${dados.map(row =>
      `<tr>${colunas.map(col => {
        const val = row[col];
        if (typeof val === "boolean") {
          return `<td><span class="badge ${val ? "badge-sim" : "badge-nao"}">${val ? "Sim" : "Não"}</span></td>`;
        }
        return `<td>${val ?? ""}</td>`;
      }).join("")}</tr>`
    ).join("")}</tbody>`;

    tabelaContainer.innerHTML = `<table>${thead}${tbody}</table>`;
  }

  function mostrarErro(msg) {
    erroEl.textContent = `⚠️ ${msg}`;
    erroEl.classList.add("visivel");
    setTimeout(() => erroEl.classList.remove("visivel"), 5000);
  }

  function carregarDados(texto, origem = "arquivo") {
    try {
      erroEl.classList.remove("visivel");
      contatosCarregados = parsearCSV(texto);
      renderizarTabela(contatosCarregados);
      btnExportar.disabled = false;
      btnLimpar.disabled = false;
      console.log(`✅ ${contatosCarregados.length} contatos carregados de ${origem}.`);
    } catch (erro) {
      mostrarErro(erro.message);
    }
  }

  // ── Eventos ───────────────────────────────────────
  zonaDropEl.addEventListener("click", () => inputArquivo.click());

  inputArquivo.addEventListener("change", (e) => {
    const arquivo = e.target.files[0];
    if (!arquivo) return;
    const reader = new FileReader();
    reader.onload = (ev) => carregarDados(ev.target.result, arquivo.name);
    reader.readAsText(arquivo, "UTF-8");
  });

  zonaDropEl.addEventListener("dragover", (e) => {
    e.preventDefault();
    zonaDropEl.classList.add("arrastando");
  });

  zonaDropEl.addEventListener("dragleave", () => {
    zonaDropEl.classList.remove("arrastando");
  });

  zonaDropEl.addEventListener("drop", (e) => {
    e.preventDefault();
    zonaDropEl.classList.remove("arrastando");
    const arquivo = e.dataTransfer.files[0];
    if (!arquivo) return;
    const reader = new FileReader();
    reader.onload = (ev) => carregarDados(ev.target.result, arquivo.name);
    reader.readAsText(arquivo, "UTF-8");
  });

  document.querySelector("#btn-exemplo").addEventListener("click", () => {
    carregarDados(csvExemplo, "exemplo");
  });

  btnExportar.addEventListener("click", () => {
    if (!contatosCarregados.length) return;
    const bom = "uFEFF";
    const blob = new Blob([bom + gerarCSV(contatosCarregados)], {
      type: "text/csv;charset=utf-8;"
    });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = "contatos-exportados.csv";
    link.click();
    setTimeout(() => URL.revokeObjectURL(url), 100);
  });

  btnLimpar.addEventListener("click", () => {
    contatosCarregados = [];
    tabelaContainer.innerHTML = '<p class="vazio">Nenhum dado carregado. Importe um CSV para começar.</p>';
    statsEl.style.display = "none";
    btnExportar.disabled = true;
    btnLimpar.disabled = true;
    inputArquivo.value = "";
  });
</script>
</body>
</html>

Tarefa para você

Construa um analisador de vendas que:

1. Leia o CSV abaixo (cole como string no código)

data,produto,categoria,quantidade,preco_unitario,vendedor
2025-01-05,Notebook,Eletrônicos,2,3500,Ana
2025-01-07,Mouse,Eletrônicos,5,89.90,Bruno
2025-01-10,Cadeira,Móveis,1,900,Ana
2025-01-12,Teclado,Eletrônicos,3,199.90,Carlos
2025-01-15,Mesa,Móveis,2,700,Bruno
2025-01-18,Monitor,Eletrônicos,4,1200,Ana
2025-01-20,Notebook,Eletrônicos,1,3500,Carlos

2. Calcule e exiba:
   a) Total de receita geral
   b) Receita por categoria
   c) Receita por vendedor (quem vendeu mais?)
   d) Produto mais vendido (por quantidade)
   e) Ticket médio por venda

3. Exporte um relatório resumido em CSV com:
   vendedor, total_vendas, total_receita, ticket_medio

Conclusão

Neste artigo você aprendeu:

  • O que é CSV e suas regras de formatação
  • Como parsear CSV simples e com campos complexos (aspas, vírgulas internas)
  • Conversão automática de tipos após o parse
  • Como gerar CSV a partir de objetos JavaScript
  • Como exportar CSV para download no navegador
  • Como ler arquivos CSV enviados pelo usuário
  • Detecção automática de delimitadores
  • Quando usar a biblioteca PapaParse
  • A comparação prática entre JSON e CSV

📚 Fontes e Referências

  • MDN Web Docs — FileReader: https://developer.mozilla.org/pt-BR/docs/Web/API/FileReader
  • MDN Web Docs — Blob: https://developer.mozilla.org/pt-BR/docs/Web/API/Blob
  • MDN Web Docs — URL.createObjectURL: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
  • RFC 4180 — Common Format for CSV Files: https://datatracker.ietf.org/doc/html/rfc4180
  • PapaParse — Fast CSV Parser for JavaScript: https://www.papaparse.com
  • JavaScript.info — File and FileReader: https://javascript.info/file
  • Node.js Design Patterns — Mario Casciaro (Packt Publishing)
  • CSV Lint — Validador online de CSV: https://csvlint.io
Comentários

Mais em DevOps

Artigo 28 — State do Terraform: Entendendo o Arquivo Mais Crítico do Projeto
Artigo 28 — State do Terraform: Entendendo o Arquivo Mais Crítico do Projeto

Artigo 28 — State do Terraform: Entendendo o Arquivo Mais Crítico do Projeto...

Gerando e Revisando Pipelines com IA
Gerando e Revisando Pipelines com IA

O GitHub Copilot foi lançado em 2021. O ChatGPT em novembro de 2022. O Cursor...

Variáveis de Ambiente e Secrets em Pipelines
Variáveis de Ambiente e Secrets em Pipelines

Em abril de 2022, a empresa de segurança GitGuardian publicou um relatório re...