Cómo eliminar líneas de texto duplicadas en JavaScript
Elimina líneas duplicadas en JavaScript con Set y gestiona mayúsculas, espacios, líneas en blanco, saltos de línea y qué aparición conservar.
Para eliminar líneas duplicadas en JavaScript, divide el texto con /\r?\n/, pasa las líneas por un Set y vuelve a unirlas: [...new Set(text.split(/\r?\n/))].join('\n').
Esto resulta útil para depurar una lista de correos electrónicos o IDs antes de una importación masiva, limpiar un archivo de log, combinar dos exportaciones CSV o eliminar repeticiones de una lista de palabras. La solución de una línea cubre los duplicados exactos, y la herramienta para eliminar líneas duplicadas lo hace directamente en el navegador si solo necesitas el resultado. Para los casi duplicados, que solo difieren en mayúsculas y minúsculas, espacios en blanco, finales de línea o líneas en blanco, este artículo ofrece una función lista para copiar con la opción de conservar la primera o la última aparición.
Puntos clave
[...new Set(lines)]elimina las líneas duplicadas exactas y mantiene cada línea en la posición donde apareció por primera vez, porque unSetse recorre en orden de inserción.- Un
Setcompara las cadenas de forma exacta, así queApple,appleyapple(con un espacio final) cuentan como tres líneas distintas. - Deduplica a partir de una clave normalizada (recortada y, opcionalmente, en minúsculas), pero devuelve la línea original para conservar las mayúsculas y la sangría.
- Llamar a
set()sobre una clave existente de unMapreemplaza el valor, pero mantiene la posición original de la clave. Llama primero adelete()para mover la clave al final. sort -uordena la salida y, por tanto, pierde el orden original.awk '!seen[$0]++'conserva el orden de entrada.
La solución de una línea para eliminar líneas duplicadas en JavaScript
La forma más breve de eliminar líneas duplicadas en JavaScript es colocar un Set entre split y join:
const dedupeLines = (text) => [...new Set(text.split(/\r?\n/))].join('\n');
dedupeLines('one\nthree\ntwo\nthree\none');
// 'one\nthree\ntwo'
Esto funciona porque un Set recorre sus valores en orden de inserción. Cada línea se queda en la posición de su primera aparición y las copias posteriores se descartan. La especificación ES2015 define este orden: add añade cada valor nuevo al final de la lista interna del conjunto. Para una entrada limpia generada por máquina, no necesitas nada más. La guía de métodos de Set en JavaScript cubre el resto de la API de Set.
¿Dónde falla la versión ingenua con Set?
Un Set compara las cadenas de forma exacta (mediante SameValueZero), por lo que basta con que difieran en un solo carácter para que dos líneas se consideren distintas. Cada una de las siguientes entradas conserva una línea que probablemente querías eliminar:
- Mayúsculas y minúsculas:
'Apple\napple'conserva ambas líneas. - Espacios en blanco finales:
'apple \napple'conserva ambas, porque una de las líneas termina en un espacio. - Finales de línea CRLF:
'apple\r\napple'.split('\n')devuelve['apple\r', 'apple']. Dividir solo por'\n'deja un\ral final de cada línea, salvo en la última si no va seguida de un salto de línea, de modo que esa última línea nunca coincide con sus copias anteriores. El separador de expresión regular ensplit/\r?\n/lo soluciona. Si esperas texto con finales de línea del Mac OS clásico (un\raislado), usa/\r\n|\r|\n/. - Líneas en blanco:
'a\n\nb\n\nc'se convierte en'a\n\nb\nc'. La segunda línea en blanco cuenta como duplicado de la primera, así que los párrafosbycquedan pegados. - Salto de línea final:
'a\n\nb\n'se convierte en'a\n\nb'. La cadena vacía que sigue al último salto de línea coincide con la línea en blanco anterior y se elimina.
Evita lines.filter((l, i, a) => a.indexOf(l) === i). indexOf recorre el array de forma lineal, así que hacerlo para cada línea convierte el recorrido completo en O(n²). La especificación exige que las búsquedas en un Set sean, de media, más rápidas que lineales, por lo que un Set evita ese recorrido repetido. Usar un objeto plano como registro de elementos vistos tiene sus propios problemas: una línea con el texto __proto__ o hasOwnProperty puede romperlo.
Deduplica con una clave normalizada y devuelve la línea original
Para tratar los casi duplicados como iguales, compara contra el Set una clave normalizada (la línea recortada, en minúsculas si quieres una comparación que no distinga mayúsculas), pero añade a la salida la línea original.
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'
Así se comportan las opciones de removeDuplicateLines:
trimusaString.prototype.trim, que también elimina los espacios de no separación y un BOM inicial. La salida conserva la sangría original.caseSensitive: falseusatoLowerCase, que produce el mismo resultado en cualquier configuración regional.blankLines: 'keep'excluye las líneas en blanco de la deduplicación, de modo que permanecen en su sitio.'collapse'reduce cada secuencia de líneas en blanco a una sola; comprueba la última línea de la salida, así que dos líneas en blanco con un duplicado descartado entre ellas también se reducen a una.'remove'elimina todas las líneas en blanco.- La función conserva el salto de línea final de la entrada. Si aparece algún
\r\nen la entrada, la salida usa\r\nen todas las líneas. Se trata de una simplificación para archivos con finales de línea mixtos.
Un Map de clave a línea funciona igual: if (!map.has(key)) map.set(key, line). Consulta Map vs Set vs Object para saber cuándo conviene cada estructura. La normalización Unicode, las comillas tipográficas y otros residuos del pegado de texto corresponden a un paso aparte, que se trata en Cleaning Up Text People Paste Into Your App.
Primera frente a última aparición
Al eliminar líneas duplicadas, conservar la primera aparición o la última produce resultados distintos. Además, conservar la última tiene dos variantes que difieren en el orden. Para la entrada ['apple', 'banana', 'Apple', 'cherry'], comparada sin distinguir mayúsculas y minúsculas:
| Estrategia | Resultado |
|---|---|
Gana la primera (Set de claves) | apple, banana, cherry |
Gana la última, solo sobrescribiendo (map.set) | Apple, banana, cherry |
| Gana la última, con delete y luego 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()];
}
Un Map conserva sus claves en el orden en que se añadieron por primera vez, y llamar a set() sobre una clave que ya contiene cambia el valor sin mover la clave. Por eso, un recorrido que solo sobrescribe devuelve la última versión de cada línea, pero en la posición donde la línea apareció por primera vez. Para colocar cada línea donde apareció por última vez, llama a delete(key) antes de set(key, line), de modo que la clave se vuelva a añadir al final.
Eliminar líneas duplicadas de archivos grandes desde la terminal
Para archivos demasiado grandes como para pegarlos, elimina las líneas duplicadas desde la shell con sort -u o awk. sort -u elimina los duplicados ordenando, por lo que se pierde el orden original. Además, compara las líneas según la intercalación de la configuración regional activa, lo que en configuraciones distintas de C puede fusionar líneas que no son idénticas byte a byte; usa LC_ALL=C sort -u para obtener coincidencias exactas.
awk '!seen[$0]++' imprime cada línea la primera vez que aparece y conserva el orden de entrada, pero almacena cada línea única en un array asociativo que se mantiene en memoria. awk '!seen[tolower($0)]++' ignora las mayúsculas y minúsculas. En los archivos CRLF, el \r llega a ambas herramientas, así que elimínalo primero con 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
Conclusión
Usa new Set para los duplicados exactos. Cuando la entrada proviene de personas o de varias fuentes, deduplica con una clave normalizada y devuelve la línea original. Elige de forma deliberada una política para las líneas en blanco y una regla de primera o última aparición, en lugar de aceptar los valores por defecto. Copia removeDuplicateLines, ajusta las opciones a tus datos y, si lo que quieres es agrupar líneas relacionadas en lugar de eliminarlas, usa Object.groupBy.
Preguntas frecuentes
¿Cómo conservo solo las líneas que aparecen exactamente una vez en JavaScript?
Cuenta primero las apariciones y después filtra. Construye en un solo recorrido un Map de línea a recuento y luego conserva las líneas cuyo recuento sea 1 con lines.filter((line) => counts.get(line) === 1). Esto no es deduplicación, que conserva una copia de cada línea repetida. Aquí, cualquier línea que aparezca dos o más veces se elimina por completo. Cuenta a partir de una clave normalizada si los casi duplicados deben considerarse la misma línea.
¿Cómo elimino de una lista las líneas que ya aparecen en otra?
Carga la segunda lista en un Set y filtra la primera contra él: const exclude = new Set(listB); listA.filter((line) => !exclude.has(line)). Cada comprobación es una búsqueda en un Set, así que evitas el coste cuadrático de llamar a includes o indexOf sobre un array para cada línea. Divide antes ambos textos con /\r?\n/ y aplica la misma clave recortada o en minúsculas a ambos lados.
¿Por qué sobreviven a la deduplicación dos líneas que parecen idénticas?
Normalmente contienen puntos de código Unicode distintos. Una letra acentuada como é puede almacenarse como un único carácter precompuesto o como una e simple seguida de un acento combinable. Un Set compara las unidades de código de forma exacta, así que trata ambas líneas como distintas. Añade line.normalize('NFC') a la función de clave para que coincidan. trim no elimina los espacios de ancho cero (U+200B), así que debes quitarlos por separado.
¿Cómo elimino líneas duplicadas de un archivo grande en Node.js sin leer el archivo completo en una cadena?
Procesa el archivo como stream con el módulo node:readline y comprueba cada línea contra un Set. Crea la interfaz con createInterface({ input: createReadStream(path), crlfDelay: Infinity }), recórrela con for await y escribe una línea solo si el Set aún no la contiene. La opción crlfDelay: Infinity hace que readline trate cada CRLF como un único salto de línea. Al igual que con awk, la memoria sigue creciendo con el número de líneas ú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