Ir al contenido
OneKitly

Limpiar texto desordenado: el orden de las operaciones que sí importa

Publicado el 8/7/2026 · 15 min de lectura · Herramientas de texto e idioma

Daniel Okonkwo

Daniel OkonkwoDesarrollador front-end y redactor de Tecnología en OneKitly

Rendimiento web · Formatos de archivo

Verificado con 5 fuentes

Ver perfil
En resumen

La limpieza de texto es un pipeline cuyos pasos no conmutan: las mismas operaciones en otro orden producen otro texto, y el daño es silencioso. Ejecutado en Node, decodificar entidades HTML antes de quitar etiquetas convierte el texto escapado &lt;b&gt; en un elemento <b> real que el limpiador borra después: «escribe <b> para mostrar una etiqueta» sale como «escribe para mostrar una etiqueta». Quitar etiquetas primero y decodificar exactamente una vez reproduce lo que muestra un navegador. Colapsar espacios con /\s+/ antes de decidir el tratamiento de los saltos de línea aplasta tres párrafos en una sola tirada de 80 caracteres, y ningún paso posterior reconstruye las fronteras: colapsa solo los espacios horizontales, con /[^\S\n]+/, después de las uniones. Deduplicar líneas antes de recortarlas no encuentra nada, porque «alpha» y «alpha» con un espacio final son cadenas distintas: cinco líneas siguen siendo cinco; recorta primero y esas mismas cinco bajan a tres. Tres caracteres sobreviven entonces a cualquier pasada ingenua: U+00A0 (reconocido por \s, eliminado por trim), U+200B (reconocido por ninguno) y U+FEFF, que \s sí reconoce pero la propiedad Unicode White_Space no. Arregla el orden, luego los invisibles, y solo después mide.

Quitar etiquetas antes de decodificar entidades, recortar antes de deduplicar, colapsar los espacios al final. Tres órdenes ejecutados en Node, un pipeline de nueve pasos en la secuencia correcta y los caracteres invisibles — U+00A0, U+200B, U+FEFF — que sobreviven a cualquier limpieza ingenua.

Los pasos de limpieza no conmutan

Un limpiador de texto parece un menú: quitar espacios sobrantes, quitar saltos de línea, quitar líneas duplicadas, quitar etiquetas HTML. Cada entrada suena autónoma, así que la gente las marca en el orden en que la interfaz las lista. No son autónomas. Cada paso reescribe la entrada que verá el siguiente, y varias parejas dan un resultado distinto según cuál corra primero. Los fallos son silenciosos: sigues obteniendo texto, sigue pareciendo plausible, y lo que perdiste es justo lo que no estabas mirando.

Tres parejas causan casi todo el daño real: etiquetas contra entidades, espacios contra saltos de línea y deduplicación contra recorte. Cada una se demuestra abajo sobre una cadena real, ejecutada en lugar de razonada. Los nueve pasos ordenados son simplemente lo que se deduce de esas tres restricciones cuando se respetan todas a la vez.

Etiquetas antes que entidades, y decodificar una sola vez

Tomemos un fragmento HTML: <p>Terms &amp;amp; conditions: write &lt;b&gt; to show a bold tag.</p>. Un navegador lo muestra así: Terms &amp; conditions: write <b> to show a bold tag. Las secuencias escapadas son texto: el autor quería un ampersand literal y una etiqueta de negrita literal y visible.

Quita primero las etiquetas y decodifica una vez: obtienes exactamente esa salida del navegador. Decodifica primero y quita después: el &lt;b&gt; ya se ha convertido en un elemento <b> real cuando corre el limpiador, así que el limpiador lo borra. El resultado es «write to show a bold tag», con un espacio doble donde estaba el sujeto de la frase. Quita primero pero decodifica dos veces y obtienes el fallo contrario: «write <b> to show a bold tag» contiene ahora una etiqueta viva que ya nada escapa, y así es como un extracto en texto plano vuelve a ser marcado en cuanto alguien lo pega en una página.

La regla detrás de los tres resultados es corta: una entidad es texto escapado, y decodificar asciende el texto a marcado. Todo lo que trate el marcado de forma especial tiene que correr, por tanto, antes del ascenso. Los navegadores evitan la cuestión tokenizando una sola vez, en una pasada, y por eso un analizador real nunca tiene que decidirlo. Un pipeline de regex sí, y la decisión es: quitar, luego decodificar, exactamente una decodificación.

Los espacios después de los saltos de línea, nunca a través de ellos

