Usar expresiones regulares para analizar HTML: ¿por qué no?

Parece que todas las preguntas en stackoverflow donde el asker está usando regex para obtener algo de información de HTML inevitablemente tendrá una “respuesta” que dice que no se debe usar regex para analizar HTML.

Por qué no? Soy consciente de que hay analizadores de HTML “reales” entre comillas, como Beautiful Soup , y estoy seguro de que son poderosos y útiles, pero si solo estás haciendo algo simple, rápido o sucio, entonces ¿por qué? ¿Te molesta usar algo tan complicado cuando unas pocas declaraciones de expresiones regulares funcionarán bien?

Además, ¿hay algo fundamental que no entiendo acerca de la expresión regular que los convierte en una mala elección para el análisis en general?

El análisis completo de HTML no es posible con expresiones regulares, ya que depende de que coincidan las tags de apertura y cierre que no es posible con las expresiones regulares.

Las expresiones regulares solo pueden coincidir con los lenguajes normales, pero HTML es un lenguaje sin contexto y no un idioma normal (como señaló @StefanPochmann, los idiomas regulares también son sin contexto, por lo que el contexto no necesariamente significa no regular). Lo único que puede hacer con las expresiones regulares en HTML es la heurística, pero eso no funcionará en todas las condiciones. Debería ser posible presentar un archivo HTML que se emparejará incorrectamente con cualquier expresión regular.

Para quick’n’dirty regexp va a estar bien. Pero lo fundamental es saber que es imposible construir una expresión regular que analice correctamente HTML.

La razón es que las expresiones regulares no pueden manejar expresiones anidadas arbitrariamente. Consulte ¿Se pueden usar expresiones regulares para hacer coincidir patrones nesteds?

