Como remover linhas de texto duplicadas em JavaScript
Remova linhas duplicadas em JavaScript com Set e trate maiúsculas, espaços, linhas em branco, quebras de linha e qual ocorrência manter.
Para remover linhas duplicadas em JavaScript, divida o texto com /\r?\n/, passe as linhas por um Set e junte-as novamente: [...new Set(text.split(/\r?\n/))].join('\n').
Isso é útil para limpar uma lista de e-mails ou IDs antes de uma importação em massa, organizar um arquivo de log, mesclar duas exportações CSV ou eliminar repetições de uma lista de palavras. A solução de uma linha resolve duplicatas exatas, e a ferramenta de remoção de linhas duplicadas faz isso direto no navegador, caso você queira apenas o resultado. Para quase-duplicatas, que diferem apenas em maiúsculas/minúsculas, espaços em branco, quebras de linha ou linhas em branco, este artigo traz uma função pronta para copiar, com opção de manter a primeira ou a última ocorrência.
Principais pontos
[...new Set(lines)]remove linhas duplicadas exatas e mantém cada linha na posição em que apareceu pela primeira vez, porque umSeté iterado na ordem de inserção.- Um
Setcompara strings de forma exata, entãoApple,appleeapple(com um espaço no final) contam como três linhas diferentes. - Faça a deduplicação com base em uma chave normalizada (sem espaços nas extremidades e, opcionalmente, em minúsculas), mas retorne a linha original, para preservar a capitalização e a indentação.
- Chamar
set()em uma chave já existente de umMapsubstitui o valor, mas mantém a posição original da chave. Chamedelete()antes para mover a chave para o final. sort -uordena a saída e, por isso, perde a ordem original.awk '!seen[$0]++'preserva a ordem de entrada.
A solução de uma linha para remover linhas duplicadas em JavaScript
A forma mais curta de remover linhas duplicadas em JavaScript é colocar um Set entre o split e o join:
const dedupeLines = (text) => [...new Set(text.split(/\r?\n/))].join('\n');
dedupeLines('one\nthree\ntwo\nthree\none');
// 'one\nthree\ntwo'
Isso funciona porque um Set itera seus valores na ordem de inserção. Cada linha permanece na posição de sua primeira ocorrência, e as cópias posteriores são descartadas. A especificação ES2015 define essa ordem: add acrescenta cada novo valor ao final da lista interna do conjunto. Para entradas limpas e geradas por máquina, isso é tudo de que você precisa. O guia de métodos de Set em JavaScript cobre o restante da API de Set.
Onde a versão ingênua com Set falha?
Um Set compara strings de forma exata (usando SameValueZero), então qualquer diferença, mesmo de um único caractere, torna duas linhas distintas. Cada entrada abaixo mantém uma linha que você provavelmente queria descartar:
- Maiúsculas/minúsculas:
'Apple\napple'mantém as duas linhas. - Espaço em branco no final:
'apple \napple'mantém as duas, porque uma das linhas termina com um espaço. - Quebras de linha CRLF:
'apple\r\napple'.split('\n')resulta em['apple\r', 'apple']. Dividir apenas por'\n'deixa um\rno final de todas as linhas, exceto na última linha sem quebra de linha depois dela, de modo que essa última linha nunca corresponde às suas cópias anteriores. O separador regex nosplit/\r?\n/resolve isso. Se você espera texto com quebras de linha do Mac OS clássico (um\risolado), use/\r\n|\r|\n/. - Linhas em branco:
'a\n\nb\n\nc'vira'a\n\nb\nc'. A segunda linha em branco é tratada como duplicata da primeira, então os parágrafosbecficam grudados. - Quebra de linha no final:
'a\n\nb\n'vira'a\n\nb'. A string vazia após a última quebra de linha corresponde à linha em branco anterior e é removida.
Evite lines.filter((l, i, a) => a.indexOf(l) === i). indexOf percorre o array linearmente, então executá-lo para cada linha torna a passagem inteira O(n²). A especificação exige que as buscas em um Set sejam, em média, mais rápidas que lineares, então um Set evita essa varredura repetida. Um objeto simples usado como lista de itens já vistos tem seus próprios problemas: uma linha com o texto __proto__ ou hasOwnProperty pode quebrá-lo.
Deduplique com uma chave normalizada e retorne a linha original
Para tratar quase-duplicatas como iguais, compare uma chave normalizada (a linha sem espaços nas extremidades e em minúsculas, se você quiser uma comparação que ignore maiúsculas/minúsculas) com o Set, mas adicione a linha original à saída.
function removeDuplicateLines(text, options = {}) {
const {
caseSensitive = true,
trim = true,
collapseWhitespace = false,
blankLines = 'collapse', // 'keep' | 'collapse' | 'remove'
} = options;
const eol = text.includes('\r\n') ? '\r\n' : '\n';
const trailingNewline = /\r?\n$/.test(text);
const lines = text.replace(/\r?\n$/, '').split(/\r?\n/);
const toKey = (line) => {
let key = trim ? line.trim() : line;
if (collapseWhitespace) key = key.replace(/\s+/g, ' ');
return caseSensitive ? key : key.toLowerCase();
};
const isBlank = (line) => line.trim() === '';
const seen = new Set();
const out = [];
for (const line of lines) {
if (isBlank(line)) {
if (blankLines === 'remove') continue;
if (blankLines === 'collapse' && out.length && isBlank(out[out.length - 1])) continue;
out.push(line);
continue;
}
const key = toKey(line);
if (seen.has(key)) continue;
seen.add(key);
out.push(line);
}
return out.join(eol) + (trailingNewline ? eol : '');
}
removeDuplicateLines('Apple\napple \n\n\nbanana\nBanana\n', { caseSensitive: false });
// 'Apple\n\nbanana\n'
Como as opções de removeDuplicateLines se comportam:
trimusaString.prototype.trim, que também remove espaços não separáveis (no-break spaces) e um BOM no início. A saída preserva a indentação original.caseSensitive: falseusatoLowerCase, que produz o mesmo resultado em qualquer locale.blankLines: 'keep'deixa as linhas em branco fora da deduplicação, então elas permanecem onde estão.'collapse'transforma cada sequência de linhas em branco em uma só; ela verifica a última linha da saída, então duas linhas em branco com uma duplicata descartada entre elas também são colapsadas.'remove'exclui todas as linhas em branco.- A função preserva a quebra de linha final da entrada. Se houver qualquer
\r\nna entrada, a saída usa\r\nem todas as linhas. Essa é uma simplificação para arquivos com quebras de linha mistas.
Um Map de chave para linha funciona da mesma forma: if (!map.has(key)) map.set(key, line). Consulte Map vs Set vs Object para saber quando usar cada estrutura. Normalização Unicode, aspas tipográficas e outros resquícios de colagem pertencem a uma etapa separada, abordada em Limpando o texto que os usuários colam no seu app.
Primeira vs. última ocorrência
Ao remover linhas duplicadas, manter a primeira ocorrência ou manter a última produz saídas diferentes. Manter a última também tem duas variantes, que diferem na ordem. Para a entrada ['apple', 'banana', 'Apple', 'cherry'], comparada sem diferenciar maiúsculas de minúsculas:
| Estratégia | Resultado |
|---|---|
Primeira vence (Set de chaves) | apple, banana, cherry |
Última vence, apenas sobrescrevendo (map.set) | Apple, banana, cherry |
| Última vence, com delete seguido de set | banana, Apple, cherry |
function keepLast(lines, toKey = (l) => l.trim().toLowerCase(), { moveToEnd = true } = {}) {
const byKey = new Map();
for (const line of lines) {
const key = toKey(line);
if (moveToEnd) byKey.delete(key);
byKey.set(key, line);
}
return [...byKey.values()];
}
Um Map mantém suas chaves na ordem em que foram adicionadas pela primeira vez, e chamar set() em uma chave que ele já contém altera o valor sem mover a chave. Uma passagem que apenas sobrescreve, portanto, retorna a última versão de cada linha, mas na posição em que a linha apareceu pela primeira vez. Para posicionar cada linha onde ela apareceu por último, chame delete(key) antes de set(key, line), para que a chave seja adicionada novamente no final.
Remova linhas duplicadas de arquivos grandes no terminal
Para arquivos grandes demais para colar, remova linhas duplicadas no shell com sort -u ou awk. O sort -u remove duplicatas ordenando as linhas, então a ordem original é perdida. Ele também compara as linhas usando as regras de collation do locale atual, que, em locales diferentes de C, podem mesclar linhas que não são idênticas byte a byte; por isso, use LC_ALL=C sort -u para correspondência exata.
awk '!seen[$0]++' imprime cada linha na primeira vez em que ela aparece e preserva a ordem de entrada, mas armazena cada linha única em um array associativo mantido em memória. awk '!seen[tolower($0)]++' ignora maiúsculas/minúsculas. Em arquivos CRLF, o \r é levado para as duas ferramentas, então remova-o antes com tr -d '\r'.
LC_ALL=C sort -u input.txt > output.txt
awk '!seen[$0]++' input.txt > output.txt
tr -d '\r' < input.txt | awk '!seen[$0]++' > output.txt
Conclusão
Use new Set para duplicatas exatas. Quando a entrada vem de pessoas ou de várias fontes, deduplique com base em uma chave normalizada e retorne a linha original. Escolha deliberadamente uma política para linhas em branco e uma regra de primeira ou última ocorrência, em vez de aceitar os padrões. Copie removeDuplicateLines, ajuste as opções de acordo com seus dados e, se quiser agrupar linhas relacionadas em vez de removê-las, use Object.groupBy.
Perguntas frequentes
Como manter apenas as linhas que aparecem exatamente uma vez em JavaScript?
Conte as ocorrências primeiro e depois filtre. Monte um Map de linha para contagem em uma única passagem e, em seguida, mantenha as linhas cuja contagem seja 1 com lines.filter((line) => counts.get(line) === 1). Isso não é deduplicação, que mantém uma cópia de cada linha repetida. Aqui, qualquer linha que apareça duas ou mais vezes é removida por completo. Faça a contagem com base em uma chave normalizada se quase-duplicatas devem contar como a mesma linha.
Como remover de uma lista as linhas que já aparecem em outra lista?
Carregue a segunda lista em um Set e filtre a primeira lista com base nele: const exclude = new Set(listB); listA.filter((line) => !exclude.has(line)). Cada verificação é uma busca em um Set, então você evita o custo quadrático de chamar includes ou indexOf em um array para cada linha. Divida os dois textos com /\r?\n/ antes e aplique a mesma chave com trim ou lowercase aos dois lados.
Por que duas linhas que parecem idênticas sobrevivem à deduplicação?
Geralmente elas contêm code points Unicode diferentes. Uma letra acentuada como é pode ser armazenada como um único caractere pré-composto ou como um e simples seguido de um acento combinante. Um Set compara code units de forma exata, então trata as duas linhas como diferentes. Adicione line.normalize('NFC') à função de chave para fazê-las corresponder. O trim não remove espaços de largura zero (U+200B), então elimine-os separadamente.
Como remover linhas duplicadas de um arquivo grande no Node.js sem carregar o arquivo inteiro em uma string?
Leia o arquivo como stream com o módulo node:readline e verifique cada linha em um Set. Crie a interface com createInterface({ input: createReadStream(path), crlfDelay: Infinity }), percorra-a com for await e escreva uma linha apenas se o Set ainda não a contiver. A opção crlfDelay: Infinity faz com que o readline trate cada CRLF como uma única quebra de linha. Assim como no awk, o consumo de memória ainda cresce com o número de linhas únicas.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k