Toma un documento de tres párrafos, algunos partidos en varias líneas, con una o tres líneas en blanco perdidas entre ellos. Aplica primero /\s+/ sustituido por un solo espacio, porque «quitar espacios sobrantes» era la primera casilla, y obtienes una tirada de 80 caracteres sin ninguna frontera de párrafo. Nada aguas abajo puede restaurarlas: los saltos de línea que llevaban la estructura eran espacios, y pediste colapsar los espacios.

Pasa el mismo documento por el orden correcto — normalizar finales de línea, recortar cada línea, unir las líneas partidas, colapsar solo las series horizontales con /[^\S\n]+/, y luego reducir tres saltos o más a una línea en blanco — y sale con 58 caracteres en dos párrafos, con la estructura intacta. La diferencia no es que una regex sea mejor que la otra. Es que /\s+/ incluye \n y \r, y una clase como [^\S\n] deliberadamente no.

Hay una segunda ordenación, más sutil, dentro de esta. Toma «Paragraph one text.», una línea que contiene un solo espacio, y «Paragraph two text.». Divide por /\n\n/ y encuentras un párrafo, no dos, porque la línea separadora no está vacía: contiene un espacio. Recorta cada línea primero y la misma división encuentra dos. Por eso el recorte de líneas va antes de cualquier decisión sobre párrafos, mientras que el colapso de series va después: las dos operaciones sobre espacios quedan a lados opuestos del paso de los saltos de línea.

Deduplicar al final, porque la igualdad es una propiedad de aguas abajo

La deduplicación de líneas compara cadenas enteras. Dale las cinco líneas «alpha» con un espacio final, «beta», «alpha», «beta» seguida de un tabulador y «gamma», y no quita nada: cinco líneas entran, cinco salen, porque «alpha» y «alpha» más un espacio son simplemente cadenas distintas, igual que «beta» y «beta» más un tabulador. Recorta primero los espacios finales y la misma deduplicación baja las cinco a tres. Los duplicados siempre estuvieron ahí; eran invisibles exactamente como es invisible un espacio final.

Lo mismo ocurre en el ejemplo trabajado al final de este artículo. Colocada en segundo lugar, justo después de quitar las etiquetas, la deduplicación quita cero líneas. La misma deduplicación colocada en octavo lugar quita una, porque para entonces el espacio de ancho cero ha desaparecido y el espacio doble se ha colapsado, así que las dos líneas que siempre fueron la misma frase por fin son la misma cadena. La deduplicación no encuentra duplicados; la normalización los crea, y la deduplicación luego los recoge.

Una decisión sigue siendo tuya: las mayúsculas. La deduplicación compara exactamente, así que «Total» y «total» son dos líneas. El plegado de mayúsculas es una elección aparte y con pérdida, que merece su propio paso y debe verse en la interfaz en vez de quedar enterrada en el deduplicador — el mismo argumento que vale para las convenciones de nombres, donde la transformación solo es segura si sabes de qué convención partes.

Los caracteres que sobreviven a cualquier limpieza ingenua

Tres puntos de código producen casi todo el residuo. U+00A0, el espacio duro, llega de los procesadores de texto, de las páginas web y de la tipografía francesa y española, donde se coloca antes de dos puntos o después de una comilla de apertura. U+200B, el espacio de ancho cero, llega de los editores de CMS y del texto enriquecido pegado, como pista invisible de corte de línea. U+FEFF, el espacio de ancho cero irrompible, es lo que resulta de decodificar una marca de orden de bytes UTF-8; aparece al principio de los ficheros producidos por exportaciones de hoja de cálculo y por herramientas de Windows, y a veces en medio tras una concatenación ingenua.

Lo que importa para un pipeline de limpieza es cuáles de ellos ven tus herramientas, y la respuesta no es intuitiva. Ejecutado en Node 22: /\s/ reconoce U+00A0 y U+FEFF pero no U+200B. El escape de propiedad Unicode /\p{White_Space}/u reconoce U+00A0 pero no U+FEFF. Discrepan, en ambos sentidos: U+0085, el control NEL, lo reconoce la propiedad Unicode y no /\s/. La razón es que la gramática de ECMAScript define su propia producción WhiteSpace, que añade la marca de orden de bytes por motivos históricos, mientras que la base de caracteres Unicode asigna White_Space con sus propios criterios y no se la da a U+FEFF.

La normalización no sustituye a nada. Aplicar NFKC lleva U+00A0 a un U+0020 normal, lo que es realmente útil, pero deja U+200B exactamente donde estaba: la cadena sigue midiendo un carácter después. Así que el pipeline necesita un paso de borrado explícito para los caracteres de formato — U+200B, U+200C, U+200D, U+FEFF, U+00AD — y un paso de conversión aparte para los espacios exóticos. Ninguno se puede delegar en una regex de espacios, porque para una regex de espacios la mitad de ellos no son espacios.

