Ir al contenido
OneKitly

Quitar HTML con criterio: lo que un eliminador de etiquetas puede y no puede hacer

Publicado el 14/7/2026 · 16 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

Quitar etiquetas y sanear HTML son trabajos distintos. Un eliminador de etiquetas produce texto plano para mostrar, guardar o contar; un saneador produce HTML seguro para insertar en una página. Confundir ambos es como se escriben los fallos de inyección, y una regex jamás hará el segundo trabajo porque no modela el analizador: dale al patrón que reconoce un signo menor que, una tirada de caracteres que no son mayor que y un signo mayor que el fragmento <a title="fast > cheap" href="/x">our guide</a>, y se detiene en el primer mayor que que encuentra, emitiendo cheap" href="/x">our guide. Allí donde la regex y el tokenizador del navegador discrepan, la equivocada es la regex, y esas discrepancias no se pueden enumerar: por eso las listas negras fallan como clase y las defensas reales son el escapado contextual en el punto de inserción o un saneador de lista blanca basado en un analizador. Como herramienta de formato, en cambio, un eliminador tiene trabajo de verdad. Sobre un fragmento de 390 caracteres, el patrón ingenuo devuelve 183 caracteres que contienen el cuerpo de la hoja de estilos, una línea de JavaScript estropeada, la cola de un comentario, entidades sin decodificar y dos elementos de lista fundidos en StandardExpress. Elimina el contenido de los elementos de texto en bruto, convierte los finales de bloque en líneas en blanco y br en un salto de línea, decodifica las entidades una vez: 118 caracteres limpios.

Quitar 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.

Dos trabajos que parecen uno

Quitar etiquetas significa producir texto plano: algo que mostrar en un resultado de búsqueda, cuyas palabras contar, que poner en un correo de texto simple, que dar a un resumidor o que guardar en una columna que nunca se renderiza como marcado. Sanear significa producir HTML: marcado que se insertará en una página viva y que por tanto debe ser seguro allí. Los dos se parecen porque ambos toman HTML y devuelven algo más corto. No son el mismo trabajo, no tienen el mismo criterio de éxito, y solo uno de ellos es un control de seguridad.

Esta guía va del primer trabajo, bien hecho. La cuestión de seguridad se responde en la última sección, brevemente y sin ambigüedad, porque la respuesta honesta es corta: un eliminador de etiquetas no es una frontera de seguridad, y ninguna cantidad de patrones extra lo convierte en una. Todo lo que hay en medio es el trabajo de formato que un limpiador tiene que hacer de verdad y que la mayoría de las implementaciones falla: eliminar el contenido de script y style, convertir la estructura en saltos de línea, tratar los comentarios y los CDATA, y decodificar las entidades en el sitio correcto.

Lo que una regex de etiquetas ingenua le hace a un fragmento real

El fragmento de prueba tiene 390 caracteres de marcado de página corriente: un contenedor article, una hoja de estilos en línea, un encabezado con un ampersand codificado, un párrafo partido por un br y con una entidad de raya corta, un comentario HTML cuyo texto contiene un signo mayor que, una lista de dos elementos, un enlace cuyo atributo title contiene un mayor que codificado, y un script en línea. Nada exótico, nada hostil: así es una exportación de un CMS.

Ejecuta el clásico de una línea — reconocer un signo menor que, cualquier tirada de caracteres que no sean mayor que, y luego un mayor que, en global — y vuelven 183 caracteres. Entre ellos: el texto literal .lead{font-weight:700}, es decir, el cuerpo de la hoja de estilos convertido en prosa. La cadena var ok = a d;, es decir, el script amputado por el medio porque la regex leyó los operadores de comparación como una etiqueta. El fragmento 0 --> , es decir, la cola del comentario, que sobrevivió porque la regex se detuvo en el mayor que que contenía. Dos entidades sin decodificar. Y las palabras Standard y Express fundidas en StandardExpress, junto a una frase que termina en Monday.Delivery porque el br no aportó nada.

El mismo fragmento a través de un limpiador que sabe qué significan los elementos devuelve 118 caracteres, en cinco bloques: el encabezado, el párrafo de dos líneas con el corte del br respetado, los dos elementos de lista en sus propias líneas, y la frase final, con el ampersand y la raya corta decodificados, y sin rastro de la hoja de estilos, el script ni el comentario. La diferencia entre 183 y 118 caracteres no es compresión. Es la retirada de cosas que nunca fueron texto.