(De http://htmlparsing.com/regexes )

Supongamos que tiene un archivo de HTML en el que intenta extraer URL de tags .

 

Entonces escribes una expresión regular como esta en Perl:

 if ( $html =~ / 

En este caso, $url contendrá de hecho http://example.com/whatever.jpg . Pero, ¿qué sucede cuando comienzas a recibir HTML así?

  

o

  

o

  

o

  

o comienzas a obtener falsos positivos de

  

Parece tan simple, y podría ser simple para un solo archivo, sin cambios, pero para todo lo que vas a hacer en datos HTML arbitrarios, las expresiones regulares son solo una receta para futuros dolores de cabeza.

En lo que respecta al análisis, las expresiones regulares pueden ser útiles en la etapa de “análisis léxico” (lexer), donde la entrada se divide en tokens. Es menos útil en la etapa real de “construir un árbol de análisis sintáctico”.

Para un analizador HTML, esperaría que solo acepte HTML bien formado y que requiera capacidades fuera de lo que puede hacer una expresión regular (no pueden “contar” y asegurarse de que un número determinado de elementos de apertura se equilibra con el mismo número de los elementos de cierre).

Dos razones rápidas:

  • escribir una expresión regular que pueda hacer frente a entradas maliciosas es difícil; mucho más difícil que usar una herramienta preconstruida
  • escribir una expresión regular que pueda funcionar con el marcado ridículo con el que inevitablemente te quedarás atrapado es difícil; mucho más difícil que usar una herramienta preconstruida

En cuanto a la idoneidad de las expresiones regulares para el análisis en general: no son adecuadas. ¿Alguna vez has visto el tipo de expresiones regulares que necesitarías para analizar la mayoría de los idiomas?

Debido a que hay muchas formas de “arruinar” el HTML que los navegadores tratarán de una manera bastante liberal, requerirá bastante esfuerzo reproducir el comportamiento liberal del navegador para cubrir todos los casos con expresiones regulares, por lo que su expresión inevitablemente fallará en algún especial casos, y que posiblemente introducirían serias brechas de seguridad en su sistema.

El problema es que la mayoría de los usuarios que hacen una pregunta que tiene que ver con HTML y expresiones regulares lo hacen porque no pueden encontrar una expresión regular propia que funcione. Entonces uno tiene que pensar si todo sería más fácil cuando se usa un analizador DOM o SAX o algo similar. Están optimizados y construidos con el propósito de trabajar con estructuras de documentos similares a XML.

Claro, hay problemas que se pueden resolver fácilmente con expresiones regulares. Pero el énfasis recae fácilmente .

Si solo desea encontrar todas las URL que parecen http://.../ , está bien con las expresiones regulares. Pero si quiere encontrar todas las URL que están en un elemento a que tiene la clase ‘mylink’, probablemente sea mejor que use un analizador apropiado.

Las expresiones regulares no se diseñaron para manejar una estructura de tags anidadas, y en el mejor de los casos es complicado (en el peor de los casos, imposible) manejar todos los casos límite posibles que se obtienen con HTML real.

Creo que la respuesta está en la teoría de cálculo. Para que un lenguaje sea analizado usando regex, debe ser por definición “regular” ( enlace ). HTML no es un lenguaje normal, ya que no cumple con una serie de criterios para un idioma normal (mucho que ver con los muchos niveles de anidamiento inherentes al código html). Si está interesado en la teoría de la computación, le recomendaría este libro.

“Depende” sin embargo. Es cierto que las expresiones regulares no pueden analizar HTML con verdadera precisión, por todos los motivos que se dan aquí. Sin embargo, si las consecuencias de equivocarse (como no manejar tags anidadas) son menores, y si las expresiones regulares son muy convenientes en su entorno (como cuando está pirateando Perl), continúe.

Supongamos que está, quizás, analizando las páginas web que enlazan con su sitio, quizás las encontró con una búsqueda de enlaces de Google, y desea una forma rápida de obtener una idea general del contexto que rodea su enlace. Está intentando ejecutar un pequeño informe que podría alertarlo para que vincule el correo no deseado, algo así.

En ese caso, una mala interpretación de algunos de los documentos no será un gran problema. Nadie más que tú verá los errores, y si tienes mucha suerte habrá pocos que puedas seguir individualmente.

Supongo que estoy diciendo que es una compensación. A veces, la implementación o el uso de un analizador correcto, por más fácil que sea, puede no valer la pena si la precisión no es crítica.

Solo ten cuidado con tus suposiciones. Puedo pensar en algunas formas en que el atajo de expresiones regulares puede ser contraproducente si estás tratando de analizar algo que se mostrará en público, por ejemplo.

Definitivamente hay casos en los que usar una expresión regular para analizar información del HTML es la forma correcta de hacerlo; depende en gran medida de la situación específica.

El consenso anterior es que, en general, es una mala idea. Sin embargo, si la estructura HTML es conocida (y es poco probable que cambie), sigue siendo un enfoque válido.

Esta expresión recupera atributos de elementos HTML. Es compatible con:

  • atributos sin cita / cotizados,
  • comillas simples / dobles,
  • fugas de comillas dentro de los atributos,
  • espacios alrededor de signos iguales,
  • cualquier cantidad de atributos,
  • verifique solo los atributos dentro de las tags,
  • escapar de los comentarios, y
  • gestionar diferentes citas dentro de un valor de atributo.

(?:\< \!\-\-(?:(?!\-\-\>)\r\n?|\n|.)*?-\-\>)|(?:< (\S+)\s+(?=.*>)|(?< =[=\s])\G)(?:((?:(?!\s|=).)*)\s*?=\s*?[\"']?((?:(?<=\")(?:(?<=\\)\"|[^\"])*|(?<=')(?:(?<=\\)'|[^'])*)|(?:(?!\"|')(?:(?!\/>|>|\s).)+))[\"']?\s*)

Compruébalo . Funciona mejor con las banderas “gisx”, como en la demostración.

Tenga en cuenta que, aunque el HTML en sí mismo no es regular, las partes de una página que está mirando pueden ser regulares.

Por ejemplo, es un error para anidar las tags

; si la página web funciona correctamente, usar una expresión regular para tomar un

sería completamente razonable.

Hace poco realicé un raspado web usando solo selenium y expresiones regulares. Me salí con la suya porque los datos que quería se pusieron en una

y los puse en un formato de tabla simple (por lo que incluso podría contar con

,

y

para no anidar– que en realidad es muy inusual). En cierto grado, las expresiones regulares eran casi necesarias, porque parte de la estructura a la que tenía que acceder estaba delimitada por comentarios. (Beautiful Soup puede darle comentarios, pero hubiera sido difícil tomar y bloques usando Beautiful Soup).

Sin embargo, si tuviera que preocuparme por las tablas anidadas, ¡mi enfoque simplemente no habría funcionado! Hubiera tenido que recurrir a Beautiful Soup. Incluso entonces, sin embargo, a veces puede usar una expresión regular para tomar el trozo que necesita y luego profundizar desde allí.

En realidad, el análisis de HTML con expresiones regulares es perfectamente posible en PHP. Solo tienes que analizar toda la cadena al revés usando strrpos para encontrar < y repetir la expresión regular desde allí utilizando especificadores sin encoding cada vez para superar las tags anidadas. No es lujoso y terriblemente lento en cosas grandes, pero lo usé para mi propio editor personal de plantillas para mi sitio web. En realidad no estaba analizando HTML, pero algunas tags personalizadas que hice para consultar las entradas de la base de datos muestran tablas de datos (mi < #if()> podría resaltar las entradas especiales de esta manera). No estaba preparado para buscar un analizador XML en solo un par de tags creadas por uno mismo (con datos muy no XML dentro de ellas) aquí y allá.

Entonces, a pesar de que esta pregunta está considerablemente muerta, todavía aparece en una búsqueda en Google. Lo leí y pensé "desafío aceptado" y terminé de arreglar mi código simple sin tener que reemplazar todo. Decidió ofrecer una opinión diferente a cualquiera que busque una razón similar. Además, la última respuesta se publicó hace 4 horas, por lo que este sigue siendo un tema candente.

Probé mi mano en una expresión regular para esto también. Es principalmente útil para encontrar fragmentos de contenido emparejados con la próxima etiqueta HTML, y no busca tags de cierre coincidentes , pero recogerá tags de cierre. Tira una stack en tu propio idioma para verificarlos.

Usar con las opciones ‘sx’. ‘g’ también si te sientes afortunado:

 (?P.*?) # Content up to next tag (?P # Entire tag < !\[CDATA\[(?P.+?)]]>| # < ![CDATA[ ... ]]> | #  \w+)\s*>| #  < (?P\w+) #  (?P\s+ # : Use this part to get the attributes out of 'attributes' group. (?P\w+) (?:\s*=\s* (?P [\w:/.\-]+| # Unquoted (?=(?P<_v> # Quoted (?P<_q>['\"]).*?(?< !\\)(?P=_q))) (?P=_v) ))? #  )* )\s* (?P/?) # Self-closing indicator >) # End of tag 

Este está diseñado para Python (podría funcionar para otros idiomas, no lo ha probado, usa lookaheads positivos, lookbehinds negativos y referencias a nombres). Apoyos:

  • Abrir etiqueta –

  • Cerrar etiqueta –
  • Comentario –
  • CDATA – < ![CDATA[ ... ]]>
  • Etiqueta de cierre automático –

  • Valores de atributo opcionales –
  • Valores de atributo sin citar / cotizados –

  • Cotizaciones simples / dobles –

  • Citas escapadas –
    (Esto no es realmente HTML válido, pero soy un buen tipo)
  • Espacios alrededor de signos iguales –
  • Capturas con nombre para bits interesantes

También es muy bueno que no se activen en tags mal formadas, como cuando te olvidas de un < o > .

Si tu sabor regex admite repetidas capturas con nombre, entonces estás dorado, pero Python re no (sé que Regex sí, pero necesito usar Python vainilla). Esto es lo que obtienes:

  • content : todo el contenido hasta la próxima etiqueta. Podrías dejar esto fuera.
  • markup : la etiqueta completa con todo lo que contiene.
  • comment : si es un comentario, el contenido del comentario.
  • cdata : si es un < ![CDATA[...]]> , el contenido de CDATA.
  • close_tag : si es una etiqueta de cierre ( ), el nombre de la etiqueta.
  • tag : si es una etiqueta abierta (

    ), el nombre de la etiqueta.

  • attributes - Todos los atributos dentro de la etiqueta. Use esto para obtener todos los atributos si no obtiene grupos repetidos.
  • attribute - Repetido, cada atributo.
  • attribute_name - Repetido, cada nombre de atributo.
  • attribute_value - Repetido, cada valor de atributo. Esto incluye las cotizaciones si fue cotizado.
  • is_self_closing - Esto es / si es una etiqueta de cierre automático, de lo contrario nada.
  • _v y _v - Ignora estos; se usan internamente para backreferences.

Si su motor de expresiones regulares no admite repetidas capturas con nombre, hay una sección llamada que puede usar para obtener cada atributo. Simplemente ejecute esa expresión regular en el grupo de attributes para obtener cada attribute , attribute_name y attribute_value fuera de este.

Demostración aquí: https://regex101.com/r/mH8jSu/11

HTML / XML se divide en marcado y contenido.
Regex solo es útil para hacer un análisis léxico de las tags.
Supongo que puedes deducir el contenido.
Sería una buena opción para un analizador SAX.
Las tags y el contenido se pueden entregar a un usuario
función definida donde anidamiento / cierre de elementos
se puede mantener un seguimiento de.

En cuanto a solo analizar las tags, se puede hacer con
regex y utilizado para quitar tags de un documento.

Durante años de pruebas, he encontrado el secreto para
navegadores de manera analizan las tags, tanto bien como mal formadas.

Los elementos normales se analizan con esta forma:

El núcleo de estas tags usa esta expresión regular

  (?: " [\S\s]*? " | ' [\S\s]*? ' | [^>]? )+ 

Notarás esto [^>]? como una de las alternancias.
Esto coincidirá con las comillas no balanceadas de las tags mal formadas.

También es la raíz más simple de todo mal para las expresiones regulares.
La forma en que se usa activará un bache para satisfacer que es codicioso, debe coincidir
contenedor cuantificado.

Si se usa pasivamente, nunca hay un problema.
Pero, si fuerza algo para que coincida al intercalarlo con
un par atributo / valor deseado, y no proporcionan una protección adecuada
de retroceder, es una pesadilla fuera de control.

Esta es la forma general de las antiguas tags simples.
Observe que [\w:] representa el nombre de la etiqueta?
En realidad, los caracteres legales que representan el nombre de la etiqueta
son una increíble lista de caracteres Unicode.

  < (?: [\w:]+ \s+ (?: " [\S\s]*? " | ' [\S\s]*? ' | [^>]? )+ \s* /? ) > 

Continuando, también vemos que no puedes buscar una etiqueta específica
sin analizar TODAS las tags.
Quiero decir que podrías, pero debería usar una combinación de
verbos como (* SKIP) (* FAIL) pero aún así todas las tags tienen que ser analizadas.

La razón es que la syntax de las tags puede estar oculta dentro de otras tags, etc.

Entonces, para analizar pasivamente todas las tags, se necesita una expresión regular como la de abajo.
Este en particular coincide con el contenido invisible también.

Como nuevo HTML o xml o cualquier otro desarrollo de construcciones nuevas, simplemente agrégalo como
una de las alternancias


Nota de la página web: nunca he visto una página web (o xhtml / xml) que este
tuvo problemas con. Si encuentras uno, házmelo saber.

Nota de rendimiento: es rápido. Este es el analizador de tags más rápido que he visto
(Puede haber más rápido, quién sabe).
Tengo varias versiones específicas. También es excelente como raspador
(si eres del tipo práctico).


Regex completa en bruto

< (?:(?:(?:(script|style|object|embed|applet|noframes|noscript|noembed)(?:\s+(?>"[\S\s]*?"|'[\S\s]*?'|(?:(?!/>)[^>])?)+)?\s*>)[\S\s]*?))|(?:/?[\w:]+\s*/?)|(?:[\w:]+\s+(?:"[\S\s]*?"|'[\S\s]*?'|[^>]?)+\s*/?)|\?[\S\s]*?\?|(?:!(?:(?:DOCTYPE[\S\s]*?)|(?:\[CDATA\[[\S\s]*?\]\])|(?:--[\S\s]*?--)|(?:ATTLIST[\S\s]*?)|(?:ENTITY[\S\s]*?)|(?:ELEMENT[\S\s]*?))))>

Apariencia formateada

  < (?: (?: (?: # Invisible content; end tag req'd ( # (1 start) script | style | object | embed | applet | noframes | noscript | noembed ) # (1 end) (?: \s+ (?> " [\S\s]*? " | ' [\S\s]*? ' | (?: (?! /> ) [^>] )? )+ )? \s* > ) [\S\s]*?  ) ) | (?: /? [\w:]+ \s* /? ) | (?: [\w:]+ \s+ (?: " [\S\s]*? " | ' [\S\s]*? ' | [^>]? )+ \s* /? ) | \? [\S\s]*? \? | (?: ! (?: (?: DOCTYPE [\S\s]*? ) | (?: \[CDATA\[ [\S\s]*? \]\] ) | (?: -- [\S\s]*? -- ) | (?: ATTLIST [\S\s]*? ) | (?: ENTITY [\S\s]*? ) | (?: ELEMENT [\S\s]*? ) ) ) ) > 

Las expresiones regulares no son lo suficientemente potentes para un lenguaje como HTML. Claro, hay algunos ejemplos donde puedes usar expresiones regulares. Pero, en general, no es apropiado para el análisis sintáctico.

Usted, sabe … hay mucha mentalidad de usted NO PUEDE hacerlo y creo que todos los que están a ambos lados de la valla están en lo correcto y lo incorrecto. PUEDE hacerlo, pero lleva un poco más de procesamiento que simplemente ejecutar una expresión regular en su contra. Toma esto (lo escribí dentro de una hora) como ejemplo. Asume que el HTML es completamente válido, pero dependiendo del idioma que utilice para aplicar la expresión regular antes mencionada, puede corregir el código HTML para asegurarse de que tendrá éxito. Por ejemplo, eliminar tags de cierre que no deberían estar allí: por ejemplo. A continuación, agregue la barra diagonal de cierre simple de HTML a los elementos que les faltan, etc.

Utilizaría esto en el contexto de escribir una biblioteca que me permitiera realizar la recuperación de elementos HTML similar a la de [x].getElementsByTagName() de JavaScript, por ejemplo. Acabo de empalmar la funcionalidad que escribí en la sección DEFINE de la expresión regular y la uso para entrar dentro de un árbol de elementos, uno a la vez.

Entonces, ¿será esta la respuesta final al 100% para validar HTML? No. Pero es un comienzo y con un poco más de trabajo, se puede hacer. Sin embargo, tratar de hacerlo dentro de una ejecución de expresiones regulares no es práctico ni eficiente.