Un ejemplo desordenado, nueve pasos, antes y después

El ejemplo tiene 126 caracteres y contiene, a propósito, todos los problemas comentados arriba: dos espacios iniciales, finales de línea CRLF, un h2 y tres elementos p, un ampersand doblemente codificado, un espacio de ancho cero pegado al final de una frase, tres saltos de línea seguidos, una frase repetida literalmente, un espacio interno duplicado, un párrafo partido en dos líneas con la continuación indentada, y una marca de orden de bytes más dos espacios justo al final.

Pasado por los nueve pasos en orden, baja a 58 caracteres: una línea de título «Q3 &amp; Q4 report», una línea «Revenue rose 12%.», una línea en blanco, y luego «Costs fell» y «slightly.» como dos líneas de un mismo párrafo. Las longitudes intermedias son 122 tras normalizar los finales de línea, 92 tras quitar las etiquetas, 88 tras la única decodificación de entidad, 86 una vez borrados el espacio de ancho cero y la marca de orden de bytes, 80 tras recortar cada línea, 77 tras el colapso horizontal, 76 tras reducir el triple salto, y 58 tras la deduplicación.

Dos de esos números son el argumento de todo el artículo. La caída de 76 a 58 es la línea duplicada, y solo existe porque los pasos 4 y 8 corrieron antes; mueve la deduplicación al segundo puesto y esa caída vale cero. El paso de 86 a 80 es el recorte línea a línea, y es lo que después permite a la división en párrafos ver una línea separadora realmente vacía. Todo lo demás es contabilidad.

Antes de entregar el texto limpio

Conserva el original. Cada paso de este pipeline es destructivo por diseño, y ninguno es reversible: no puedes recuperar qué espacios eran duros, qué salto de línea era un corte y cuál un párrafo, ni cuál de dos líneas idénticas era la que querías conservar. El texto limpio es un artefacto derivado, y un artefacto derivado nunca debe ser la única copia.

Luego pasa el pipeline dos veces sobre su propia salida. Una limpieza bien ordenada es idempotente: la segunda pasada no debe cambiar nada. Si cambia algo, tienes un paso que no es estable bajo repetición, y en la práctica casi siempre es la decodificación de entidades: la única operación de la lista que puede crearse trabajo nuevo a sí misma. Una comprobación de idempotencia cuesta una línea de código y atrapa la clase de fallo que solo aparece cuando un documento vuelve a pasar por la herramienta seis meses después, porque alguien lo reimportó.

Por último, cuenta solo al final. Cualquier longitud, recuento de palabras o índice de legibilidad tomado a mitad del pipeline mide una cadena que ya no existe, y una longitud depende en particular de qué estés dispuesto a llamar carácter: una cuestión que conviene zanjar aparte antes de fiarte de cualquier cifra que te dé un contador.

Seis caracteres invisibles, y si las herramientas de espacios de JavaScript los ven. Ejecutado en Node 22: U+FEFF y U+0085 discrepan en sentidos opuestos, porque la producción WhiteSpace de ECMAScript y la propiedad Unicode White_Space no son la misma lista.
CarácterPunto de código/\s/ lo reconoce/\p{White_Space}/u lo reconoce.trim() lo elimina
Espacio duroU+00A0
Espacio duro estrechoU+202F
Espacio de ancho ceroU+200BNoNoNo
Espacio de ancho cero irrompible, la marca de orden de bytesU+FEFFNo
Línea siguienteU+0085NoNo
Guion blandoU+00ADNoNoNo
Eliminar espacios de másReduce los espacios repetidos y recorta cada línea para limpiar el texto.Probar la herramienta

Preguntas frecuentes