El contenido de script y style no son etiquetas

El mayor defecto de formato de los limpiadores ingenuos es que quitan las etiquetas de apertura y cierre de script y style y dejan todo lo que hay entre ellas. No es un problema cosmético menor: las reglas CSS de una página o el código de un script en línea aterrizan en medio de lo que debía ser prosa legible, y serán indexados, contados, resumidos y mostrados a alguien. En el fragmento de prueba, el bloque de declaraciones CSS aparece como la segunda línea de la salida, justo encima del encabezado.

La norma HTML los llama elementos de texto en bruto: dentro de script y style, el analizador deja de buscar etiquetas y lee hasta la etiqueta de cierre correspondiente. Por eso la regex ingenua también estropea lo que deja escapar. En el script de prueba, la expresión que contiene un menor que y un mayor que la lee la regex como una etiqueta y la borra, así que el JavaScript filtrado ni siquiera es el JavaScript original: es una versión acortada que por casualidad parece una frase. Dos modos de fallo en una sola construcción.

El arreglo es una regla puesta antes que todas las demás: reconocer la etiqueta de apertura, su contenido y su etiqueta de cierre como una sola unidad para script, style, template y noscript, y borrarlo todo. Ponla primera en el pipeline, porque una vez que las etiquetas han desaparecido ya no puedes saber qué texto estaba dentro de ellas.

Los bloques necesitan cortes, y br y p no son el mismo corte

Borrar una etiqueta borra el espacio que implicaba. Dos párrafos seguidos sin espacio entre ellos en el origen se convierten en OneTwo. Dos elementos de lista se convierten en AB. Un encabezado seguido de un párrafo se convierte en TB. Es el defecto que hace ilegible el texto limpiado y falsos los recuentos de palabras, y es invisible en pruebas siempre que el HTML de origen esté formateado con saltos de línea entre las etiquetas, que es justo por lo que llega a producción.

La correspondencia que produce una salida legible es corta. La etiqueta de cierre de un elemento de bloque — p, div, li, tr, h1 a h6, blockquote, section, article y los demás — se convierte en una línea en blanco. Un br se convierte en exactamente un salto de línea, porque un br es un corte dentro de un bloque, no un bloque nuevo. Un hr se convierte en una línea en blanco. Cualquier otra etiqueta se convierte en nada. Luego reduce las series de tres saltos de línea o más a una sola línea en blanco, porque un bloque anidado emite dos etiquetas de cierre y por tanto dos líneas en blanco.

Una advertencia honesta: esta correspondencia pone una línea en blanco entre elementos de lista, porque li es un bloque. Eso se lee raro en una lista compacta, y la versión más pulida trata li como un simple salto de línea y reserva la línea en blanco solo al ul u ol envolvente. Quererlo así es una preferencia de formato y no una cuestión de corrección, pero es una preferencia que hay que expresar explícitamente: un limpiador que nunca ha pensado en las listas simplemente las pegará, y eso no es una preferencia, es un fallo.

Comentarios, CDATA y entidades: tres gramáticas especiales

Un comentario HTML no es una etiqueta y no sigue la gramática de las etiquetas: va de una apertura de cuatro caracteres a un cierre de tres, y entre ambos puede aparecer absolutamente cualquier cosa, incluidos signos mayor que. Una regex de etiquetas que parece quitar comentarios se libra solo porque la mayoría de los comentarios no contiene ningún mayor que. Pon uno — una nota del tipo conservar si el stock es mayor que cero — y la regex se detiene ahí, dejando el resto del texto del comentario en tu salida. En el fragmento de prueba, el residuo visible es la cadena 0 seguida del terminador de comentario.

Las secciones CDATA son un caso vecino que sobrevive en la práctica dentro de SVG y de exportaciones de estilo XHTML. Su contenido no es por definición marcado, y su terminador tampoco es un signo mayor que. Pasa por la regex ingenua una sección CDATA que contenga texto con aspecto de marcado y obtendrás el texto interior con sus etiquetas interiores quitadas y la secuencia de corchetes de cierre colgando: un resultado equivocado de tres maneras a la vez. Reconoce y borra las secciones CDATA como una unidad, antes de la pasada general de etiquetas, exactamente igual que con los comentarios.