Si ejecuto el limpiador dos veces, ¿sigue importando el orden?
Sí, y ejecutarlo dos veces puede empeorar las cosas. La repetición no recrea la información que destruyó un paso anterior: las fronteras de párrafo colapsadas siguen colapsadas por muchas pasadas que hagas. Mientras tanto, la decodificación de entidades no es idempotente: una segunda pasada decodifica &amp;amp; otra vez, convirtiendo un ampersand literal deliberado del texto en uno estructural. La prueba correcta no es ejecutarlo dos veces para mejorar el resultado, sino ejecutarlo dos veces para confirmar que la segunda pasada no cambia nada.
¿Por qué .trim() elimina la marca de orden de bytes pero no el espacio de ancho cero?
Porque trim se define contra la producción WhiteSpace de ECMAScript, no contra la propiedad Unicode White_Space, y esa producción lista explícitamente U+FEFF por razones históricas que vienen de cuando la marca de orden de bytes se encontraba habitualmente al principio de un flujo. U+200B nunca ha estado en esa lista: Unicode lo clasifica como carácter de formato en la categoría general Cf, con el argumento de que marca una oportunidad de corte de línea y no un espacio entre palabras. Así que trim quita uno y no el otro, y ni /\s/ ni trim te ayudarán jamás con U+200B. Bórralo explícitamente.
¿Hay alguna buena razón para usar /\s+/ sobre un documento entero?
Sí, en exactamente una situación: cuando has decidido que la salida es una sola línea y la estructura es irrelevante — una clave de búsqueda, una huella para comparar, un valor que va a una celda CSV de una línea. Ahí, aplanarlo todo a espacios simples es precisamente el objetivo. En cualquier sitio donde la salida vaya a leerla una persona, /\s+/ es la clase equivocada, porque trata el salto de línea que separa dos párrafos y los dos espacios tras un punto como la misma cosa. Úsala a propósito para claves y nunca por defecto para prosa.
¿Cómo veo siquiera que hay un carácter invisible?
Compara la longitud que esperas con la que obtienes, y luego vuelca los puntos de código. Dos cadenas que se ven idénticas en pantalla pueden tener longitudes distintas: esa es toda la pista. Cuando la longitud te sorprenda, lista cada carácter con su punto de código en hexadecimal y el culpable salta a la vista: un U+00A0 donde suponías un U+0020, o un U+200B pegado al final de una frase. Hacerlo una vez sobre una muestra de cada fuente que importas te dirá qué productores de tu cadena emiten qué caracteres, y entonces podrás borrar exactamente esos.
¿Debe la deduplicación conservar el orden original de las líneas?
Casi siempre sí, y debe conservar la primera aparición, no la última. Ordenar para localizar duplicados es una costumbre heredada de los pipelines de línea de comandos, y reordena en silencio un documento cuyo orden llevaba significado: una lista de pasos, un registro de cambios, una transcripción. Conservar la primera aparición también coincide con cómo lee la gente: la línea anterior suele ser la que tiene contexto alrededor. Si una herramienta ofrece ordenar mientras deduplica, trátalo como dos operaciones separadas y pide solo la que quieres.

Artículos que podrían interesarte

Todas las guías
GuíaQuitar el Markdown: lo que el texto plano pierde, y en qué se equivoca una regexUn enlace se convierte en texto con su destino borrado, una lista anidada pierde su jerarquía, una tabla se vuelve una fila de palabras. Luego la mitad técnica: el markdown no tiene una especificación única, y un limpiador a base de regex destroza un nombre de archivo, un signo de multiplicación y el interior de un bloque de código — todo confrontado con un analizador de verdad.GuíaQuitar HTML con criterio: lo que un eliminador de etiquetas puede y no puede hacerQuitar etiquetas y sanear HTML son dos trabajos distintos. Un fragmento real pasado por una regex ingenua y por un limpiador consciente del formato, con el contenido de script y style, los cortes de bloque, los comentarios, los CDATA y el orden de las entidades mostrados en la salida.ExplicaciónMayúscula inicial y mayúsculas de título: las reglas cambian según el idiomaLas mayúsculas de título inglesas tienen tres cortes distintos según el manual de estilo. El español, el francés, el portugués y el italiano no tienen ninguno. El alemán pone en mayúscula cada sustantivo. La herramienta no sabe nada de esto: aquí está exactamente lo que hace.ExplicaciónEncontrar duplicados en una lista sin hoja de cálculoDos líneas que parecen idénticas a menudo no lo son. La caja, un espacio final, un espacio duro y dos codificaciones distintas de la misma letra acentuada se pasaron por el buscador de duplicados, y no señaló ninguno en tres de los cuatro casos.GuíaLimpiar una lista pegada desde una hoja de cálculo o un PDFUn pegado arrastra caracteres que no se ven: espacios duros, guiones opcionales, espacios de ancho cero, tabulaciones y CRLF. Se pasaron cuatro herramientas de limpieza por cada uno, y usan tres definiciones distintas de espacio.ExplicaciónDetectar el idioma de un texto y por qué los textos cortos fallanMedido, no afirmado: 90 frases cortas reales en seis idiomas, ninguna rechazada y 68 acertadas — un 76%, que baja al 64% por debajo de dieciséis letras. Cuatro de las respuestas erróneas volvieron con un 100% de confianza.

Herramientas relacionadas

Fuentes

¿Has detectado un error en este artículo?