Las entidades van al final, y exactamente una vez. Decodifica después de que las etiquetas hayan desaparecido, nunca antes: un signo menor que escapado en el origen es texto que el autor quería visible, y decodificarlo primero lo asciende a etiqueta que el limpiador borra después. En un fragmento que dice usar un elemento em para el énfasis, con el nombre del elemento escapado, quitar y luego decodificar preserva el nombre visible del elemento, mientras que decodificar y luego quitar lo borra y deja un hueco. Decodifica dos veces y fabricas marcado vivo a partir de algo escapado a propósito, que es un fallo de formato camino de convertirse en uno peor.

Dónde está realmente la frontera de seguridad

Un eliminador de etiquetas es una herramienta de formato. No es una frontera de seguridad, y no se convierte en una añadiendo patrones. La razón es estructural y ya se ve en la tabla de arriba: el patrón ingenuo y el tokenizador del navegador discrepan sobre dónde acaba una etiqueta en cuanto un valor de atributo contiene un signo mayor que, y la versión consciente del formato discrepa en el mismo sitio. Cualquier filtro construido a base de patrones es una lista negra de las formas que a su autor se le ocurrieron, y una lista negra vale lo que la imaginación de su autor, mientras que el analizador es una especificación fija, publicada e independiente del adversario. No se pueden enumerar las maneras en que dos gramáticas difieren.

Las defensas correctas son dos, y ninguna es un quitado de etiquetas. La primera y más importante es el escapado contextual de salida: escapa el valor en el punto donde lo insertas, con la regla de escapado de ese contexto, porque las reglas difieren para texto HTML, valores de atributo, URL, script y style. Un motor de plantillas que escapa por defecto lo hace por ti, y los fallos ocurren donde alguien se salió de él. La segunda, necesaria solo cuando los usuarios están realmente autorizados a enviar marcado que debe renderizarse como marcado, es un saneador basado en un analizador: analiza la entrada en un árbol y la reconstruye a partir de una lista blanca de elementos y atributos, el modelo de la especificación HTML Sanitizer y de las bibliotecas de saneamiento establecidas. Lista blanca, analizador, no regex.

Hay una simplificación limpia que merece decirse. Si el texto limpiado es texto plano y sigue siéndolo — mostrado en un nodo de texto, escrito en una columna de texto, contado, enviado como correo de texto plano — entonces la salida del limpiador no tiene ningún papel de seguridad, porque ese texto ya no vuelve a analizarse como marcado. El peligro aparece solo cuando alguien coge el texto limpiado y lo devuelve a HTML. En ese momento el control relevante es el escapado en ese punto de inserción, y no nada de lo que el limpiador hiciera antes. Mantener esos dos momentos separados en la cabeza es casi todo lo que esta sección intenta enseñar.

Ocho construcciones HTML a través de una regex de etiquetas ingenua y de un limpiador consciente del formato. Cada columna de salida es la cadena literal que devolvió el código en Node 22.
EntradaLa regex ingenua devuelveEl limpiador consciente del formato devuelve
<p>One</p><p>Two</p>OneTwoOne, línea en blanco, Two
<p>One<br>Two</p>OneTwoOne, salto de línea simple, Two
<li>A</li><li>B</li>ABA y B en líneas separadas
<h2>T</h2><p>B</p>TBT, línea en blanco, B
Un elemento style que contiene .lead{font-weight:700}el texto CSS .lead{font-weight:700} como salida visiblenada: el elemento y su contenido se eliminan juntos
Un elemento script que contiene var ok = a < b && c > d;var ok = a d; — la parte central engullida como si fuera una etiquetanada
<p>A<!-- keep if stock > 0 -->B</p>A 0 -->BAB
<a title="fast > cheap" href="/x">our guide</a>cheap" href="/x">our guidetambién mal: ninguna regex arregla esto, solo un analizador
Eliminar etiquetas HTMLElimina todas las etiquetas HTML de un fragmento y deja solo el texto.Probar la herramienta

Preguntas frecuentes

¿Puedo simplemente usar el navegador y leer textContent?
Analizar con el analizador de la propia plataforma es el instinto correcto, y resuelve gratis los problemas de atributos y comentarios. Pero fíjate en lo que dan las dos propiedades. textContent devuelve el texto concatenado de todos los descendientes sin formato alguno, incluido el contenido de script y style, y sin saltos de línea para los bloques, así que reproduce dos de los defectos de este artículo. innerText aproxima el texto renderizado, sí inserta saltos de línea y sí omite los elementos ocultos, pero depende de la maquetación y por tanto del CSS. En un navegador, analiza la entrada en un documento inerte, elimina explícitamente los nodos script, style y template, y luego recorre el árbol emitiendo tus propios cortes. En un servidor sin DOM, una biblioteca real de análisis HTML te da el mismo árbol.
¿Debo truncar antes o después de quitar las etiquetas?
Después, siempre. Truncar HTML corta una etiqueta por la mitad o deja un elemento sin cerrar, y el fragmento resultante no es ni marcado válido ni texto sensato: un extracto que acaba en mitad de un valor de atributo es una fuente clásica de maquetaciones rotas cuando más tarde se inserta en algún sitio. Quita primero, luego trunca el texto plano en una frontera de palabra, y luego añade los puntos suspensivos. El mismo argumento vale para contar: un recuento de caracteres o palabras hecho sobre el marcado cuenta los nombres de etiqueta y los valores de atributo como contenido, así que está equivocado en la proporción de marcado que tuviera el origen, que en una exportación típica de CMS es una fracción grande.
Si el texto limpiado vuelve a una página, ¿decodifico las entidades?
Decodifícalo una vez para almacenarlo, y luego deja que la capa de salida lo vuelva a escapar cuando lo inserte. Suena a trabajo extra y es en realidad el único arreglo que se mantiene correcto: tu valor guardado es el texto real, y cada consumidor lo escapa para su propio contexto — texto HTML, un atributo, una URL, una celda CSV, un correo de texto plano. Si en cambio guardas la forma codificada y te saltas el escapado en la salida porque «ya está escapada», el primer consumidor que no sea HTML recibe secuencias de ampersand literales, y el primero que escape de todos modos lo codifica dos veces. Un valor decodificado canónico en el almacenamiento, escapado en cada frontera.
¿Se puede hacer seguro un limpiador de regex añadiendo más patrones?
No, y la razón merece interiorizarse porque se generaliza. Una lista de patrones codifica las formas que su autor anticipó; el analizador acepta todas las formas que define la especificación, incluido el comportamiento de recuperación ante entradas malformadas, que nadie escribe como regla. La seguridad solo se sigue si el modelo del lenguaje que tiene el filtro es al menos tan completo como el del consumidor, y una lista de patrones es por construcción menos completa. Ese es todo el argumento a favor de las listas blancas frente a las negras y de los analizadores frente a los patrones, y por eso este artículo te da un limpiador para el formato y te señala un saneador para la seguridad, en vez de vender una sola herramienta para ambas cosas.
¿Qué debe hacer el limpiador con imágenes, enlaces y tablas?
Decide a propósito y documenta la decisión, porque los tres llevan información que vive fuera del texto. Una imagen aporta su texto alternativo, que es la descripción accesible y a menudo la única frase que vale la pena conservar; eliminar el elemento en silencio la pierde. Un enlace aporta su texto de anclaje por defecto, y si el destino debe añadirse entre corchetes depende de si tu salida se leerá alguna vez sin forma de seguir enlaces. Una tabla necesita un separador de celdas y un separador de filas o cada fila se convierte en una tirada ilegible de valores concatenados: un tabulador entre celdas y un salto de línea entre filas es el mínimo habitual. Nada de esto lo deciden por ti los nombres de las etiquetas; es una elección editorial sobre qué debe preservar el texto plano.

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.TutorialLimpiar texto desordenado: el orden de las operaciones que sí importaQuitar 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.GuíaConstruir una URL con parámetros que sobreviva a un copiar y pegarTres codificaciones, una diferencia visible: %20 o +. El modo formulario del generador reproduce URLSearchParams byte a byte en diecisiete valores, pero dale una URL base con un fragmento y todos los parámetros acaban dentro del hash, donde ningún servidor los ve.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.GuíaLos límites de caracteres que de verdad muerden: unidades de código, puntos de código y grafemasUn carácter son tres cosas a la vez. Un emoji con tono de piel es 1 grafema, 2 puntos de código y 4 unidades UTF-16. Todos los recuentos de esta guía se midieron en Node, más por qué un SMS cae de 160 a 70 y por qué VARCHAR(255) no son 255 de nada en concreto.ExplicaciónContar palabras es ambiguo, y cada herramienta responde distintoUn recuento de palabras es una definición, no una medida. Contamos el mismo párrafo de cuatro formas y obtuvimos 25, 28, 33 y 38; luego contamos 50 000 caracteres de prosa corriente y obtuvimos acuerdo dentro del 4,5 %. La diferencia se debe por completo a compuestos, cifras y URL.

Herramientas relacionadas

Fuentes

¿Has detectado un error en este artículo?